71 Commits

Author SHA1 Message Date
c80beab5e4 Update README.md 2021-06-05 14:44:29 +02:00
71897dd340 update name 2021-06-05 14:25:23 +02:00
7ca2f4d4fb more cleanup 2021-06-05 14:21:53 +02:00
2b05098faf remove unused param 2021-06-05 14:14:54 +02:00
78aff342ff format 2021-06-05 14:12:01 +02:00
bcd3d376b1 lint 2021-06-05 14:11:48 +02:00
565777588d cleanup 2021-06-05 14:11:25 +02:00
7fe0b3a0b9 test 2021-06-05 14:00:02 +02:00
2e7f2059ee test 2021-06-05 13:57:28 +02:00
f7ad337105 tesst 2021-06-05 13:54:25 +02:00
b7837011bd test 2021-06-05 13:50:02 +02:00
d363b4a3d0 fix IO await 2021-06-05 13:47:27 +02:00
40bfed39fc await remote stream 2021-06-05 13:39:59 +02:00
4ab7fcecfe set content type header 2021-06-05 13:34:52 +02:00
a5df11eaca debug 2021-06-05 13:30:57 +02:00
02b35dd9a1 error handling 2021-06-05 13:27:25 +02:00
b52e167ab0 test 2021-06-05 13:21:45 +02:00
a48c618622 test output link 2021-06-05 13:13:36 +02:00
8167ea6a3e ignore tests 2021-06-05 13:10:53 +02:00
7a45cee3de add simple integration test 2021-06-05 13:09:07 +02:00
518c6f09f4 update formatting 2021-06-05 13:08:14 +02:00
200441494b update rules 2021-06-05 11:58:22 +02:00
68c752a9df eslint 2021-06-05 11:54:54 +02:00
f07d8c68b0 update package json 2021-06-05 11:45:53 +02:00
ea9ac9cb73 fix empty file 2021-06-02 21:09:26 +02:00
c1ee7f2095 change ctor 2021-06-02 21:08:44 +02:00
cfb60b9a7f test 2021-06-02 20:21:59 +02:00
ab2bf8a4ac test 2021-06-02 20:19:43 +02:00
51f5e63425 test 2021-06-02 20:12:51 +02:00
cbff2d05e7 test 2021-06-02 20:10:05 +02:00
6226379dbd test 2021-06-02 20:03:09 +02:00
1f2878a134 test 2021-06-02 20:00:10 +02:00
a96cb53d3a a 2021-06-02 19:56:10 +02:00
4669814861 test 2021-06-02 19:53:32 +02:00
4ac99d8b52 test 2021-06-02 19:51:36 +02:00
3cf7a462d4 test 2021-06-02 19:45:24 +02:00
a54a89d701 test 2021-06-02 19:39:26 +02:00
0c1792a993 test 2021-06-02 19:38:09 +02:00
e97b799152 test 2021-06-02 19:35:57 +02:00
d873964202 test 2021-06-02 19:26:46 +02:00
5b01b7a17b fix path format 2021-06-02 19:21:08 +02:00
2431c40b14 test 2021-06-02 19:17:08 +02:00
c38757acba test 2021-06-02 19:08:27 +02:00
ae39220a7a test 2021-06-02 19:00:35 +02:00
f5a8d5a548 test 2021-06-02 18:56:19 +02:00
ac058f2460 test 2021-06-02 18:56:10 +02:00
71d45ee738 test 2021-06-02 18:51:56 +02:00
353b719074 test 2021-06-02 18:43:40 +02:00
cb9c817940 test 2021-06-02 18:41:08 +02:00
80a86a24ab test 2021-06-02 18:39:35 +02:00
18228c6e6d test 2021-06-02 18:39:26 +02:00
70ba52c79f build 2021-06-02 18:36:53 +02:00
4e614273b8 more logging 2021-06-02 18:36:35 +02:00
77a901b885 log response 2021-06-02 18:28:35 +02:00
03e2a736d6 use dav client 2021-06-02 18:23:33 +02:00
438fd898e1 test 2021-06-02 17:06:14 +02:00
e8b44e8c3b test 2021-06-02 17:04:43 +02:00
75e2916679 test 2021-06-02 17:00:08 +02:00
e96c1262aa fix directory exists 2021-06-02 16:40:06 +02:00
83cbe577f4 fix uuid 2021-06-02 16:36:55 +02:00
146df697b5 test 2021-06-02 16:28:31 +02:00
6def34f2d3 more impor tfix 2021-06-02 16:24:13 +02:00
1cb76bcfb4 fix imports 2021-06-02 16:20:16 +02:00
a87ab4cf3b exclide node modules 2021-06-02 16:17:19 +02:00
7189094530 add github 2021-06-02 16:14:41 +02:00
314b7642ee fix promises import 2021-06-02 16:09:37 +02:00
1259b887e0 delete lib 2021-06-02 16:06:14 +02:00
aae7f9ffbb update files 2021-06-02 16:04:09 +02:00
0141d42d10 rename; build to lib 2021-06-02 16:04:02 +02:00
a6f0f86670 add build script 2021-06-02 16:01:49 +02:00
22aca2a29a add dist 2021-06-02 15:56:02 +02:00
30 changed files with 55532 additions and 377 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

57
.eslintrc Normal file
View File

@ -0,0 +1,57 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"camelcase": "off",
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-shadow": "off",
"no-unused-vars": "off",
"prefer-template": "off",
"semi": [ "error", "never"],
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/semi": ["error", "never"],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}

3
.gitignore vendored
View File

@ -1,3 +1,5 @@
lib
# Logs
logs
*.log
@ -80,7 +82,6 @@ typings/
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/

5
.prettierignore Normal file
View File

@ -0,0 +1,5 @@
dist/
lib/
node_modules/
__tests__/__outputs__
__tests__/__snapshots__

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
}

View File

@ -1 +1,29 @@
# nextcloud-artifacts-action
# Nextcloud Artifact Upload Action
Upload artifacts to nextcloud and outputs a shareable URL.
### How it looks:
![image](https://user-images.githubusercontent.com/23460729/120891750-7f247380-c60a-11eb-9998-3b3b7f61066f.png)
### Example:
Simple example. Globbing is supported.
```yaml
on:
pull_request:
push:
jobs:
build-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2 # checkout the repo
- name: Nextcloud Artifact
uses: trympet/nextcloud-artifacts-action@v2
with:
name: 'my-artifact' # Name of the artifact
path: 'bin/**/*.exe' # Globbing supported
nextcloud-url: 'https://nextcloud.example.com' # Format of test results
nextcloud-username: ${{ secrets.NEXTCLOUD_USERNAME }} # Username from repository secret
nextcloud-password: ${{ secrets.NEXTCLOUD_PASSWORD }} # Password from repository secret
```

View File

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Inputs } from '../../src/Inputs'
import { NoFileOption } from '../../src/NoFileOption'
export class InputsDouble implements Inputs {
get ArtifactName(): string {
return process.env['ARTIFACT_NAME']!
}
get ArtifactPath(): string {
return process.env['ARTIFACT_PATH']!
}
get Retention(): string {
return ''
}
get Endpoint(): URL {
return new URL(process.env['ENDPOINT']!)
}
get Username(): string {
return process.env['USERNAME']!
}
get Password(): string {
return process.env['PASSWORD']!
}
get Token(): string {
return process.env['TOKEN']!
}
get NoFileBehvaior(): NoFileOption {
return NoFileOption.error
}
}

View File

View File

@ -0,0 +1,9 @@
import { NextcloudArtifact } from '../src/nextcloud/NextcloudArtifact'
import { InputsDouble } from './doubles/InputsDouble'
describe('integration tests', () => {
it('works', async () => {
const artifact = new NextcloudArtifact(new InputsDouble())
await artifact.run()
})
})

1
__tests__/setup.ts Normal file
View File

@ -0,0 +1 @@
require('dotenv').config()

View File

@ -25,11 +25,10 @@ inputs:
error: Fail the action with an error message
ignore: Do not output any warnings or errors, the action does not fail
default: 'warn'
retention-days:
description: >
Duration after which artifact will expire in days. 0 means using default retention.
Minimum 1 day.
Maximum 90 days unless changed from the repository settings page.
token:
description: GitHub Access Token
required: false
default: ${{ github.token }}
runs:
using: 'node12'
main: 'dist/index.js'

45407
dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because it is too large Load Diff

1
dist/sourcemap-register.js vendored Normal file

File diff suppressed because one or more lines are too long

12
jest.config.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true,
setupFiles: ["./__tests__/setup.ts"]
}

6519
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,70 @@
{
"name": "nextcloud-artifacts-action",
"version": "1.0.0",
"version": "2.0.0",
"description": "",
"main": "index.js",
"main": "lib/nextcloud-artifacts.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts",
"package": "ncc build --source-map --license licenses.txt",
"test": "jest --ci"
},
"repository": {
"type": "git",
"url": "git+https://github.com/trympet/nextcloud-artifacts-action.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "Trym Lund Flogard <trym@flogard.no>",
"license": "GPL-2.0-only",
"bugs": {
"url": "https://github.com/trympet/nextcloud-artifacts-action/issues"
},
"homepage": "https://github.com/trympet/nextcloud-artifacts-action#readme",
"dependencies": {
"@actions/core": "^1.3.0",
"@actions/exec": "^1.0.4",
"@actions/github": "^5.0.0",
"@actions/glob": "^0.1.2",
"archiver": "^5.3.0",
"btoa": "^1.2.1",
"node-fetch": "^2.6.1"
"node-fetch": "^2.6.1",
"uuid": "^8.3.2",
"webdav": "^4.6.0"
},
"devDependencies": {
"@octokit/types": "^6.16.2",
"@octokit/webhooks": "^9.6.3",
"@types/archiver": "^5.1.0",
"@types/btoa": "^1.2.3",
"@types/jest": "^26.0.23",
"@types/node": "^15.6.2",
"@types/node-fetch": "^2.5.10",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"@vercel/ncc": "^0.28.6",
"dotenv": "^10.0.0",
"eslint": "^7.21.0",
"eslint-plugin-github": "^4.1.3",
"eslint-plugin-jest": "^24.1.7",
"jest": "^26.6.3",
"jest-circus": "^26.6.3",
"jest-junit": "^12.0.0",
"js-yaml": "^4.0.0",
"prettier": "2.2.1",
"ts-jest": "^26.5.3",
"typescript": "^4.3.2"
},
"jest-junit": {
"suiteName": "jest tests",
"outputDirectory": "__tests__/__results__",
"outputName": "jest-junit.xml",
"ancestorSeparator": " ",
"uniqueOutputName": "false",
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
}
}

45
src/ActionInputs.ts Normal file
View File

@ -0,0 +1,45 @@
import * as core from '@actions/core'
import { NoFileOption } from './NoFileOption'
import { Inputs } from './Inputs'
import { URL } from 'url'
export class ActionInputs implements Inputs {
get ArtifactName(): string {
return core.getInput('name', { required: false }) || 'Nextcloud Artifact'
}
get ArtifactPath(): string {
return core.getInput('path', { required: true })
}
get Endpoint(): URL {
return new URL(core.getInput('nextcloud-url', { required: true }))
}
get Username(): string {
return core.getInput('nextcloud-username', { required: true })
}
get Password(): string {
return core.getInput('nextcloud-password', { required: true })
}
get Token(): string {
return core.getInput('token', { required: true })
}
get NoFileBehvaior(): NoFileOption {
const notFoundAction = core.getInput('if-no-files-found', { required: false }) || 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
}
}

View File

@ -1,8 +1,8 @@
import * as glob from '@actions/glob'
import {stat} from 'fs'
import {debug, info} from '@actions/core'
import { stat } from 'fs'
import { debug, info } from '@actions/core'
import * as path from 'path'
import {promisify} from 'util'
import { promisify } from 'util'
const stats = promisify(stat)
export class FileFinder {
@ -10,20 +10,17 @@ export class FileFinder {
followSymbolicLinks: true,
implicitDescendants: true,
omitBrokenSymbolicLinks: true
};
}
private globOptions: glob.GlobOptions
public constructor(private searchPath: string, globOptions?: glob.GlobOptions) {
this.globOptions = globOptions || FileFinder.DefaultGlobOptions;
constructor(private searchPath: string, globOptions?: glob.GlobOptions) {
this.globOptions = globOptions || FileFinder.DefaultGlobOptions
}
public async findFiles() {
async findFiles() {
const searchResults: string[] = []
const globber = await glob.create(
this.searchPath,
this.globOptions
);
const globber = await glob.create(this.searchPath, this.globOptions)
const rawSearchResults: string[] = await globber.glob()
@ -53,9 +50,7 @@ export class FileFinder {
set.add(searchResult.toLowerCase())
}
} else {
debug(
`Removing ${searchResult} from rawSearchResults because it is a directory`
)
debug(`Removing ${searchResult} from rawSearchResults because it is a directory`)
}
}
@ -63,13 +58,9 @@ export class FileFinder {
const searchPaths: string[] = globber.getSearchPaths()
if (searchPaths.length > 1) {
info(
`Multiple search paths detected. Calculating the least common ancestor of all paths`
)
info(`Multiple search paths detected. Calculating the least common ancestor of all paths`)
const lcaSearchPath = this.getMultiPathLCA(searchPaths)
info(
`The least common ancestor is ${lcaSearchPath}. This will be the root directory of the artifact`
)
info(`The least common ancestor is ${lcaSearchPath}. This will be the root directory of the artifact`)
return {
filesToUpload: searchResults,

View File

@ -1,43 +1,18 @@
import core from '@actions/core';
import { NoFileOption } from './NoFileOption';
import { URL } from 'url'
import { NoFileOption } from './NoFileOption'
export class Inputs {
static get ArtifactName(): string {
return core.getInput("name");
}
export interface Inputs {
readonly ArtifactName: string
static get ArtifactPath(): string {
return core.getInput("path");
}
readonly ArtifactPath: string
static get Retention(): string {
return core.getInput("retention-days");
}
readonly Endpoint: URL
static get Endpoint(): string {
return core.getInput("nextcloud-url");
}
readonly Username: string
static get Username(): string {
return core.getInput("nextcloud-username");
}
readonly Password: string
static get Password(): string {
return core.getInput("nextcloud-password");
}
readonly Token: string
static get NoFileBehvaior(): NoFileOption {
const notFoundAction = core.getInput("if-no-files-found");
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;
}
readonly NoFileBehvaior: NoFileOption
}

View File

@ -12,5 +12,5 @@ export enum NoFileOption {
/**
* Do not output any warnings or errors, the action does not fail
*/
ignore = 'ignore',
}
ignore = 'ignore'
}

View File

@ -1,5 +0,0 @@
import { Inputs } from './Inputs';
import { NextcloudArtifact } from './nextcloud/NextcloudArtifact';
var artifact = new NextcloudArtifact(Inputs.ArtifactName, Inputs.ArtifactPath, Inputs.NoFileBehvaior);
artifact.run();

View File

@ -0,0 +1,15 @@
import { NextcloudArtifact } from './nextcloud/NextcloudArtifact'
import * as core from '@actions/core'
import { ActionInputs } from './ActionInputs'
async function run() {
try {
const artifact = new NextcloudArtifact(new ActionInputs())
await artifact.run()
core.info('Finished')
} catch (error) {
core.setFailed(error.message)
}
}
run()

View File

@ -1,63 +1,153 @@
import core from '@actions/core';
import { FileFinder } from '../FileFinder';
import { Inputs } from '../Inputs';
import { NextcloudClient } from './NextcloudClient';
import { NoFileOption } from '../NoFileOption';
import * as core from '@actions/core'
import * as github from '@actions/github'
import { GitHub } from '@actions/github/lib/utils'
import { FileFinder } from '../FileFinder'
import { Inputs } from '../Inputs'
import { NextcloudClient } from './NextcloudClient'
import { NoFileOption } from '../NoFileOption'
export class NextcloudArtifact {
public constructor(
private name: string,
private path: string,
private errorBehavior: NoFileOption) { }
readonly octokit: InstanceType<typeof GitHub>
readonly context = NextcloudArtifact.getCheckRunContext()
readonly token: string
readonly name: string
readonly artifactTitle: string
readonly path: string
readonly errorBehavior: NoFileOption
public async run() {
const fileFinder = new FileFinder(this.path);
const files = await fileFinder.findFiles();
constructor(private inputs: Inputs) {
this.token = inputs.Token
this.name = inputs.ArtifactName
this.artifactTitle = `Nextcloud - ${this.name}`
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) {
await this.uploadFiles(files);
}
else {
this.logNoFilesFound();
await this.uploadFiles(files)
} else {
this.logNoFilesFound()
}
}
private async uploadFiles(files: { filesToUpload: string[]; rootDirectory: string; }) {
this.logUpload(files.filesToUpload.length, files.rootDirectory);
private static getCheckRunContext(): { sha: string; runId: number } {
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);
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: this.artifactTitle,
status: 'in_progress',
output: {
title: `Nextcloud - ${this.name}`,
summary: 'Uploading...'
},
...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)
core.info(`Nextcloud shareable URL: ${shareableUrl}`)
const resp = await this.octokit.rest.checks.update({
check_run_id: createResp.data.id,
conclusion: 'success',
status: 'completed',
output: {
title: this.artifactTitle,
summary: 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.trySetFailed(createResp.data.id)
core.setFailed(error)
}
}
private async trySetFailed(checkId: number) {
try {
await this.octokit.rest.checks.update({
check_run_id: checkId,
conclusion: 'failure',
status: 'completed',
output: {
title: this.artifactTitle,
summary: 'Check failed.'
},
...github.context.repo
})
return true
} catch (error) {
core.error(`Failed to update check status to failure`)
return false
}
}
private logUpload(fileCount: number, rootDirectory: string) {
const s = fileCount === 1 ? '' : 's';
core.info(
`With the provided path, there will be ${fileCount} file${s} uploaded`
);
core.debug(`Root artifact directory is ${rootDirectory}`);
const s = fileCount === 1 ? '' : 's'
core.info(`With the provided path, there will be ${fileCount} file${s} uploaded`)
core.debug(`Root artifact directory is ${rootDirectory}`)
if (fileCount > 10000) {
core.warning(
`There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.`
);
)
}
}
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) {
case NoFileOption.warn: {
core.warning(errorMessage);
break;
core.warning(errorMessage)
break
}
case NoFileOption.error: {
core.setFailed(errorMessage);
break;
core.setFailed(errorMessage)
break
}
case NoFileOption.ignore: {
core.info(errorMessage);
break;
core.info(errorMessage)
break
}
}
}

View File

@ -0,0 +1,303 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
exports.__esModule = true;
exports.NextcloudClient = void 0;
var fsSync = require("fs");
var path = require("path");
var core = require("@actions/core");
var os = require("os");
var archiver = require("archiver");
var node_fetch_1 = require("node-fetch");
var btoa_1 = require("btoa");
var uuid_1 = require("uuid");
var webdav = require("webdav");
var fs = fsSync.promises;
var NextcloudClient = /** @class */ (function () {
function NextcloudClient(endpoint, artifact, rootDirectory, username, password) {
this.endpoint = endpoint;
this.artifact = artifact;
this.rootDirectory = rootDirectory;
this.username = username;
this.password = password;
this.guid = uuid_1.v4();
this.headers = { 'Authorization': 'Basic ' + btoa_1["default"](this.username + ":" + this.password) };
this.davClient = webdav.createClient(this.endpoint + "/remote.php/dav/files/" + this.username, {
username: this.username,
password: this.password
});
}
NextcloudClient.prototype.uploadFiles = function (files) {
return __awaiter(this, void 0, void 0, function () {
var spec, zip, path;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
core.info("Preparing upload...");
spec = this.uploadSpec(files);
core.info("Zipping files...");
return [4 /*yield*/, this.zipFiles(spec)];
case 1:
zip = _a.sent();
core.info("Uploading to Nextcloud...");
return [4 /*yield*/, this.upload(zip)];
case 2:
path = _a.sent();
core.info("File path: " + path);
core.info("Sharing file...");
return [4 /*yield*/, this.shareFile(path)];
case 3:
_a.sent();
return [2 /*return*/];
}
});
});
};
NextcloudClient.prototype.uploadSpec = function (files) {
var 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
var root = path.normalize(this.rootDirectory);
root = path.resolve(root);
/*
Example to demonstrate behavior
Input:
artifactName: my-artifact
rootDirectory: '/home/user/files/plz-upload'
artifactFiles: [
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
]
Output:
specifications: [
['/home/user/files/plz-upload/file1.txt', 'my-artifact/file1.txt'],
['/home/user/files/plz-upload/file1.txt', 'my-artifact/file2.txt'],
['/home/user/files/plz-upload/file1.txt', 'my-artifact/dir/file3.txt']
]
*/
for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
var file = files_1[_i];
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
var 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
path.join handles all the following cases and would return 'artifact-name/file-to-upload.txt
join('artifact-name/', 'file-to-upload.txt')
join('artifact-name/', '/file-to-upload.txt')
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;
};
NextcloudClient.prototype.zipFiles = function (specs) {
return __awaiter(this, void 0, void 0, function () {
var tempArtifactDir, artifactPath, copies, _i, specs_1, spec, dstpath, dstDir, _a, _b, _c, archivePath, _d, _e, _f;
return __generator(this, function (_g) {
switch (_g.label) {
case 0:
tempArtifactDir = path.join(os.tmpdir(), this.guid);
artifactPath = path.join(tempArtifactDir, "artifact-" + this.artifact);
return [4 /*yield*/, fs.mkdir(path.join(artifactPath, this.artifact), { recursive: true })];
case 1:
_g.sent();
copies = [];
_i = 0, specs_1 = specs;
_g.label = 2;
case 2:
if (!(_i < specs_1.length)) return [3 /*break*/, 6];
spec = specs_1[_i];
dstpath = path.join(artifactPath, spec.uploadPath);
dstDir = path.dirname(dstpath);
if (!!fsSync.existsSync(dstDir)) return [3 /*break*/, 4];
return [4 /*yield*/, fs.mkdir(dstDir, { recursive: true })];
case 3:
_g.sent();
_g.label = 4;
case 4:
copies.push(fs.copyFile(spec.absolutePath, dstpath));
_g.label = 5;
case 5:
_i++;
return [3 /*break*/, 2];
case 6: return [4 /*yield*/, Promise.all(copies)];
case 7:
_g.sent();
_b = (_a = core).info;
_c = "files: ";
return [4 /*yield*/, fs.readdir(path.join(artifactPath, this.artifact))];
case 8:
_b.apply(_a, [_c + (_g.sent())]);
archivePath = path.join(artifactPath, this.artifact + ".zip");
return [4 /*yield*/, this.zip(path.join(artifactPath, this.artifact), archivePath)];
case 9:
_g.sent();
_e = (_d = core).info;
_f = "archive stat: ";
return [4 /*yield*/, fs.stat(archivePath)];
case 10:
_e.apply(_d, [_f + (_g.sent()).size]);
return [2 /*return*/, archivePath];
}
});
});
};
NextcloudClient.prototype.zip = function (dirpath, destpath) {
return __awaiter(this, void 0, void 0, function () {
var archive, stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
archive = archiver.create('zip', { zlib: { level: 9 } });
stream = archive.directory(dirpath, false)
.pipe(fsSync.createWriteStream(destpath));
return [4 /*yield*/, archive.finalize()];
case 1:
_a.sent();
return [4 /*yield*/, new Promise(function (resolve, reject) {
stream.on('error', function (e) { return reject(e); })
.on('close', function () { return resolve(); });
})];
case 2: return [2 /*return*/, _a.sent()];
}
});
});
};
NextcloudClient.prototype.upload = function (file) {
return __awaiter(this, void 0, void 0, function () {
var remoteFileDir, remoteFilePath;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
remoteFileDir = "/artifacts/" + this.guid;
core.info("Checking directory...");
return [4 /*yield*/, this.davClient.exists(remoteFileDir)];
case 1:
if (!!(_a.sent())) return [3 /*break*/, 3];
core.info("Creating directory...");
return [4 /*yield*/, this.davClient.createDirectory(remoteFileDir, { recursive: true })];
case 2:
_a.sent();
_a.label = 3;
case 3:
remoteFilePath = remoteFileDir + "/" + this.artifact + ".zip";
core.info("Transferring file... (" + file + ")");
return [4 /*yield*/, this.transferFile(remoteFilePath, file)];
case 4:
_a.sent();
core.info("finish");
return [2 /*return*/, remoteFilePath];
}
});
});
};
NextcloudClient.prototype.transferFile = function (remoteFilePath, file) {
var fileStream = fsSync.createReadStream(file);
var fileStreamPromise = new Promise(function (resolve, reject) {
fileStream.on('error', function () { return reject("Failed to read file"); })
.on('end', function () { return resolve(); });
});
var remoteStream = this.davClient.createWriteStream(remoteFilePath);
fileStream.pipe(remoteStream);
var remoteStreamPromise = new Promise(function (resolve, reject) {
remoteStream.on('error', function () { return reject("Failed to upload file"); })
.on('close', function () { return resolve(); });
});
return Promise.all([remoteStreamPromise, fileStreamPromise]);
};
NextcloudClient.prototype.shareFile = function (remoteFilePath) {
return __awaiter(this, void 0, void 0, function () {
var url, body, res, _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
url = this.endpoint + "/ocs/v2.php/apps/files_sharing/api/v1/shares";
body = {
path: remoteFilePath,
shareType: 3,
publicUpload: "false",
permissions: 1
};
return [4 /*yield*/, node_fetch_1["default"](url, {
method: 'PUT',
headers: this.headers,
body: JSON.stringify(body)
})];
case 1:
res = _c.sent();
res.status;
_b = (_a = core).info;
return [4 /*yield*/, res.text()];
case 2:
_b.apply(_a, [_c.sent()]);
return [2 /*return*/];
}
});
});
};
return NextcloudClient;
}());
exports.NextcloudClient = NextcloudClient;

View File

@ -1,160 +1,183 @@
import * as fsSync from 'fs'
import * as fs from 'fs/promises'
import * as path from 'path'
import core from '@actions/core';
import * as os from 'os';
import { randomUUID } from 'crypto';
import * as archiver from 'archiver';
import { URL } from 'url';
import fetch, { HeadersInit } from 'node-fetch';
import { Inputs } from '../Inputs';
import btoa from 'btoa';
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'
import { URL } from 'url'
const fs = fsSync.promises
interface FileSpec {
absolutePath: string,
absolutePath: string
uploadPath: string
}
export class NextcloudClient {
private guid: string;
private headers: HeadersInit;
private guid: string
private headers: HeadersInit
private davClient
public constructor(
private endpoint: string,
constructor(
private endpoint: URL,
private artifact: string,
private rootDirectory: string) {
this.guid = randomUUID();
this.headers = {'Authorization': 'Basic ' + btoa(`${Inputs.Username}:${Inputs.Password}`)};
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.href}remote.php/dav/files/${this.username}`, {
username: this.username,
password: this.password
})
}
public async uploadFiles(files: string[]) {
const spec = this.uploadSpec(files);
var zip = await this.zipFiles(spec);
const path = await this.upload(zip);
await this.shareFile(path);
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(`Remote file path: ${filePath}`)
return await this.shareFile(filePath)
}
private uploadSpec(files: string[]): FileSpec[] {
const specifications = [];
const specifications = []
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()) {
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
let root = path.normalize(this.rootDirectory);
root = path.resolve(root);
/*
Example to demonstrate behavior
Input:
artifactName: my-artifact
rootDirectory: '/home/user/files/plz-upload'
artifactFiles: [
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
]
Output:
specifications: [
['/home/user/files/plz-upload/file1.txt', 'my-artifact/file1.txt'],
['/home/user/files/plz-upload/file1.txt', 'my-artifact/file2.txt'],
['/home/user/files/plz-upload/file1.txt', 'my-artifact/dir/file3.txt']
]
*/
let root = path.normalize(this.rootDirectory)
root = path.resolve(root)
for (let file of files) {
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()) {
// Normalize and resolve, this allows for either absolute or relative paths to be used
file = path.normalize(file);
file = path.resolve(file);
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}`);
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
path.join handles all the following cases and would return 'artifact-name/file-to-upload.txt
join('artifact-name/', 'file-to-upload.txt')
join('artifact-name/', '/file-to-upload.txt')
join('artifact-name', 'file-to-upload.txt')
join('artifact-name', '/file-to-upload.txt')
*/
const uploadPath = file.replace(root, '')
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`);
})
} else {
core.debug(`Removing ${file} from rawSearchResults because it is a directory`)
}
}
return specifications;
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(artifactPath, { recursive: true });
for (let spec of specs) {
await fs.copyFile(spec.absolutePath, path.join(artifactPath, spec.uploadPath));
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 })
}
const archivePath = path.join(artifactPath, `${this.artifact}.zip`);
await this.zip(path.join(artifactPath, this.artifact), archivePath);
copies.push(fs.copyFile(spec.absolutePath, dstpath))
}
return archivePath;
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)
return archivePath
}
private async zip(dirpath: string, destpath: string) {
const archive = archiver.create('zip', { zlib: { level: 9 } });
const stream = fsSync.createWriteStream(destpath);
archive.directory(dirpath, false)
.on('error', e => Promise.reject())
.on('close', () => Promise.resolve())
.pipe(stream);
const archive = archiver.create('zip', { zlib: { level: 9 } })
const stream = archive.directory(dirpath, false).pipe(fsSync.createWriteStream(destpath))
return archive.finalize();
await archive.finalize()
return await new Promise<void>((resolve, reject) => {
stream.on('error', e => reject(e)).on('close', () => resolve())
})
}
private async upload(file: string) {
const filePath = `/artifacts/${this.guid}/${this.artifact}`;
const url = this.endpoint + `/remote.php/dav/files/${Inputs.Username}` + filePath;
const stream = fsSync.createReadStream(file);
const res = await fetch(url, {
method: 'PUT',
body: stream,
headers: this.headers
});
core.debug(await res.json())
return filePath;
private async upload(file: string): Promise<string> {
const remoteFileDir = `/artifacts/${this.guid}`
if (!(await this.davClient.exists(remoteFileDir))) {
await this.davClient.createDirectory(remoteFileDir, { recursive: true })
}
private async shareFile(nextcloudPath: string) {
const url = this.endpoint + `/ocs/v2.php/apps/files_sharing/api/v1/shares`;
const remoteFilePath = `${remoteFileDir}/${this.artifact}.zip`
core.debug(`Transferring file... (${file})`)
const fileStat = await fs.stat(file)
const fileStream = fsSync.createReadStream(file)
const fileStreamPromise = new Promise<void>((resolve, reject) => {
fileStream.on('error', e => reject(e)).on('close', () => resolve())
})
const remoteStream = this.davClient.createWriteStream(remoteFilePath, {
headers: { 'Content-Length': fileStat.size.toString() }
})
const remoteStreamPromise = new Promise<void>((resolve, reject) => {
remoteStream.on('error', e => reject(e)).on('finish', () => resolve())
})
fileStream.pipe(remoteStream)
const timer = setTimeout(() => {}, 20_000)
await Promise.all([fileStreamPromise, remoteStreamPromise])
// HACK: Nextcloud has not fully processed the file, despite returning 200.
// Waiting for 1s seems to do the trick.
await new Promise(resolve => setTimeout(resolve, 1_000))
clearTimeout(timer)
return remoteFilePath
}
private async shareFile(remoteFilePath: string): Promise<string> {
const url = `${this.endpoint.href}ocs/v2.php/apps/files_sharing/api/v1/shares`
const body = {
path: nextcloudPath,
path: remoteFilePath,
shareType: 3,
publicUpload: "false",
permissions: 1,
};
publicUpload: 'false',
permissions: 1
}
const res = await fetch(url, {
method: 'PUT',
headers: this.headers,
body: JSON.stringify(body),
});
method: 'POST',
headers: Object.assign(this.headers, {
'OCS-APIRequest': true,
'Content-Type': 'application/json'
}),
body: JSON.stringify(body)
})
core.debug(await res.json())
const result = await res.text()
core.debug(`Share response: ${result}`)
const re = /<url>(?<share_url>.*)<\/url>/
const match = re.exec(result)
core.debug(`Match groups:\n${JSON.stringify(match?.groups)}`)
const sharableUrl = (match?.groups || {})['share_url']
if (!sharableUrl) {
throw new Error(`Failed to parse or find sharable URL:\n${result}`)
}
return sharableUrl
}
}

View File

@ -1,14 +1,16 @@
{
"compilerOptions": {
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"noImplicitAny": true,
"rootDir": "src",
"outDir": "dist",
"outDir": "lib",
"sourceMap": true,
"lib": ["es6"]
}
"lib": ["ES2019"]
},
"exclude": ["node_modules", "__tests__/**/*.ts"]
}

9
workspace.code-workspace Normal file
View File

@ -0,0 +1,9 @@
{
"folders": [
{
"path": "."
}
],
"remoteAuthority": "wsl+Ubuntu-20.04",
"settings": {}
}