test output link

This commit is contained in:
Trym Lund Flogard
2021-06-05 13:13:36 +02:00
parent 8167ea6a3e
commit a48c618622
14 changed files with 15281 additions and 1326 deletions

View File

@ -1,64 +1,65 @@
import * as fsSync from 'fs'
import * as path from 'path'
import * as core from '@actions/core';
import * as os from 'os';
import * as archiver from 'archiver';
import fetch, { HeadersInit } from 'node-fetch';
import btoa from 'btoa';
import { v4 as uuidv4 } from 'uuid';
import * as core from '@actions/core'
import * as os from 'os'
import * as archiver from 'archiver'
import fetch, { HeadersInit } from 'node-fetch'
import btoa from 'btoa'
import { v4 as uuidv4 } from 'uuid'
import * as webdav from 'webdav'
const fs = fsSync.promises;
const fs = fsSync.promises
interface FileSpec {
absolutePath: string,
uploadPath: string
absolutePath: string
uploadPath: string
}
export class NextcloudClient {
private guid: string;
private headers: HeadersInit;
private davClient;
private guid: string
private headers: HeadersInit
private davClient
public constructor(
private endpoint: string,
private artifact: string,
private rootDirectory: string,
private username: string,
private password: string) {
this.guid = uuidv4();
this.headers = { 'Authorization': 'Basic ' + btoa(`${this.username}:${this.password}`) };
this.davClient = webdav.createClient(`${this.endpoint}/remote.php/dav/files/${this.username}`, {
username: this.username,
password: this.password,
});
constructor(
private endpoint: string,
private artifact: string,
private rootDirectory: string,
private username: string,
private password: string
) {
this.guid = uuidv4()
this.headers = { Authorization: 'Basic ' + btoa(`${this.username}:${this.password}`) }
this.davClient = webdav.createClient(`${this.endpoint}/remote.php/dav/files/${this.username}`, {
username: this.username,
password: this.password
})
}
async uploadFiles(files: string[]): Promise<string> {
core.info('Preparing upload...')
const spec = this.uploadSpec(files)
core.info('Zipping files...')
const zip = await this.zipFiles(spec)
core.info('Uploading to Nextcloud...')
const filePath = await this.upload(zip)
core.info(`File path: ${filePath}`)
core.info('Sharing file...')
return await this.shareFile(filePath)
}
private uploadSpec(files: string[]): FileSpec[] {
const specifications = []
if (!fsSync.existsSync(this.rootDirectory)) {
throw new Error(`this.rootDirectory ${this.rootDirectory} does not exist`)
}
public async uploadFiles(files: string[]) {
core.info("Preparing upload...");
const spec = this.uploadSpec(files);
core.info("Zipping files...");
var zip = await this.zipFiles(spec);
core.info("Uploading to Nextcloud...");
const path = await this.upload(zip);
core.info(`File path: ${path}`);
core.info("Sharing file...");
await this.shareFile(path);
if (!fsSync.lstatSync(this.rootDirectory).isDirectory()) {
throw new Error(`this.rootDirectory ${this.rootDirectory} is not a valid directory`)
}
private uploadSpec(files: string[]): FileSpec[] {
const specifications = [];
if (!fsSync.existsSync(this.rootDirectory)) {
throw new Error(`this.rootDirectory ${this.rootDirectory} does not exist`);
}
if (!fsSync.lstatSync(this.rootDirectory).isDirectory()) {
throw new Error(`this.rootDirectory ${this.rootDirectory} is not a valid directory`);
}
// Normalize and resolve, this allows for either absolute or relative paths to be used
let root = path.normalize(this.rootDirectory);
root = path.resolve(root);
/*
// Normalize and resolve, this allows for either absolute or relative paths to be used
let root = path.normalize(this.rootDirectory)
root = path.resolve(root)
/*
Example to demonstrate behavior
Input:
@ -77,20 +78,20 @@ export class NextcloudClient {
['/home/user/files/plz-upload/file1.txt', 'my-artifact/dir/file3.txt']
]
*/
for (let file of files) {
if (!fsSync.existsSync(file)) {
throw new Error(`File ${file} does not exist`);
}
if (!fsSync.lstatSync(file).isDirectory()) {
// Normalize and resolve, this allows for either absolute or relative paths to be used
file = path.normalize(file);
file = path.resolve(file);
if (!file.startsWith(root)) {
throw new Error(`The rootDirectory: ${root} is not a parent directory of the file: ${file}`);
}
// Check for forbidden characters in file paths that will be rejected during upload
const uploadPath = file.replace(root, '');
/*
for (let file of files) {
if (!fsSync.existsSync(file)) {
throw new Error(`File ${file} does not exist`)
}
if (!fsSync.lstatSync(file).isDirectory()) {
// Normalize and resolve, this allows for either absolute or relative paths to be used
file = path.normalize(file)
file = path.resolve(file)
if (!file.startsWith(root)) {
throw new Error(`The rootDirectory: ${root} is not a parent directory of the file: ${file}`)
}
// Check for forbidden characters in file paths that will be rejected during upload
const uploadPath = file.replace(root, '')
/*
uploadFilePath denotes where the file will be uploaded in the file container on the server. During a run, if multiple artifacts are uploaded, they will all
be saved in the same container. The artifact name is used as the root directory in the container to separate and distinguish uploaded artifacts
@ -100,100 +101,105 @@ export class NextcloudClient {
join('artifact-name', 'file-to-upload.txt')
join('artifact-name', '/file-to-upload.txt')
*/
specifications.push({
absolutePath: file,
uploadPath: path.join(this.artifact, uploadPath)
});
}
else {
// Directories are rejected by the server during upload
core.debug(`Removing ${file} from rawSearchResults because it is a directory`);
}
}
return specifications;
}
private async zipFiles(specs: FileSpec[]): Promise<string> {
const tempArtifactDir = path.join(os.tmpdir(), this.guid);
const artifactPath = path.join(tempArtifactDir, `artifact-${this.artifact}`);
await fs.mkdir(path.join(artifactPath, this.artifact), { recursive: true });
const copies = [];
for (let spec of specs) {
const dstpath = path.join(artifactPath, spec.uploadPath);
const dstDir = path.dirname(dstpath);
if (!fsSync.existsSync(dstDir)) {
await fs.mkdir(dstDir, { recursive: true });
}
copies.push(fs.copyFile(spec.absolutePath, dstpath));
}
await Promise.all(copies);
core.info(`files: ${await fs.readdir(path.join(artifactPath, this.artifact))}`);
const archivePath = path.join(artifactPath, `${this.artifact}.zip`);
await this.zip(path.join(artifactPath, this.artifact), archivePath);
core.info(`archive stat: ${(await fs.stat(archivePath)).size}`);
return archivePath;
}
private async zip(dirpath: string, destpath: string) {
const archive = archiver.create('zip', { zlib: { level: 9 } });
const stream = archive.directory(dirpath, false)
.pipe(fsSync.createWriteStream(destpath));
await archive.finalize();
return await new Promise<void>((resolve, reject) => {
stream.on('error', e => reject(e))
.on('close', () => resolve());
specifications.push({
absolutePath: file,
uploadPath: path.join(this.artifact, uploadPath)
})
} else {
// Directories are rejected by the server during upload
core.debug(`Removing ${file} from rawSearchResults because it is a directory`)
}
}
return specifications
}
private async zipFiles(specs: FileSpec[]): Promise<string> {
const tempArtifactDir = path.join(os.tmpdir(), this.guid)
const artifactPath = path.join(tempArtifactDir, `artifact-${this.artifact}`)
await fs.mkdir(path.join(artifactPath, this.artifact), { recursive: true })
const copies = []
for (const spec of specs) {
const dstpath = path.join(artifactPath, spec.uploadPath)
const dstDir = path.dirname(dstpath)
if (!fsSync.existsSync(dstDir)) {
await fs.mkdir(dstDir, { recursive: true })
}
copies.push(fs.copyFile(spec.absolutePath, dstpath))
}
private async upload(file: string): Promise<string> {
const remoteFileDir = `/artifacts/${this.guid}`;
core.info("Checking directory...");
if (!(await this.davClient.exists(remoteFileDir))) {
core.info("Creating directory...");
await this.davClient.createDirectory(remoteFileDir, { recursive: true });
}
await Promise.all(copies)
core.info(`files: ${await fs.readdir(path.join(artifactPath, this.artifact))}`)
const remoteFilePath = `${remoteFileDir}/${this.artifact}.zip`;
core.info(`Transferring file... (${file})`);
const archivePath = path.join(artifactPath, `${this.artifact}.zip`)
await this.zip(path.join(artifactPath, this.artifact), archivePath)
core.info(`archive stat: ${(await fs.stat(archivePath)).size}`)
const fileStat = await fs.stat(file);
const fileStream = fsSync.createReadStream(file);
const remoteStream = this.davClient.createWriteStream(remoteFilePath, {
headers: { "Content-Length": fileStat.size.toString() },
});
return archivePath
}
fileStream.pipe(remoteStream);
private async zip(dirpath: string, destpath: string) {
const archive = archiver.create('zip', { zlib: { level: 9 } })
const stream = archive.directory(dirpath, false).pipe(fsSync.createWriteStream(destpath))
await new Promise<void>((resolve, reject) => {
fileStream.on('error', e => reject(e))
.on('finish', () => resolve());
});
await archive.finalize()
return remoteFilePath;
return await new Promise<void>((resolve, reject) => {
stream.on('error', e => reject(e)).on('close', () => resolve())
})
}
private async upload(file: string): Promise<string> {
const remoteFileDir = `/artifacts/${this.guid}`
core.info('Checking directory...')
if (!(await this.davClient.exists(remoteFileDir))) {
core.info('Creating directory...')
await this.davClient.createDirectory(remoteFileDir, { recursive: true })
}
private async shareFile(remoteFilePath: string) {
const url = this.endpoint + `/ocs/v2.php/apps/files_sharing/api/v1/shares`;
const body = {
path: remoteFilePath,
shareType: 3,
publicUpload: "false",
permissions: 1,
};
const remoteFilePath = `${remoteFileDir}/${this.artifact}.zip`
core.info(`Transferring file... (${file})`)
const res = await fetch(url, {
method: 'PUT',
headers: this.headers,
body: JSON.stringify(body),
});
res.status
core.info(await res.text())
const fileStat = await fs.stat(file)
const fileStream = fsSync.createReadStream(file)
const remoteStream = this.davClient.createWriteStream(remoteFilePath, {
headers: { 'Content-Length': fileStat.size.toString() }
})
fileStream.pipe(remoteStream)
await new Promise<void>((resolve, reject) => {
fileStream.on('error', e => reject(e)).on('finish', () => resolve())
})
return remoteFilePath
}
private async shareFile(remoteFilePath: string): Promise<string> {
const url = this.endpoint + `/ocs/v2.php/apps/files_sharing/api/v1/shares`
const body = {
path: remoteFilePath,
shareType: 3,
publicUpload: 'false',
permissions: 1
}
}
const res = await fetch(url, {
method: 'POST',
headers: Object.assign(this.headers, {
'OCS-APIRequest': true
}),
body: JSON.stringify(body)
})
const result = await res.text()
const re = /<url>(?<share_url>.*)<\/url>/
const match = re.exec(result)
const sharableUrl = (match?.groups || {})['share_url']
if (!sharableUrl) {
throw new Error('Failed to parse sharable URL.')
}
return sharableUrl
}
}