@shopify/oxygen-cli 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/dist/commands/oxygen/deploy.d.ts +26 -0
- package/dist/commands/oxygen/deploy.js +121 -0
- package/dist/deploy/build-cancel.d.ts +6 -0
- package/dist/deploy/build-cancel.js +36 -0
- package/dist/deploy/build-cancel.test.d.ts +2 -0
- package/dist/deploy/build-cancel.test.js +73 -0
- package/dist/deploy/build-initiate.d.ts +6 -0
- package/dist/deploy/build-initiate.js +38 -0
- package/dist/deploy/build-initiate.test.d.ts +2 -0
- package/dist/deploy/build-initiate.test.js +81 -0
- package/dist/deploy/build-project.d.ts +5 -0
- package/dist/deploy/build-project.js +32 -0
- package/dist/deploy/build-project.test.d.ts +2 -0
- package/dist/deploy/build-project.test.js +53 -0
- package/dist/deploy/deployment-cancel.d.ts +6 -0
- package/dist/deploy/deployment-cancel.js +36 -0
- package/dist/deploy/deployment-cancel.test.d.ts +2 -0
- package/dist/deploy/deployment-cancel.test.js +78 -0
- package/dist/deploy/deployment-complete.d.ts +6 -0
- package/dist/deploy/deployment-complete.js +33 -0
- package/dist/deploy/deployment-complete.test.d.ts +2 -0
- package/dist/deploy/deployment-complete.test.js +77 -0
- package/dist/deploy/deployment-initiate.d.ts +17 -0
- package/dist/deploy/deployment-initiate.js +40 -0
- package/dist/deploy/deployment-initiate.test.d.ts +2 -0
- package/dist/deploy/deployment-initiate.test.js +136 -0
- package/dist/deploy/get-upload-files.d.ts +5 -0
- package/dist/deploy/get-upload-files.js +65 -0
- package/dist/deploy/get-upload-files.test.d.ts +2 -0
- package/dist/deploy/get-upload-files.test.js +56 -0
- package/dist/deploy/graphql/build-cancel.d.ts +14 -0
- package/dist/deploy/graphql/build-cancel.js +14 -0
- package/dist/deploy/graphql/build-initiate.d.ts +15 -0
- package/dist/deploy/graphql/build-initiate.js +15 -0
- package/dist/deploy/graphql/deployment-cancel.d.ts +14 -0
- package/dist/deploy/graphql/deployment-cancel.js +14 -0
- package/dist/deploy/graphql/deployment-complete.d.ts +17 -0
- package/dist/deploy/graphql/deployment-complete.js +16 -0
- package/dist/deploy/graphql/deployment-initiate.d.ts +28 -0
- package/dist/deploy/graphql/deployment-initiate.js +25 -0
- package/dist/deploy/index.d.ts +5 -0
- package/dist/deploy/index.js +74 -0
- package/dist/deploy/metadata.d.ts +11 -0
- package/dist/deploy/metadata.js +65 -0
- package/dist/deploy/metadata.test.d.ts +2 -0
- package/dist/deploy/metadata.test.js +131 -0
- package/dist/deploy/types.d.ts +52 -0
- package/dist/deploy/types.js +7 -0
- package/dist/deploy/upload-files.d.ts +6 -0
- package/dist/deploy/upload-files.js +156 -0
- package/dist/deploy/upload-files.test.d.ts +2 -0
- package/dist/deploy/upload-files.test.js +194 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/oxygen-cli.d.ts +1 -0
- package/dist/oxygen-cli.js +5 -0
- package/dist/utils/test-helper.d.ts +14 -0
- package/dist/utils/test-helper.js +27 -0
- package/dist/utils/utils.d.ts +20 -0
- package/dist/utils/utils.js +126 -0
- package/dist/utils/utils.test.d.ts +2 -0
- package/dist/utils/utils.test.js +154 -0
- package/oclif.manifest.json +108 -0
- package/package.json +68 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
import { OxygenError } from '../types.js';
|
2
|
+
|
3
|
+
declare const DeploymentInitiateQuery = "\n mutation DeploymentInitiate($buildId: ID, $environment: EnvironmentSelectorInput, $labels: [String!], $files: [FileInput!]!) {\n deploymentInitiate(buildId: $buildId, environment: $environment, labels: $labels, files: $files) {\n deployment {\n id\n status\n }\n deploymentTargets {\n filePath\n fileSize\n uploadUrl\n fileType\n parameters {\n name\n value\n }\n }\n userErrors {\n message\n }\n }\n }\n";
|
4
|
+
interface DeploymentInitiateQueryData {
|
5
|
+
deploymentInitiate: DeploymentInitiateResponse;
|
6
|
+
}
|
7
|
+
interface DeploymentInitiateResponse {
|
8
|
+
deployment: Deployment;
|
9
|
+
deploymentTargets: DeploymentTargetResponse[];
|
10
|
+
userErrors: OxygenError[];
|
11
|
+
}
|
12
|
+
interface DeploymentTargetResponse {
|
13
|
+
filePath: string;
|
14
|
+
fileSize: number;
|
15
|
+
uploadUrl: string;
|
16
|
+
fileType: string;
|
17
|
+
parameters: DeploymentInitiateParameters[] | null;
|
18
|
+
}
|
19
|
+
interface Deployment {
|
20
|
+
id: string;
|
21
|
+
status: string;
|
22
|
+
}
|
23
|
+
interface DeploymentInitiateParameters {
|
24
|
+
name: string;
|
25
|
+
value: string;
|
26
|
+
}
|
27
|
+
|
28
|
+
export { DeploymentInitiateQuery, DeploymentInitiateQueryData, DeploymentInitiateResponse, DeploymentTargetResponse };
|
@@ -0,0 +1,25 @@
|
|
1
|
+
const DeploymentInitiateQuery = `
|
2
|
+
mutation DeploymentInitiate($buildId: ID, $environment: EnvironmentSelectorInput, $labels: [String!], $files: [FileInput!]!) {
|
3
|
+
deploymentInitiate(buildId: $buildId, environment: $environment, labels: $labels, files: $files) {
|
4
|
+
deployment {
|
5
|
+
id
|
6
|
+
status
|
7
|
+
}
|
8
|
+
deploymentTargets {
|
9
|
+
filePath
|
10
|
+
fileSize
|
11
|
+
uploadUrl
|
12
|
+
fileType
|
13
|
+
parameters {
|
14
|
+
name
|
15
|
+
value
|
16
|
+
}
|
17
|
+
}
|
18
|
+
userErrors {
|
19
|
+
message
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
`;
|
24
|
+
|
25
|
+
export { DeploymentInitiateQuery };
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import { outputSuccess, outputWarn, consoleError } from '@shopify/cli-kit/node/output';
|
2
|
+
import { verifyConfig } from '../utils/utils.js';
|
3
|
+
import { buildInitiate } from './build-initiate.js';
|
4
|
+
import { buildCancel } from './build-cancel.js';
|
5
|
+
import { getUploadFiles } from './get-upload-files.js';
|
6
|
+
import { deploymentInitiate } from './deployment-initiate.js';
|
7
|
+
import { deploymentComplete } from './deployment-complete.js';
|
8
|
+
import { deploymentCancel } from './deployment-cancel.js';
|
9
|
+
import { uploadFiles } from './upload-files.js';
|
10
|
+
import { buildProject } from './build-project.js';
|
11
|
+
import { getMetadata, createLabels, getEnvironmentInput } from './metadata.js';
|
12
|
+
|
13
|
+
async function createDeploy(config) {
|
14
|
+
const build = {};
|
15
|
+
let buildCompleted;
|
16
|
+
let deployment;
|
17
|
+
try {
|
18
|
+
const metadata = await getMetadata(config);
|
19
|
+
const labels = createLabels(metadata);
|
20
|
+
const environment = getEnvironmentInput(config, metadata);
|
21
|
+
if (!config.workerOnly && !config.skipBuild) {
|
22
|
+
const buildInitiateResponse = await buildInitiate(
|
23
|
+
config,
|
24
|
+
environment,
|
25
|
+
labels
|
26
|
+
);
|
27
|
+
build.id = buildInitiateResponse.build.id;
|
28
|
+
build.assetPath = buildInitiateResponse.build.assetPath;
|
29
|
+
}
|
30
|
+
if (!config.skipBuild) {
|
31
|
+
await buildProject(config, build.assetPath);
|
32
|
+
verifyConfig({ config, performedBuild: true });
|
33
|
+
}
|
34
|
+
buildCompleted = true;
|
35
|
+
const manifest = await getUploadFiles(config);
|
36
|
+
const deploymentInitiateInput = build.id ? { buildId: build.id, manifest } : { environment, manifest, labels };
|
37
|
+
deployment = await deploymentInitiate(config, deploymentInitiateInput);
|
38
|
+
await uploadFiles(config, deployment.deploymentTargets);
|
39
|
+
const deploymentCompleteOp = await deploymentComplete(
|
40
|
+
config,
|
41
|
+
deployment.deployment.id
|
42
|
+
);
|
43
|
+
outputSuccess(
|
44
|
+
`Deployment complete: ${deploymentCompleteOp.deployment.url}`
|
45
|
+
);
|
46
|
+
} catch (error) {
|
47
|
+
if (!(error instanceof Error)) {
|
48
|
+
console.error("Unknown error", error);
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
if (build.id && !buildCompleted) {
|
52
|
+
outputWarn(`Build failed with: ${error.message}, cancelling build.`);
|
53
|
+
await buildCancel(config, build.id, error.message).catch((err) => {
|
54
|
+
if (err instanceof Error) {
|
55
|
+
outputWarn(`Failed to cancel build: ${err.message}`);
|
56
|
+
}
|
57
|
+
});
|
58
|
+
} else if (deployment?.deployment.id) {
|
59
|
+
outputWarn(
|
60
|
+
`Deployment failed with: ${error.message}, cancelling deployment.`
|
61
|
+
);
|
62
|
+
await deploymentCancel(config, deployment.deployment.id, "failed").catch(
|
63
|
+
(err) => {
|
64
|
+
if (err instanceof Error) {
|
65
|
+
outputWarn(`Failed to cancel deployment: ${err.message}`);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
);
|
69
|
+
}
|
70
|
+
consoleError(error.message);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
export { createDeploy };
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { CIMetadata } from '@shopify/cli-kit/node/context/local';
|
2
|
+
import { DeployConfig, EnvironmentInput } from './types.js';
|
3
|
+
|
4
|
+
type Metadata = CIMetadata & {
|
5
|
+
name: string;
|
6
|
+
};
|
7
|
+
declare function getMetadata(config: DeployConfig): Promise<Metadata>;
|
8
|
+
declare function getEnvironmentInput(config: DeployConfig, metadata: CIMetadata): EnvironmentInput | undefined;
|
9
|
+
declare function createLabels(metadata: Metadata): string[];
|
10
|
+
|
11
|
+
export { createLabels, getEnvironmentInput, getMetadata };
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import { ciPlatform } from '@shopify/cli-kit/node/context/local';
|
2
|
+
import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
|
3
|
+
import { outputWarn } from '@shopify/cli-kit/node/output';
|
4
|
+
import { maxLabelLength } from '../utils/utils.js';
|
5
|
+
|
6
|
+
async function getMetadata(config) {
|
7
|
+
const ciInfo = ciPlatform();
|
8
|
+
let metadata = {};
|
9
|
+
if (ciInfo.isCI && ciInfo.name !== "unknown") {
|
10
|
+
metadata = ciInfo.metadata;
|
11
|
+
} else {
|
12
|
+
try {
|
13
|
+
const gitCommit = await getLatestGitCommit(config.rootPath);
|
14
|
+
const branch = gitCommit.refs.split(" ");
|
15
|
+
metadata = {
|
16
|
+
actor: gitCommit.author_name,
|
17
|
+
branch: branch[branch.length - 1],
|
18
|
+
commitSha: gitCommit.hash,
|
19
|
+
commitMessage: gitCommit.message
|
20
|
+
};
|
21
|
+
} catch (error) {
|
22
|
+
outputWarn("No CI metadata loaded from environment");
|
23
|
+
}
|
24
|
+
}
|
25
|
+
return {
|
26
|
+
name: ciInfo.isCI ? ciInfo.name : "unknown",
|
27
|
+
...metadata,
|
28
|
+
actor: config.metadata.user ?? metadata.actor,
|
29
|
+
commitSha: config.metadata.version ?? metadata.commitSha,
|
30
|
+
url: config.metadata.url ?? metadata.url
|
31
|
+
};
|
32
|
+
}
|
33
|
+
function getEnvironmentInput(config, metadata) {
|
34
|
+
const tag = config.environmentTag || metadata.branch;
|
35
|
+
return tag ? { tag } : void 0;
|
36
|
+
}
|
37
|
+
function createLabels(metadata) {
|
38
|
+
const labels = [];
|
39
|
+
const checkLabel = (labelKey, labelValue) => {
|
40
|
+
if (labelValue.length > maxLabelLength) {
|
41
|
+
throw new Error(
|
42
|
+
`Provided ${labelKey} metadata exceeds maximum length (max ${maxLabelLength} characters).`
|
43
|
+
);
|
44
|
+
}
|
45
|
+
const label = `${labelKey}=${JSON.stringify(labelValue)}`;
|
46
|
+
labels.push(label);
|
47
|
+
};
|
48
|
+
if (metadata.name !== "unknown" && metadata.run) {
|
49
|
+
labels.push(`${metadata.name}-runId=${JSON.stringify(metadata.run)}`);
|
50
|
+
}
|
51
|
+
const keys = ["url", "actor", "commitSha"];
|
52
|
+
const keyMapping = {
|
53
|
+
actor: "user",
|
54
|
+
commitSha: "version"
|
55
|
+
};
|
56
|
+
keys.forEach((key) => {
|
57
|
+
if (metadata[key]) {
|
58
|
+
const labelKey = keyMapping[key] || key;
|
59
|
+
checkLabel(labelKey, metadata[key]);
|
60
|
+
}
|
61
|
+
});
|
62
|
+
return labels;
|
63
|
+
}
|
64
|
+
|
65
|
+
export { createLabels, getEnvironmentInput, getMetadata };
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
|
2
|
+
import { ciPlatform } from '@shopify/cli-kit/node/context/local';
|
3
|
+
import { vi, describe, test, expect } from 'vitest';
|
4
|
+
import { maxLabelLength } from '../utils/utils.js';
|
5
|
+
import { createTestConfig } from '../utils/test-helper.js';
|
6
|
+
import { getMetadata, getEnvironmentInput, createLabels } from './metadata.js';
|
7
|
+
|
8
|
+
vi.mock("@shopify/cli-kit/node/context/local");
|
9
|
+
vi.mock("@shopify/cli-kit/node/git");
|
10
|
+
describe("getMetadata", () => {
|
11
|
+
test("should return custom metadata from environment", async () => {
|
12
|
+
vi.mocked(ciPlatform).mockReturnValueOnce({
|
13
|
+
isCI: true,
|
14
|
+
name: "circle",
|
15
|
+
metadata: {
|
16
|
+
actor: "circle_actor",
|
17
|
+
commitSha: "circle_sha",
|
18
|
+
url: "circle_url"
|
19
|
+
}
|
20
|
+
});
|
21
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
22
|
+
const metadataResult = await getMetadata(testConfig);
|
23
|
+
expect(metadataResult.actor).toBe("circle_actor");
|
24
|
+
expect(metadataResult.commitSha).toBe("circle_sha");
|
25
|
+
expect(metadataResult.name).toBe("circle");
|
26
|
+
expect(metadataResult.url).toBe("circle_url");
|
27
|
+
});
|
28
|
+
test("should return custom metadata when provided in config", async () => {
|
29
|
+
vi.mocked(ciPlatform).mockReturnValueOnce({
|
30
|
+
isCI: true,
|
31
|
+
name: "circle",
|
32
|
+
metadata: {
|
33
|
+
actor: "circle_actor",
|
34
|
+
commitSha: "circle_sha",
|
35
|
+
url: "circle_url"
|
36
|
+
}
|
37
|
+
});
|
38
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
39
|
+
testConfig.metadata = {
|
40
|
+
user: "custom_user",
|
41
|
+
version: "custom_version",
|
42
|
+
url: "custom_url"
|
43
|
+
};
|
44
|
+
const metadataResult = await getMetadata(testConfig);
|
45
|
+
expect(metadataResult.actor).toBe("custom_user");
|
46
|
+
expect(metadataResult.commitSha).toBe("custom_version");
|
47
|
+
expect(metadataResult.name).toBe("circle");
|
48
|
+
expect(metadataResult.url).toBe("custom_url");
|
49
|
+
});
|
50
|
+
test("should return commit data from latest commit through getLastestGitCommit", async () => {
|
51
|
+
vi.mocked(ciPlatform).mockReturnValueOnce({
|
52
|
+
isCI: false
|
53
|
+
});
|
54
|
+
vi.mocked(getLatestGitCommit).mockResolvedValueOnce({
|
55
|
+
author_name: "gh_author",
|
56
|
+
author_email: "test@github.com",
|
57
|
+
hash: "gh_hash",
|
58
|
+
message: "gh_message",
|
59
|
+
refs: "gh_refs",
|
60
|
+
date: "gh_date",
|
61
|
+
body: "gh_body"
|
62
|
+
});
|
63
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
64
|
+
const metadataResult = await getMetadata(testConfig);
|
65
|
+
expect(metadataResult.actor).toBe("gh_author");
|
66
|
+
expect(metadataResult.commitSha).toBe("gh_hash");
|
67
|
+
expect(metadataResult.name).toBe("unknown");
|
68
|
+
expect(metadataResult.url).toBe(void 0);
|
69
|
+
});
|
70
|
+
});
|
71
|
+
describe("getEnvironmentInput", () => {
|
72
|
+
test("should return environment tag from config", () => {
|
73
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
74
|
+
testConfig.environmentTag = "test_tag";
|
75
|
+
const metadata = {
|
76
|
+
branch: "test_branch"
|
77
|
+
};
|
78
|
+
const environmentInput = getEnvironmentInput(testConfig, metadata);
|
79
|
+
expect(environmentInput.tag).toBe("test_tag");
|
80
|
+
});
|
81
|
+
test("should return environment tag from metadata", () => {
|
82
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
83
|
+
delete testConfig.environmentTag;
|
84
|
+
const metadata = {
|
85
|
+
branch: "test_branch"
|
86
|
+
};
|
87
|
+
const environmentInput = getEnvironmentInput(testConfig, metadata);
|
88
|
+
expect(environmentInput.tag).toBe("test_branch");
|
89
|
+
});
|
90
|
+
test("should return undefined if no environment tag is provided", () => {
|
91
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
92
|
+
delete testConfig.environmentTag;
|
93
|
+
const metadata = {};
|
94
|
+
expect(getEnvironmentInput(testConfig, metadata)).toBe(void 0);
|
95
|
+
});
|
96
|
+
});
|
97
|
+
describe("createLabels", () => {
|
98
|
+
test("should return labels from metadata", () => {
|
99
|
+
const metadata = {
|
100
|
+
name: "circle",
|
101
|
+
run: "circle_1234",
|
102
|
+
url: "http://circleci.com/workflow/123",
|
103
|
+
actor: "circle_actor",
|
104
|
+
commitSha: "circle_sha"
|
105
|
+
};
|
106
|
+
const labels = [
|
107
|
+
'circle-runId="circle_1234"',
|
108
|
+
'url="http://circleci.com/workflow/123"',
|
109
|
+
'user="circle_actor"',
|
110
|
+
'version="circle_sha"'
|
111
|
+
];
|
112
|
+
expect(createLabels(metadata)).toEqual(labels);
|
113
|
+
});
|
114
|
+
test("should return labels from metadata only for populated fields", () => {
|
115
|
+
const metadata = {
|
116
|
+
name: "circle",
|
117
|
+
run: "circle_run"
|
118
|
+
};
|
119
|
+
const labels = ['circle-runId="circle_run"'];
|
120
|
+
expect(createLabels(metadata)).toEqual(labels);
|
121
|
+
});
|
122
|
+
test("should throw when user metadata exceeds maximum length", () => {
|
123
|
+
const metadata = {
|
124
|
+
actor: "a".repeat(101),
|
125
|
+
name: "unknown"
|
126
|
+
};
|
127
|
+
expect(() => createLabels(metadata)).toThrow(
|
128
|
+
`Provided user metadata exceeds maximum length (max ${maxLabelLength} characters).`
|
129
|
+
);
|
130
|
+
});
|
131
|
+
});
|
@@ -0,0 +1,52 @@
|
|
1
|
+
interface Build {
|
2
|
+
id: string;
|
3
|
+
assetPath: string;
|
4
|
+
}
|
5
|
+
interface ClientError extends Error {
|
6
|
+
statusCode: number;
|
7
|
+
}
|
8
|
+
interface DeployConfig {
|
9
|
+
assetsDir?: string;
|
10
|
+
buildCommand: string;
|
11
|
+
deploymentToken: DeploymentToken;
|
12
|
+
deploymentUrl: string;
|
13
|
+
environmentTag?: string;
|
14
|
+
metadata: {
|
15
|
+
user?: string;
|
16
|
+
version?: string;
|
17
|
+
url?: string;
|
18
|
+
};
|
19
|
+
rootPath?: string;
|
20
|
+
skipBuild: boolean;
|
21
|
+
workerDir?: string;
|
22
|
+
workerOnly: boolean;
|
23
|
+
}
|
24
|
+
interface DeploymentToken {
|
25
|
+
accessToken: string;
|
26
|
+
allowedResource: string;
|
27
|
+
appId: string;
|
28
|
+
client: string;
|
29
|
+
expiresAt: string;
|
30
|
+
namespace: string;
|
31
|
+
namespaceId: string;
|
32
|
+
}
|
33
|
+
interface DeploymentManifestFile {
|
34
|
+
filePath: string;
|
35
|
+
fileSize: number;
|
36
|
+
mimeType: string;
|
37
|
+
fileHash: string;
|
38
|
+
fileType: string;
|
39
|
+
}
|
40
|
+
interface EnvironmentInput {
|
41
|
+
handle?: string;
|
42
|
+
tag?: string;
|
43
|
+
}
|
44
|
+
declare enum FileType {
|
45
|
+
Worker = "WORKER",
|
46
|
+
Asset = "ASSET"
|
47
|
+
}
|
48
|
+
interface OxygenError {
|
49
|
+
message: string;
|
50
|
+
}
|
51
|
+
|
52
|
+
export { Build, ClientError, DeployConfig, DeploymentManifestFile, DeploymentToken, EnvironmentInput, FileType, OxygenError };
|
@@ -0,0 +1,156 @@
|
|
1
|
+
import { formData, fetch } from '@shopify/cli-kit/node/http';
|
2
|
+
import { createFileReadStream } from '@shopify/cli-kit/node/fs';
|
3
|
+
import { outputInfo, outputCompleted } from '@shopify/cli-kit/node/output';
|
4
|
+
import { joinPath } from '@shopify/cli-kit/node/path';
|
5
|
+
import { mapLimit } from 'async';
|
6
|
+
import { deployDefaults } from '../utils/utils.js';
|
7
|
+
|
8
|
+
async function uploadFiles(config, targets) {
|
9
|
+
outputInfo(`Uploading ${targets.length} files...`);
|
10
|
+
return mapLimit(targets, 6, async (target) => {
|
11
|
+
await uploadFile(config, target);
|
12
|
+
}).then(() => {
|
13
|
+
outputCompleted(`Files uploaded successfully`);
|
14
|
+
});
|
15
|
+
}
|
16
|
+
async function uploadFile(config, target) {
|
17
|
+
const localFolderPath = target.fileType === "WORKER" ? joinPath(config.rootPath, config.workerDir) : joinPath(config.rootPath, config.assetsDir);
|
18
|
+
if (target.parameters !== null && target.parameters.length > 0) {
|
19
|
+
const form = formData();
|
20
|
+
target.parameters.forEach((param) => {
|
21
|
+
form.append(param.name, param.value);
|
22
|
+
});
|
23
|
+
form.append(
|
24
|
+
"file",
|
25
|
+
createFileReadStream(joinPath(localFolderPath, target.filePath))
|
26
|
+
);
|
27
|
+
await formUpload(form, target);
|
28
|
+
} else {
|
29
|
+
const initData = await initiateResumableUpload(target);
|
30
|
+
await performResumableUpload(
|
31
|
+
joinPath(localFolderPath, target.filePath),
|
32
|
+
initData
|
33
|
+
);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
async function formUpload(form, target, attemptNumber = 0) {
|
37
|
+
try {
|
38
|
+
const timeoutDuration = 6e4;
|
39
|
+
const controller = new AbortController();
|
40
|
+
const timeout = setTimeout(() => {
|
41
|
+
controller.abort();
|
42
|
+
}, timeoutDuration);
|
43
|
+
const response = await fetch(target.uploadUrl, {
|
44
|
+
method: "POST",
|
45
|
+
body: form,
|
46
|
+
signal: controller.signal,
|
47
|
+
headers: {
|
48
|
+
Connection: "keep-alive"
|
49
|
+
}
|
50
|
+
});
|
51
|
+
clearTimeout(timeout);
|
52
|
+
if (!response.ok) {
|
53
|
+
throw new Error(`${response.status}`);
|
54
|
+
}
|
55
|
+
} catch (err) {
|
56
|
+
if (isErrorCode(err, "ENOENT")) {
|
57
|
+
throw new Error(`File not found: ${target.filePath}`);
|
58
|
+
}
|
59
|
+
if (attemptNumber < Number(deployDefaults.maxUploadAttempts)) {
|
60
|
+
await formUpload(form, target, attemptNumber + 1);
|
61
|
+
} else {
|
62
|
+
if (err instanceof Error && err.name === "AbortError") {
|
63
|
+
throw new Error(`Request timeout whilst uploading ${target.filePath}`);
|
64
|
+
}
|
65
|
+
throw new Error(`Failed to upload file ${target.filePath}`);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
async function initiateResumableUpload(target) {
|
70
|
+
return fetch(target.uploadUrl, {
|
71
|
+
method: "POST",
|
72
|
+
headers: {
|
73
|
+
"x-goog-resumable": "start",
|
74
|
+
"X-Goog-Content-Length-Range": `0,${target.fileSize}`,
|
75
|
+
"User-Agent": "oxygen-cli"
|
76
|
+
}
|
77
|
+
}).then((res) => {
|
78
|
+
return {
|
79
|
+
sessionUri: res.headers.get("x-guploader-uploadid"),
|
80
|
+
location: res.headers.get("location"),
|
81
|
+
target
|
82
|
+
};
|
83
|
+
}).catch((err) => {
|
84
|
+
throw new Error(
|
85
|
+
`Failed to initiate resumable upload for file ${target.filePath} (status code ${err.statusCode})`
|
86
|
+
);
|
87
|
+
});
|
88
|
+
}
|
89
|
+
async function performResumableUpload(localFilePath, initData, startByte = 0, attemptNumber = 0) {
|
90
|
+
await uploadResumable(initData.location, localFilePath, startByte).catch(
|
91
|
+
async (err) => {
|
92
|
+
if (isErrorCode(err, "ENOENT")) {
|
93
|
+
throw new Error(`File not found: ${initData.target.filePath}`);
|
94
|
+
}
|
95
|
+
if (err && attemptNumber >= Number(deployDefaults.maxResumabeUploadAttempts)) {
|
96
|
+
throw new Error(
|
97
|
+
`Failed to upload file ${initData.target.filePath} after ${deployDefaults.maxResumabeUploadAttempts} attempts`
|
98
|
+
);
|
99
|
+
}
|
100
|
+
const status = await resumableUploadStatus(
|
101
|
+
initData.location,
|
102
|
+
initData.target.fileSize
|
103
|
+
);
|
104
|
+
if (!status.complete) {
|
105
|
+
const nextAttemptStartByte = status.lastReceivedByte;
|
106
|
+
const attempt = attemptNumber + 1;
|
107
|
+
await performResumableUpload(
|
108
|
+
localFilePath,
|
109
|
+
initData,
|
110
|
+
nextAttemptStartByte,
|
111
|
+
attempt
|
112
|
+
);
|
113
|
+
}
|
114
|
+
}
|
115
|
+
);
|
116
|
+
}
|
117
|
+
async function uploadResumable(location, filePath, lastReceivedByte) {
|
118
|
+
const file = createFileReadStream(filePath, { start: lastReceivedByte });
|
119
|
+
return fetch(location, {
|
120
|
+
method: "PUT",
|
121
|
+
body: file
|
122
|
+
}).then((res) => {
|
123
|
+
return res.status;
|
124
|
+
});
|
125
|
+
}
|
126
|
+
async function resumableUploadStatus(location, fileSize) {
|
127
|
+
const getLastByte = (range) => {
|
128
|
+
if (!range || range.split("-").length !== 2)
|
129
|
+
return 0;
|
130
|
+
const rangeParts = range.split("-");
|
131
|
+
return parseInt(rangeParts[1], 10);
|
132
|
+
};
|
133
|
+
return fetch(location, {
|
134
|
+
method: "PUT",
|
135
|
+
headers: {
|
136
|
+
"Content-Length": "0",
|
137
|
+
"Content-Range": `bytes */${fileSize}`
|
138
|
+
}
|
139
|
+
}).then((res) => {
|
140
|
+
return {
|
141
|
+
complete: res.status === 200,
|
142
|
+
lastReceivedByte: getLastByte(res.headers.get("range"))
|
143
|
+
};
|
144
|
+
}).catch((err) => {
|
145
|
+
console.error(err);
|
146
|
+
return {
|
147
|
+
complete: false,
|
148
|
+
lastReceivedByte: 0
|
149
|
+
};
|
150
|
+
});
|
151
|
+
}
|
152
|
+
function isErrorCode(err, code) {
|
153
|
+
return err instanceof Error && "code" in err && err.code === code;
|
154
|
+
}
|
155
|
+
|
156
|
+
export { uploadFiles };
|