Skip to content

Commit

Permalink
DownloadBuildArtifact: FileContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
scottdallamura committed Jun 12, 2017
1 parent fbd1ba7 commit e8b0215
Show file tree
Hide file tree
Showing 15 changed files with 4,538 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Tasks/DownloadBuildArtifact/ArtifactProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {BuildArtifact} from 'vso-node-api/interfaces/BuildInterfaces';

export interface ArtifactProvider {
supportsArtifactType(artifactType: string): boolean;
downloadArtifact(artifact: BuildArtifact, targetPath: string): Promise<void>;
}
81 changes: 81 additions & 0 deletions Tasks/DownloadBuildArtifact/Downloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as path from 'path';
import * as tl from 'vsts-task-lib/task';
import * as fs from 'fs';

/**
* Represents an item to be downloaded
*/
export interface DownloadItem<T> {
/**
* The path to the item, relative to the target path
*/
relativePath: string;

/**
* Artifact-specific data
*/
data: T;
}

/**
* Downloads items
* @param items the items to download
* @param targetPath the folder that will hold the downloaded items
* @param maxConcurrency the maximum number of items to download simultaneously
* @param downloader a function that, given a DownloadItem, will return a content stream
*/
export async function download<T>(items: DownloadItem<T>[], targetPath: string, maxConcurrency: number, downloader: (item: DownloadItem<T>) => Promise<fs.ReadStream>): Promise<void> {
// keep track of folders we've touched so we don't call mkdirP for every single file
let createdFolders: { [key: string]: boolean } = {};

maxConcurrency = Math.min(maxConcurrency, items.length);
let downloaders: Promise<{}>[] = [];
for (let i = 0; i < maxConcurrency; ++i) {
downloaders.push(new Promise(async (resolve, reject) => {
try {
while (items.length > 0) {
let item = items.pop();

// the full path of the downloaded file
let outputFilename = path.join(targetPath, item.relativePath);

// create the folder if necessary
let folder = path.dirname(outputFilename);
if (!createdFolders.hasOwnProperty(folder)) {
if (!tl.exist(folder)) {
tl.mkdirP(folder);
}
createdFolders[folder] = true;
}

tl.debug(`Downloading ${item.relativePath} to ${outputFilename}`);
await new Promise(async (downloadResolve, downloadReject) => {
try {
// get the content stream from the provider
let contentStream = await downloader(item);

// create the target stream
let outputStream = fs.createWriteStream(outputFilename);

// pipe the content to the target
contentStream.pipe(outputStream);
contentStream.on('end', () => {
tl.debug(`Downloaded ${item.relativePath} to ${outputFilename}`);
downloadResolve();
});
}
catch (err) {
downloadReject(err);
}
});
}
resolve();
}
catch (err) {
reject(err);
}
}));
}

await Promise.all(downloaders);
}
80 changes: 80 additions & 0 deletions Tasks/DownloadBuildArtifact/FileContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as tl from 'vsts-task-lib/task';
import * as fs from 'fs';
import * as path from 'path';

import {BuildArtifact} from 'vso-node-api/interfaces/BuildInterfaces';
import {FileContainerItem, ContainerItemType} from 'vso-node-api/interfaces/FileContainerInterfaces';
import {IFileContainerApi} from 'vso-node-api/FileContainerApi';
import {WebApi, getBearerHandler} from 'vso-node-api/WebApi';

import {DownloadItem, download} from './Downloader';
import {ArtifactProvider} from './ArtifactProvider';

export class FileContainerProvider implements ArtifactProvider {
public supportsArtifactType(artifactType: string): boolean {
return !!artifactType && artifactType.toLowerCase() === "container";
}

public async downloadArtifact(artifact: BuildArtifact, targetPath: string): Promise<void> {
if (!artifact || !artifact.resource || !artifact.resource.data) {
throw new Error(tl.loc("FileContainerInvalidArtifact"));
}

let containerParts: string[] = artifact.resource.data.split('/', 3);
if (containerParts.length !== 3) {
throw new Error(tl.loc("FileContainerInvalidArtifactData"));
}

let containerId: number = parseInt(containerParts[1]);
let containerPath: string = containerParts[2];

let accessToken = getAuthToken();
let credentialHandler = getBearerHandler(accessToken);
let collectionUrl = tl.getVariable("System.TeamFoundationCollectionUri");
let vssConnection = new WebApi(collectionUrl, credentialHandler);

let fileContainerApi = vssConnection.getFileContainerApi();

// get all items
let items: FileContainerItem[] = await fileContainerApi.getItems(containerId, null, containerPath, false, null, null, false, false);

// ignore folders
items = items.filter(item => item.itemType === ContainerItemType.File);
tl.debug(`Found ${items.length} File items in #/${containerId}/${containerPath}`);

let downloadItems: DownloadItem<FileContainerItem>[] = items.map((item) => {
return {
relativePath: item.path,
data: item
};
})

// download the items
await download(downloadItems, targetPath, 8, (item: DownloadItem<FileContainerItem>) => new Promise(async (resolve, reject) => {
try {
let downloadFilename = item.data.path.substring(item.data.path.lastIndexOf("/") + 1);
let itemResponse = await fileContainerApi.getItem(containerId, null, item.data.path, downloadFilename);
if (itemResponse.statusCode === 200) {
resolve(itemResponse.result);
}
else {
// TODO: decide whether to retry or bail
reject(itemResponse);
}
}
catch (err) {
reject(err);
}
}));
}
}

function getAuthToken() {
let auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false);
if (auth.scheme.toLowerCase() === 'oauth') {
return auth.parameters['AccessToken'];
}
else {
throw new Error(tl.loc("CredentialsNotFound"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"loc.friendlyName": "Download Build Artifact",
"loc.helpMarkDown": "",
"loc.description": "Download a build artifact.",
"loc.instanceNameFormat": "Download Build Artifact",
"loc.input.label.buildId": "Build",
"loc.input.help.buildId": "The build from which to download the artifact",
"loc.input.label.artifactName": "Artifact name",
"loc.input.help.artifactName": "The name of the artifact to download",
"loc.input.label.downloadPath": "Destination directory",
"loc.input.help.downloadPath": "Path on the agent machine where the artifact will be downloaded",
"loc.messages.FileContainerCredentialsNotFound": "Could not determine credentials to connect to file container service.",
"loc.messages.FileContainerInvalidArtifact": "Invalid file container artifact",
"loc.messages.FileContainerInvalidArtifactData": "Invalid file container artifact. Resource data must be in the format #/{container id}/path",
"loc.messages.ArtifactProviderNotFound": "Could not determine a provider to download artifact of type {0}"
}
48 changes: 48 additions & 0 deletions Tasks/DownloadBuildArtifact/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {BuildArtifact, ArtifactResource} from 'vso-node-api/interfaces/BuildInterfaces';
import {WebApi, getBearerHandler} from 'vso-node-api/WebApi';
import * as tl from 'vsts-task-lib/task';

import {ArtifactProvider} from './ArtifactProvider';
import {FileContainerProvider} from './FileContainer';

async function main(): Promise<void> {
let projectId = tl.getVariable('System.TeamProjectId');
let buildId = parseInt(tl.getInput("buildId"));
let artifactName = tl.getInput("artifactName");
let downloadPath = tl.getPathInput("downloadPath");

let accessToken = getAuthToken();
let credentialHandler = getBearerHandler(accessToken);
let collectionUrl = tl.getVariable("System.TeamFoundationCollectionUri");
let vssConnection = new WebApi(collectionUrl, credentialHandler);

// get the artifact metadata
let buildApi = vssConnection.getBuildApi();
let artifact = await buildApi.getArtifact(buildId, artifactName, projectId);

let providers: ArtifactProvider[] = [
new FileContainerProvider()
];

let provider = providers.filter((provider) => provider.supportsArtifactType(artifact.resource.type))[0];
if (provider) {
await provider.downloadArtifact(artifact, downloadPath);
}
else {
throw new Error(tl.loc("ArtifactProviderNotFound", artifact.resource.type));
}
}

function getAuthToken() {
let auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false);
if (auth.scheme.toLowerCase() === 'oauth') {
return auth.parameters['AccessToken'];
}
else {
throw new Error(tl.loc("CredentialsNotFound"))
}
}

main()
.then((result) => tl.setResult(tl.TaskResult.Succeeded, ""))
.catch((error) => tl.setResult(tl.TaskResult.Failed, error));
23 changes: 23 additions & 0 deletions Tasks/DownloadBuildArtifact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "downloadbuildartifact",
"version": "0.1.0",
"description": "Download Build Artifact Task",
"main": "download.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Microsoft/vsts-tasks.git"
},
"author": "Microsoft Corporation",
"license": "MIT",
"bugs": {
"url": "https://github.com/Microsoft/vsts-tasks/issues"
},
"homepage": "https://github.com/Microsoft/vsts-tasks#readme",
"dependencies": {
"vso-node-api": "^6.2.4-preview",
"vsts-task-lib": "^2.0.3-preview"
}
}
59 changes: 59 additions & 0 deletions Tasks/DownloadBuildArtifact/task.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"id": "a433f589-fce1-4460-9ee6-44a624aeb1fb",
"name": "DownloadBuildArtifact",
"friendlyName": "Download Build Artifact",
"description": "Download a build artifact.",
"helpMarkDown": "",
"category": "Utility",
"author": "Microsoft Corporation",
"version": {
"Major": 0,
"Minor": 1,
"Patch": 48
},
"demands": [],
"inputs": [
{
"name": "buildId",
"type": "pickList",
"label": "Build",
"required": false,
"helpMarkDown": "The build from which to download the artifact",
"defaultValue": "$(Build.BuildId)",
"options": {
"$(Build.BuildId)": "The current build"
}
},
{
"name": "artifactName",
"type": "string",
"label": "Artifact name",
"defaultValue": "drop",
"required": true,
"helpMarkDown": "The name of the artifact to download"
},
{
"name": "downloadPath",
"type": "string",
"label": "Destination directory",
"defaultValue": "$(System.ArtifactsDirectory)",
"required": true,
"helpMarkDown": "Path on the agent machine where the artifact will be downloaded"
}
],
"dataSourceBindings": [
],
"instanceNameFormat": "Download Build Artifact",
"execution": {
"Node": {
"target": "main.js",
"argumentFormat": ""
}
},
"messages": {
"FileContainerCredentialsNotFound": "Could not determine credentials to connect to file container service.",
"FileContainerInvalidArtifact": "Invalid file container artifact",
"FileContainerInvalidArtifactData": "Invalid file container artifact. Resource data must be in the format #/{container id}/path",
"ArtifactProviderNotFound": "Could not determine a provider to download artifact of type {0}"
}
}
58 changes: 58 additions & 0 deletions Tasks/DownloadBuildArtifact/task.loc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"id": "a433f589-fce1-4460-9ee6-44a624aeb1fb",
"name": "DownloadBuildArtifact",
"friendlyName": "ms-resource:loc.friendlyName",
"description": "ms-resource:loc.description",
"helpMarkDown": "ms-resource:loc.helpMarkDown",
"category": "Utility",
"author": "Microsoft Corporation",
"version": {
"Major": 0,
"Minor": 1,
"Patch": 47
},
"demands": [],
"inputs": [
{
"name": "buildId",
"type": "pickList",
"label": "ms-resource:loc.input.label.buildId",
"required": false,
"helpMarkDown": "ms-resource:loc.input.help.buildId",
"defaultValue": "$(Build.BuildId)",
"options": {
"$(Build.BuildId)": "The current build"
}
},
{
"name": "artifactName",
"type": "string",
"label": "ms-resource:loc.input.label.artifactName",
"defaultValue": "drop",
"required": true,
"helpMarkDown": "ms-resource:loc.input.help.artifactName"
},
{
"name": "downloadPath",
"type": "string",
"label": "ms-resource:loc.input.label.downloadPath",
"defaultValue": "$(System.ArtifactsDirectory)",
"required": true,
"helpMarkDown": "ms-resource:loc.input.help.downloadPath"
}
],
"dataSourceBindings": [],
"instanceNameFormat": "ms-resource:loc.instanceNameFormat",
"execution": {
"Node": {
"target": "main.js",
"argumentFormat": ""
}
},
"messages": {
"FileContainerCredentialsNotFound": "ms-resource:loc.messages.FileContainerCredentialsNotFound",
"FileContainerInvalidArtifact": "ms-resource:loc.messages.FileContainerInvalidArtifact",
"FileContainerInvalidArtifactData": "ms-resource:loc.messages.FileContainerInvalidArtifactData",
"ArtifactProviderNotFound": "ms-resource:loc.messages.ArtifactProviderNotFound"
}
}
6 changes: 6 additions & 0 deletions Tasks/DownloadBuildArtifact/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
}
}
Loading

0 comments on commit e8b0215

Please sign in to comment.