mirror of
https://github.com/trympet/nextcloud-artifacts-action.git
synced 2025-04-24 20:16:08 +02:00
test output link
This commit is contained in:
parent
8167ea6a3e
commit
a48c618622
@ -30,6 +30,10 @@ inputs:
|
|||||||
Duration after which artifact will expire in days. 0 means using default retention.
|
Duration after which artifact will expire in days. 0 means using default retention.
|
||||||
Minimum 1 day.
|
Minimum 1 day.
|
||||||
Maximum 90 days unless changed from the repository settings page.
|
Maximum 90 days unless changed from the repository settings page.
|
||||||
|
token:
|
||||||
|
description: GitHub Access Token
|
||||||
|
required: false
|
||||||
|
default: ${{ github.token }}
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
6746
dist/index.js
vendored
6746
dist/index.js
vendored
File diff suppressed because one or more lines are too long
1
dist/index.js.map
vendored
Normal file
1
dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2581
dist/licenses.txt
vendored
Normal file
2581
dist/licenses.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/sourcemap-register.js
vendored
Normal file
1
dist/sourcemap-register.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6253
package-lock.json
generated
6253
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,11 +4,11 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "lib/nextcloud-artifacts.js",
|
"main": "lib/nextcloud-artifacts.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsc",
|
"build": "tsc",
|
||||||
"format": "prettier --write **/*.ts",
|
"format": "prettier --write **/*.ts",
|
||||||
"format-check": "prettier --check **/*.ts",
|
"format-check": "prettier --check **/*.ts",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
"package": "npx ncc build --source-map --license licenses.txt",
|
"package": "ncc build --source-map --license licenses.txt",
|
||||||
"test": "jest --ci"
|
"test": "jest --ci"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -44,6 +44,7 @@
|
|||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.16.1",
|
"@typescript-eslint/eslint-plugin": "^4.16.1",
|
||||||
"@typescript-eslint/parser": "^4.16.1",
|
"@typescript-eslint/parser": "^4.16.1",
|
||||||
|
"@vercel/ncc": "^0.28.6",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^7.21.0",
|
"eslint": "^7.21.0",
|
||||||
"eslint-plugin-github": "^4.1.3",
|
"eslint-plugin-github": "^4.1.3",
|
||||||
|
48
src/ActionInputs.ts
Normal file
48
src/ActionInputs.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import {NoFileOption} from './NoFileOption'
|
||||||
|
import {Inputs} from './Inputs'
|
||||||
|
|
||||||
|
export class ActionInputs implements Inputs {
|
||||||
|
get ArtifactName(): string {
|
||||||
|
return core.getInput('name')
|
||||||
|
}
|
||||||
|
|
||||||
|
get ArtifactPath(): string {
|
||||||
|
return core.getInput('path')
|
||||||
|
}
|
||||||
|
|
||||||
|
get Retention(): string {
|
||||||
|
return core.getInput('retention-days')
|
||||||
|
}
|
||||||
|
|
||||||
|
get Endpoint(): string {
|
||||||
|
return core.getInput('nextcloud-url')
|
||||||
|
}
|
||||||
|
|
||||||
|
get Username(): string {
|
||||||
|
return core.getInput('nextcloud-username')
|
||||||
|
}
|
||||||
|
|
||||||
|
get Password(): string {
|
||||||
|
return core.getInput('nextcloud-password')
|
||||||
|
}
|
||||||
|
|
||||||
|
get Token(): string {
|
||||||
|
return core.getInput('token', { required: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
get NoFileBehvaior(): NoFileOption {
|
||||||
|
const notFoundAction = core.getInput('if-no-files-found') || NoFileOption.warn
|
||||||
|
const noFileBehavior: NoFileOption = NoFileOption[notFoundAction as keyof typeof NoFileOption]
|
||||||
|
|
||||||
|
if (!noFileBehavior) {
|
||||||
|
core.setFailed(
|
||||||
|
`Unrecognized ${'ifNoFilesFound'} input. Provided: ${notFoundAction}. Available options: ${Object.keys(
|
||||||
|
NoFileOption
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return noFileBehavior
|
||||||
|
}
|
||||||
|
}
|
@ -10,20 +10,17 @@ export class FileFinder {
|
|||||||
followSymbolicLinks: true,
|
followSymbolicLinks: true,
|
||||||
implicitDescendants: true,
|
implicitDescendants: true,
|
||||||
omitBrokenSymbolicLinks: true
|
omitBrokenSymbolicLinks: true
|
||||||
};
|
}
|
||||||
|
|
||||||
private globOptions: glob.GlobOptions
|
private globOptions: glob.GlobOptions
|
||||||
|
|
||||||
public constructor(private searchPath: string, globOptions?: glob.GlobOptions) {
|
constructor(private searchPath: string, globOptions?: glob.GlobOptions) {
|
||||||
this.globOptions = globOptions || FileFinder.DefaultGlobOptions;
|
this.globOptions = globOptions || FileFinder.DefaultGlobOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findFiles() {
|
async findFiles() {
|
||||||
const searchResults: string[] = []
|
const searchResults: string[] = []
|
||||||
const globber = await glob.create(
|
const globber = await glob.create(this.searchPath, this.globOptions)
|
||||||
this.searchPath,
|
|
||||||
this.globOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const rawSearchResults: string[] = await globber.glob()
|
const rawSearchResults: string[] = await globber.glob()
|
||||||
|
|
||||||
@ -53,9 +50,7 @@ export class FileFinder {
|
|||||||
set.add(searchResult.toLowerCase())
|
set.add(searchResult.toLowerCase())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug(
|
debug(`Removing ${searchResult} from rawSearchResults because it is a directory`)
|
||||||
`Removing ${searchResult} from rawSearchResults because it is a directory`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,13 +58,9 @@ export class FileFinder {
|
|||||||
const searchPaths: string[] = globber.getSearchPaths()
|
const searchPaths: string[] = globber.getSearchPaths()
|
||||||
|
|
||||||
if (searchPaths.length > 1) {
|
if (searchPaths.length > 1) {
|
||||||
info(
|
info(`Multiple search paths detected. Calculating the least common ancestor of all paths`)
|
||||||
`Multiple search paths detected. Calculating the least common ancestor of all paths`
|
|
||||||
)
|
|
||||||
const lcaSearchPath = this.getMultiPathLCA(searchPaths)
|
const lcaSearchPath = this.getMultiPathLCA(searchPaths)
|
||||||
info(
|
info(`The least common ancestor is ${lcaSearchPath}. This will be the root directory of the artifact`)
|
||||||
`The least common ancestor is ${lcaSearchPath}. This will be the root directory of the artifact`
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filesToUpload: searchResults,
|
filesToUpload: searchResults,
|
||||||
|
@ -12,5 +12,5 @@ export enum NoFileOption {
|
|||||||
/**
|
/**
|
||||||
* Do not output any warnings or errors, the action does not fail
|
* Do not output any warnings or errors, the action does not fail
|
||||||
*/
|
*/
|
||||||
ignore = 'ignore',
|
ignore = 'ignore'
|
||||||
}
|
}
|
@ -1,12 +1,15 @@
|
|||||||
import { Inputs } from './Inputs';
|
import {NextcloudArtifact} from './nextcloud/NextcloudArtifact'
|
||||||
import { NextcloudArtifact } from './nextcloud/NextcloudArtifact';
|
import * as core from '@actions/core'
|
||||||
import * as core from '@actions/core';
|
import {ActionInputs} from './ActionInputs'
|
||||||
|
|
||||||
|
async function run() {
|
||||||
try {
|
try {
|
||||||
var artifact = new NextcloudArtifact(Inputs.ArtifactName, Inputs.ArtifactPath, Inputs.NoFileBehvaior);
|
const artifact = new NextcloudArtifact(new ActionInputs())
|
||||||
artifact.run()
|
await artifact.run()
|
||||||
.catch(e => core.setFailed(e));
|
core.info('Finished')
|
||||||
core.info("Finished");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(error.message);
|
core.setFailed(error.message)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
@ -1,63 +1,138 @@
|
|||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core'
|
||||||
import { FileFinder } from '../FileFinder';
|
import * as github from '@actions/github'
|
||||||
import { Inputs } from '../Inputs';
|
import { GitHub } from '@actions/github/lib/utils'
|
||||||
import { NextcloudClient } from './NextcloudClient';
|
|
||||||
import { NoFileOption } from '../NoFileOption';
|
import { FileFinder } from '../FileFinder'
|
||||||
|
import { Inputs } from '../Inputs'
|
||||||
|
import { NextcloudClient } from './NextcloudClient'
|
||||||
|
import { NoFileOption } from '../NoFileOption'
|
||||||
|
|
||||||
export class NextcloudArtifact {
|
export class NextcloudArtifact {
|
||||||
public constructor(
|
readonly octokit: InstanceType<typeof GitHub>
|
||||||
private name: string,
|
readonly context = NextcloudArtifact.getCheckRunContext()
|
||||||
private path: string,
|
readonly token: string
|
||||||
private errorBehavior: NoFileOption) { }
|
readonly name: string
|
||||||
|
readonly path: string
|
||||||
|
readonly errorBehavior: NoFileOption
|
||||||
|
|
||||||
public async run() {
|
constructor(private inputs: Inputs) {
|
||||||
const fileFinder = new FileFinder(this.path);
|
this.token = inputs.Token;
|
||||||
const files = await fileFinder.findFiles();
|
this.name = inputs.ArtifactName
|
||||||
|
this.path = inputs.ArtifactPath
|
||||||
|
this.errorBehavior = inputs.NoFileBehvaior
|
||||||
|
this.name = inputs.ArtifactName
|
||||||
|
this.octokit = github.getOctokit(this.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const fileFinder = new FileFinder(this.path)
|
||||||
|
const files = await fileFinder.findFiles()
|
||||||
|
|
||||||
if (files.filesToUpload.length > 0) {
|
if (files.filesToUpload.length > 0) {
|
||||||
await this.uploadFiles(files);
|
await this.uploadFiles(files)
|
||||||
}
|
} else {
|
||||||
else {
|
this.logNoFilesFound()
|
||||||
this.logNoFilesFound();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadFiles(files: { filesToUpload: string[]; rootDirectory: string; }) {
|
private static getCheckRunContext(): { sha: string; runId: number } {
|
||||||
this.logUpload(files.filesToUpload.length, files.rootDirectory);
|
if (github.context.eventName === 'workflow_run') {
|
||||||
|
core.info('Action was triggered by workflow_run: using SHA and RUN_ID from triggering workflow')
|
||||||
|
const event = github.context.payload
|
||||||
|
if (!event.workflow_run) {
|
||||||
|
throw new Error("Event of type 'workflow_run' is missing 'workflow_run' field")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sha: event.workflow_run.head_commit.id,
|
||||||
|
runId: event.workflow_run.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const client = new NextcloudClient(Inputs.Endpoint, this.name, files.rootDirectory, Inputs.Username, Inputs.Password);
|
const runId = github.context.runId
|
||||||
|
if (github.context.payload.pull_request) {
|
||||||
|
core.info(`Action was triggered by ${github.context.eventName}: using SHA from head of source branch`)
|
||||||
|
const pr = github.context.payload.pull_request
|
||||||
|
return { sha: pr.head.sha, runId }
|
||||||
|
}
|
||||||
|
|
||||||
await client.uploadFiles(files.filesToUpload);
|
return { sha: github.context.sha, runId }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadFiles(files: { filesToUpload: string[]; rootDirectory: string }) {
|
||||||
|
this.logUpload(files.filesToUpload.length, files.rootDirectory)
|
||||||
|
const createResp = await this.octokit.rest.checks.create({
|
||||||
|
head_sha: this.context.sha,
|
||||||
|
name: 'Nextcloud Artifacts',
|
||||||
|
status: 'in_progress',
|
||||||
|
output: {
|
||||||
|
title: 'Nextcloud Artifacts',
|
||||||
|
summary: ''
|
||||||
|
},
|
||||||
|
...github.context.repo
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = new NextcloudClient(
|
||||||
|
this.inputs.Endpoint,
|
||||||
|
this.name,
|
||||||
|
files.rootDirectory,
|
||||||
|
this.inputs.Username,
|
||||||
|
this.inputs.Password
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shareableUrl = await client.uploadFiles(files.filesToUpload)
|
||||||
|
const resp = await this.octokit.rest.checks.update({
|
||||||
|
check_run_id: createResp.data.id,
|
||||||
|
conclusion: 'success',
|
||||||
|
status: 'completed',
|
||||||
|
output: {
|
||||||
|
title: 'Nextcloud Artifacts',
|
||||||
|
summary: `${this.name}: ${shareableUrl}`
|
||||||
|
},
|
||||||
|
...github.context.repo
|
||||||
|
})
|
||||||
|
core.info(`Check run create response: ${resp.status}`)
|
||||||
|
core.info(`Check run URL: ${resp.data.url}`)
|
||||||
|
core.info(`Check run HTML: ${resp.data.html_url}`)
|
||||||
|
} catch (error) {
|
||||||
|
await this.octokit.rest.checks.update({
|
||||||
|
check_run_id: createResp.data.id,
|
||||||
|
conclusion: 'failure',
|
||||||
|
status: 'completed',
|
||||||
|
output: {
|
||||||
|
title: 'Nextcloud Artifacts'
|
||||||
|
},
|
||||||
|
...github.context.repo
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private logUpload(fileCount: number, rootDirectory: string) {
|
private logUpload(fileCount: number, rootDirectory: string) {
|
||||||
const s = fileCount === 1 ? '' : 's';
|
const s = fileCount === 1 ? '' : 's'
|
||||||
core.info(
|
core.info(`With the provided path, there will be ${fileCount} file${s} uploaded`)
|
||||||
`With the provided path, there will be ${fileCount} file${s} uploaded`
|
core.debug(`Root artifact directory is ${rootDirectory}`)
|
||||||
);
|
|
||||||
core.debug(`Root artifact directory is ${rootDirectory}`);
|
|
||||||
|
|
||||||
if (fileCount > 10000) {
|
if (fileCount > 10000) {
|
||||||
core.warning(
|
core.warning(
|
||||||
`There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.`
|
`There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.`
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private logNoFilesFound() {
|
private logNoFilesFound() {
|
||||||
const errorMessage = `No files were found with the provided path: ${this.path}. No artifacts will be uploaded.`;
|
const errorMessage = `No files were found with the provided path: ${this.path}. No artifacts will be uploaded.`
|
||||||
switch (this.errorBehavior) {
|
switch (this.errorBehavior) {
|
||||||
case NoFileOption.warn: {
|
case NoFileOption.warn: {
|
||||||
core.warning(errorMessage);
|
core.warning(errorMessage)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
case NoFileOption.error: {
|
case NoFileOption.error: {
|
||||||
core.setFailed(errorMessage);
|
core.setFailed(errorMessage)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
case NoFileOption.ignore: {
|
case NoFileOption.ignore: {
|
||||||
core.info(errorMessage);
|
core.info(errorMessage)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,64 @@
|
|||||||
import * as fsSync from 'fs'
|
import * as fsSync from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core'
|
||||||
import * as os from 'os';
|
import * as os from 'os'
|
||||||
import * as archiver from 'archiver';
|
import * as archiver from 'archiver'
|
||||||
import fetch, { HeadersInit } from 'node-fetch';
|
import fetch, { HeadersInit } from 'node-fetch'
|
||||||
import btoa from 'btoa';
|
import btoa from 'btoa'
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as webdav from 'webdav'
|
import * as webdav from 'webdav'
|
||||||
|
|
||||||
const fs = fsSync.promises;
|
const fs = fsSync.promises
|
||||||
|
|
||||||
interface FileSpec {
|
interface FileSpec {
|
||||||
absolutePath: string,
|
absolutePath: string
|
||||||
uploadPath: string
|
uploadPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NextcloudClient {
|
export class NextcloudClient {
|
||||||
private guid: string;
|
private guid: string
|
||||||
private headers: HeadersInit;
|
private headers: HeadersInit
|
||||||
private davClient;
|
private davClient
|
||||||
|
|
||||||
public constructor(
|
constructor(
|
||||||
private endpoint: string,
|
private endpoint: string,
|
||||||
private artifact: string,
|
private artifact: string,
|
||||||
private rootDirectory: string,
|
private rootDirectory: string,
|
||||||
private username: string,
|
private username: string,
|
||||||
private password: string) {
|
private password: string
|
||||||
this.guid = uuidv4();
|
) {
|
||||||
this.headers = { 'Authorization': 'Basic ' + btoa(`${this.username}:${this.password}`) };
|
this.guid = uuidv4()
|
||||||
|
this.headers = { Authorization: 'Basic ' + btoa(`${this.username}:${this.password}`) }
|
||||||
this.davClient = webdav.createClient(`${this.endpoint}/remote.php/dav/files/${this.username}`, {
|
this.davClient = webdav.createClient(`${this.endpoint}/remote.php/dav/files/${this.username}`, {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password,
|
password: this.password
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async uploadFiles(files: string[]) {
|
async uploadFiles(files: string[]): Promise<string> {
|
||||||
core.info("Preparing upload...");
|
core.info('Preparing upload...')
|
||||||
const spec = this.uploadSpec(files);
|
const spec = this.uploadSpec(files)
|
||||||
core.info("Zipping files...");
|
core.info('Zipping files...')
|
||||||
var zip = await this.zipFiles(spec);
|
const zip = await this.zipFiles(spec)
|
||||||
|
|
||||||
core.info("Uploading to Nextcloud...");
|
core.info('Uploading to Nextcloud...')
|
||||||
const path = await this.upload(zip);
|
const filePath = await this.upload(zip)
|
||||||
core.info(`File path: ${path}`);
|
core.info(`File path: ${filePath}`)
|
||||||
core.info("Sharing file...");
|
core.info('Sharing file...')
|
||||||
await this.shareFile(path);
|
return await this.shareFile(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
private uploadSpec(files: string[]): FileSpec[] {
|
private uploadSpec(files: string[]): FileSpec[] {
|
||||||
const specifications = [];
|
const specifications = []
|
||||||
if (!fsSync.existsSync(this.rootDirectory)) {
|
if (!fsSync.existsSync(this.rootDirectory)) {
|
||||||
throw new Error(`this.rootDirectory ${this.rootDirectory} does not exist`);
|
throw new Error(`this.rootDirectory ${this.rootDirectory} does not exist`)
|
||||||
}
|
}
|
||||||
if (!fsSync.lstatSync(this.rootDirectory).isDirectory()) {
|
if (!fsSync.lstatSync(this.rootDirectory).isDirectory()) {
|
||||||
throw new Error(`this.rootDirectory ${this.rootDirectory} is not a valid directory`);
|
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
|
// Normalize and resolve, this allows for either absolute or relative paths to be used
|
||||||
let root = path.normalize(this.rootDirectory);
|
let root = path.normalize(this.rootDirectory)
|
||||||
root = path.resolve(root);
|
root = path.resolve(root)
|
||||||
/*
|
/*
|
||||||
Example to demonstrate behavior
|
Example to demonstrate behavior
|
||||||
|
|
||||||
@ -79,17 +80,17 @@ export class NextcloudClient {
|
|||||||
*/
|
*/
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
if (!fsSync.existsSync(file)) {
|
if (!fsSync.existsSync(file)) {
|
||||||
throw new Error(`File ${file} does not exist`);
|
throw new Error(`File ${file} does not exist`)
|
||||||
}
|
}
|
||||||
if (!fsSync.lstatSync(file).isDirectory()) {
|
if (!fsSync.lstatSync(file).isDirectory()) {
|
||||||
// Normalize and resolve, this allows for either absolute or relative paths to be used
|
// Normalize and resolve, this allows for either absolute or relative paths to be used
|
||||||
file = path.normalize(file);
|
file = path.normalize(file)
|
||||||
file = path.resolve(file);
|
file = path.resolve(file)
|
||||||
if (!file.startsWith(root)) {
|
if (!file.startsWith(root)) {
|
||||||
throw new Error(`The rootDirectory: ${root} is not a parent directory of the file: ${file}`);
|
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
|
// Check for forbidden characters in file paths that will be rejected during upload
|
||||||
const uploadPath = file.replace(root, '');
|
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
|
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
|
be saved in the same container. The artifact name is used as the root directory in the container to separate and distinguish uploaded artifacts
|
||||||
@ -103,97 +104,102 @@ export class NextcloudClient {
|
|||||||
specifications.push({
|
specifications.push({
|
||||||
absolutePath: file,
|
absolutePath: file,
|
||||||
uploadPath: path.join(this.artifact, uploadPath)
|
uploadPath: path.join(this.artifact, uploadPath)
|
||||||
});
|
})
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Directories are rejected by the server during upload
|
// Directories are rejected by the server during upload
|
||||||
core.debug(`Removing ${file} from rawSearchResults because it is a directory`);
|
core.debug(`Removing ${file} from rawSearchResults because it is a directory`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return specifications;
|
return specifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async zipFiles(specs: FileSpec[]): Promise<string> {
|
private async zipFiles(specs: FileSpec[]): Promise<string> {
|
||||||
const tempArtifactDir = path.join(os.tmpdir(), this.guid);
|
const tempArtifactDir = path.join(os.tmpdir(), this.guid)
|
||||||
const artifactPath = path.join(tempArtifactDir, `artifact-${this.artifact}`);
|
const artifactPath = path.join(tempArtifactDir, `artifact-${this.artifact}`)
|
||||||
await fs.mkdir(path.join(artifactPath, this.artifact), { recursive: true });
|
await fs.mkdir(path.join(artifactPath, this.artifact), { recursive: true })
|
||||||
const copies = [];
|
const copies = []
|
||||||
for (let spec of specs) {
|
for (const spec of specs) {
|
||||||
const dstpath = path.join(artifactPath, spec.uploadPath);
|
const dstpath = path.join(artifactPath, spec.uploadPath)
|
||||||
const dstDir = path.dirname(dstpath);
|
const dstDir = path.dirname(dstpath)
|
||||||
if (!fsSync.existsSync(dstDir)) {
|
if (!fsSync.existsSync(dstDir)) {
|
||||||
await fs.mkdir(dstDir, { recursive: true });
|
await fs.mkdir(dstDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
copies.push(fs.copyFile(spec.absolutePath, dstpath));
|
copies.push(fs.copyFile(spec.absolutePath, dstpath))
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(copies);
|
await Promise.all(copies)
|
||||||
core.info(`files: ${await fs.readdir(path.join(artifactPath, this.artifact))}`);
|
core.info(`files: ${await fs.readdir(path.join(artifactPath, this.artifact))}`)
|
||||||
|
|
||||||
const archivePath = path.join(artifactPath, `${this.artifact}.zip`);
|
const archivePath = path.join(artifactPath, `${this.artifact}.zip`)
|
||||||
await this.zip(path.join(artifactPath, this.artifact), archivePath);
|
await this.zip(path.join(artifactPath, this.artifact), archivePath)
|
||||||
core.info(`archive stat: ${(await fs.stat(archivePath)).size}`);
|
core.info(`archive stat: ${(await fs.stat(archivePath)).size}`)
|
||||||
|
|
||||||
return archivePath;
|
return archivePath
|
||||||
}
|
}
|
||||||
|
|
||||||
private async zip(dirpath: string, destpath: string) {
|
private async zip(dirpath: string, destpath: string) {
|
||||||
const archive = archiver.create('zip', { zlib: { level: 9 } });
|
const archive = archiver.create('zip', { zlib: { level: 9 } })
|
||||||
const stream = archive.directory(dirpath, false)
|
const stream = archive.directory(dirpath, false).pipe(fsSync.createWriteStream(destpath))
|
||||||
.pipe(fsSync.createWriteStream(destpath));
|
|
||||||
|
|
||||||
await archive.finalize();
|
await archive.finalize()
|
||||||
|
|
||||||
return await new Promise<void>((resolve, reject) => {
|
return await new Promise<void>((resolve, reject) => {
|
||||||
stream.on('error', e => reject(e))
|
stream.on('error', e => reject(e)).on('close', () => resolve())
|
||||||
.on('close', () => resolve());
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async upload(file: string): Promise<string> {
|
private async upload(file: string): Promise<string> {
|
||||||
const remoteFileDir = `/artifacts/${this.guid}`;
|
const remoteFileDir = `/artifacts/${this.guid}`
|
||||||
core.info("Checking directory...");
|
core.info('Checking directory...')
|
||||||
if (!(await this.davClient.exists(remoteFileDir))) {
|
if (!(await this.davClient.exists(remoteFileDir))) {
|
||||||
core.info("Creating directory...");
|
core.info('Creating directory...')
|
||||||
await this.davClient.createDirectory(remoteFileDir, { recursive: true });
|
await this.davClient.createDirectory(remoteFileDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteFilePath = `${remoteFileDir}/${this.artifact}.zip`;
|
const remoteFilePath = `${remoteFileDir}/${this.artifact}.zip`
|
||||||
core.info(`Transferring file... (${file})`);
|
core.info(`Transferring file... (${file})`)
|
||||||
|
|
||||||
const fileStat = await fs.stat(file);
|
const fileStat = await fs.stat(file)
|
||||||
const fileStream = fsSync.createReadStream(file);
|
const fileStream = fsSync.createReadStream(file)
|
||||||
const remoteStream = this.davClient.createWriteStream(remoteFilePath, {
|
const remoteStream = this.davClient.createWriteStream(remoteFilePath, {
|
||||||
headers: { "Content-Length": fileStat.size.toString() },
|
headers: { 'Content-Length': fileStat.size.toString() }
|
||||||
});
|
})
|
||||||
|
|
||||||
fileStream.pipe(remoteStream);
|
fileStream.pipe(remoteStream)
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
fileStream.on('error', e => reject(e))
|
fileStream.on('error', e => reject(e)).on('finish', () => resolve())
|
||||||
.on('finish', () => resolve());
|
})
|
||||||
});
|
|
||||||
|
|
||||||
return remoteFilePath;
|
return remoteFilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
private async shareFile(remoteFilePath: string) {
|
private async shareFile(remoteFilePath: string): Promise<string> {
|
||||||
const url = this.endpoint + `/ocs/v2.php/apps/files_sharing/api/v1/shares`;
|
const url = this.endpoint + `/ocs/v2.php/apps/files_sharing/api/v1/shares`
|
||||||
const body = {
|
const body = {
|
||||||
path: remoteFilePath,
|
path: remoteFilePath,
|
||||||
shareType: 3,
|
shareType: 3,
|
||||||
publicUpload: "false",
|
publicUpload: 'false',
|
||||||
permissions: 1,
|
permissions: 1
|
||||||
};
|
}
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: this.headers,
|
headers: Object.assign(this.headers, {
|
||||||
body: JSON.stringify(body),
|
'OCS-APIRequest': true
|
||||||
});
|
}),
|
||||||
res.status
|
body: JSON.stringify(body)
|
||||||
core.info(await res.text())
|
})
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
9
workspace.code-workspace
Normal file
9
workspace.code-workspace
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remoteAuthority": "wsl+Ubuntu-20.04",
|
||||||
|
"settings": {}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user