@vee3/upload 0.1.1 → 0.2.1
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/package.json +31 -24
- package/src/api.js +43 -34
- package/src/errors.js +51 -0
- package/src/format.js +17 -0
- package/src/index.js +121 -96
- package/src/progress.js +59 -0
- package/src/systemCertificates.js +61 -0
- package/src/uploadFile.js +52 -25
package/package.json
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vee3/upload",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Upload local files to Vee3.",
|
|
5
|
-
"homepage": "https://vee3.io",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"vee3-upload": "./src/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@vee3/upload",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Upload local files to Vee3.",
|
|
5
|
+
"homepage": "https://vee3.io",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vee3-upload": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/vee3io/Vee3.git",
|
|
22
|
+
"directory": "packages/upload"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"vee3",
|
|
26
|
+
"upload",
|
|
27
|
+
"cli",
|
|
28
|
+
"mcp"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|
package/src/api.js
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
1
|
+
import { UploadError, describeNetworkError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_API_BASE_URL = "https://api.vee3.io";
|
|
4
|
+
|
|
5
|
+
async function formatApiError(response) {
|
|
6
|
+
const responseText = await response.text();
|
|
7
|
+
try {
|
|
8
|
+
const payload = JSON.parse(responseText);
|
|
9
|
+
if (payload && typeof payload.error === "object") {
|
|
10
|
+
const requestId = payload.error.request_id ? ` (request_id=${payload.error.request_id})` : "";
|
|
11
|
+
return `${payload.error.code}: ${payload.error.message}${requestId}`;
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
// Fall through to raw response text.
|
|
15
|
+
}
|
|
16
|
+
return responseText || `HTTP ${response.status}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function resolveUpload({ apiBaseUrl, uploadCode, contentType }) {
|
|
20
|
+
const requestUrl = `${apiBaseUrl.replace(/\/$/, "")}/v1/agent-uploads/resolve`;
|
|
21
|
+
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await fetch(requestUrl, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json"
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
upload_code: uploadCode,
|
|
31
|
+
content_type: contentType
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new UploadError(describeNetworkError(error, `the Vee3 API at ${apiBaseUrl}`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new UploadError(await formatApiError(response));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return await response.json();
|
|
43
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class UsageError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "UsageError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class UploadError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "UploadError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TLS_VERIFICATION_CODES = new Set([
|
|
16
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
17
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
18
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
19
|
+
"CERT_HAS_EXPIRED",
|
|
20
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Turns a low-level fetch failure into a message a user can act on. Node's
|
|
25
|
+
* fetch throws a generic "fetch failed" TypeError and hides the real reason in
|
|
26
|
+
* error.cause, so we surface the underlying code here.
|
|
27
|
+
*/
|
|
28
|
+
export function describeNetworkError(error, targetDescription) {
|
|
29
|
+
const cause = error?.cause ?? error;
|
|
30
|
+
const code = cause?.code;
|
|
31
|
+
|
|
32
|
+
if (code === "ECONNREFUSED") {
|
|
33
|
+
return `Could not connect to ${targetDescription}. Make sure it is running and the address is correct.`;
|
|
34
|
+
}
|
|
35
|
+
if (code === "ENOTFOUND") {
|
|
36
|
+
return `Could not find ${targetDescription}. Check the address and your network connection.`;
|
|
37
|
+
}
|
|
38
|
+
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
39
|
+
return `Connection to ${targetDescription} timed out.`;
|
|
40
|
+
}
|
|
41
|
+
if (code && TLS_VERIFICATION_CODES.has(code)) {
|
|
42
|
+
return (
|
|
43
|
+
`Could not verify the TLS certificate for ${targetDescription}. ` +
|
|
44
|
+
"This usually means a proxy or antivirus is inspecting HTTPS traffic. " +
|
|
45
|
+
"Use Node 22.15 or newer, or set NODE_EXTRA_CA_CERTS to your root certificate."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const detail = cause?.message ?? error?.message ?? String(error);
|
|
50
|
+
return `Request to ${targetDescription} failed: ${detail}`;
|
|
51
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
2
|
+
|
|
3
|
+
export function formatBytes(byteCount) {
|
|
4
|
+
if (!Number.isFinite(byteCount) || byteCount <= 0) {
|
|
5
|
+
return "0 B";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let unitIndex = 0;
|
|
9
|
+
let value = byteCount;
|
|
10
|
+
while (value >= 1024 && unitIndex < BYTE_UNITS.length - 1) {
|
|
11
|
+
value /= 1024;
|
|
12
|
+
unitIndex += 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const decimals = unitIndex === 0 ? 0 : 1;
|
|
16
|
+
return `${value.toFixed(decimals)} ${BYTE_UNITS[unitIndex]}`;
|
|
17
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,96 +1,121 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { DEFAULT_API_BASE_URL, resolveUpload } from "./api.js";
|
|
6
|
+
import { detectContentType } from "./detectContentType.js";
|
|
7
|
+
import { UploadError, UsageError } from "./errors.js";
|
|
8
|
+
import { formatBytes } from "./format.js";
|
|
9
|
+
import { ensureSystemCertificateTrust } from "./systemCertificates.js";
|
|
10
|
+
import { uploadFileToSignedUrl } from "./uploadFile.js";
|
|
11
|
+
|
|
12
|
+
ensureSystemCertificateTrust();
|
|
13
|
+
|
|
14
|
+
function printUsage() {
|
|
15
|
+
console.error("Usage: vee3-upload <upload_code> <file_path>");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArguments(argumentsList) {
|
|
19
|
+
const positionalArguments = [];
|
|
20
|
+
let apiBaseUrl = DEFAULT_API_BASE_URL;
|
|
21
|
+
let outputJson = false;
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
24
|
+
const argument = argumentsList[index];
|
|
25
|
+
if (argument === "--api-base-url") {
|
|
26
|
+
const nextArgument = argumentsList[index + 1];
|
|
27
|
+
if (!nextArgument) {
|
|
28
|
+
throw new UsageError("--api-base-url requires a value");
|
|
29
|
+
}
|
|
30
|
+
apiBaseUrl = nextArgument;
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (argument === "--json") {
|
|
35
|
+
outputJson = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
positionalArguments.push(argument);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (positionalArguments.length !== 2) {
|
|
42
|
+
throw new UsageError("Expected upload_code and file_path");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
uploadCode: positionalArguments[0],
|
|
47
|
+
filePath: positionalArguments[1],
|
|
48
|
+
apiBaseUrl,
|
|
49
|
+
outputJson
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readFileStat(filePath) {
|
|
54
|
+
try {
|
|
55
|
+
return await stat(filePath);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error?.code === "ENOENT") {
|
|
58
|
+
throw new UploadError(`File not found: ${filePath}`);
|
|
59
|
+
}
|
|
60
|
+
throw new UploadError(`Could not read file: ${filePath} (${error.message})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
const { uploadCode, filePath, apiBaseUrl, outputJson } = parseArguments(
|
|
66
|
+
process.argv.slice(2)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const fileStat = await readFileStat(filePath);
|
|
70
|
+
if (!fileStat.isFile()) {
|
|
71
|
+
throw new UploadError(`Not a file: ${filePath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const contentType = await detectContentType(filePath);
|
|
75
|
+
|
|
76
|
+
console.error("Resolving upload code...");
|
|
77
|
+
const resolvedUpload = await resolveUpload({
|
|
78
|
+
apiBaseUrl,
|
|
79
|
+
uploadCode,
|
|
80
|
+
contentType
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (fileStat.size > resolvedUpload.max_bytes) {
|
|
84
|
+
throw new UploadError(
|
|
85
|
+
`File is too large: ${formatBytes(fileStat.size)} exceeds the ${formatBytes(
|
|
86
|
+
resolvedUpload.max_bytes
|
|
87
|
+
)} limit.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.error(`Uploading ${basename(filePath)} (${formatBytes(fileStat.size)})...`);
|
|
92
|
+
await uploadFileToSignedUrl({
|
|
93
|
+
uploadUrl: resolvedUpload.upload_url,
|
|
94
|
+
filePath,
|
|
95
|
+
fileSizeBytes: fileStat.size,
|
|
96
|
+
requiredHeaders: resolvedUpload.required_headers
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (outputJson) {
|
|
100
|
+
console.log(
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
upload_id: resolvedUpload.upload_id,
|
|
103
|
+
content_type: contentType,
|
|
104
|
+
size_bytes: fileStat.size
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(resolvedUpload.upload_id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
main().catch((error) => {
|
|
114
|
+
if (error instanceof UsageError) {
|
|
115
|
+
printUsage();
|
|
116
|
+
console.error(error.message);
|
|
117
|
+
} else {
|
|
118
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
119
|
+
}
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
package/src/progress.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { formatBytes } from "./format.js";
|
|
2
|
+
|
|
3
|
+
const TTY_RENDER_INTERVAL_MS = 150;
|
|
4
|
+
const NON_TTY_MILESTONE_STEP = 20;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reports upload progress to stderr so that stdout stays clean for the
|
|
8
|
+
* upload_id. In an interactive terminal it redraws a single line; in
|
|
9
|
+
* non-interactive contexts (such as agent logs) it prints discrete milestone
|
|
10
|
+
* lines to avoid carriage-return spam.
|
|
11
|
+
*/
|
|
12
|
+
export function createUploadProgressReporter(totalBytes) {
|
|
13
|
+
const isInteractive = Boolean(process.stderr.isTTY);
|
|
14
|
+
let lastRenderTime = 0;
|
|
15
|
+
let lastMilestone = 0;
|
|
16
|
+
|
|
17
|
+
function computePercent(uploadedBytes) {
|
|
18
|
+
if (totalBytes <= 0) {
|
|
19
|
+
return 100;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(100, Math.floor((uploadedBytes / totalBytes) * 100));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function report(uploadedBytes) {
|
|
25
|
+
const percent = computePercent(uploadedBytes);
|
|
26
|
+
|
|
27
|
+
if (isInteractive) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const isComplete = uploadedBytes >= totalBytes;
|
|
30
|
+
if (!isComplete && now - lastRenderTime < TTY_RENDER_INTERVAL_MS) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
lastRenderTime = now;
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
`\rUploading ${percent}% (${formatBytes(uploadedBytes)} / ${formatBytes(totalBytes)})`
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const milestone = Math.floor(percent / NON_TTY_MILESTONE_STEP) * NON_TTY_MILESTONE_STEP;
|
|
41
|
+
if (milestone <= lastMilestone || milestone >= 100) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
lastMilestone = milestone;
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`Uploading ${milestone}% (${formatBytes(uploadedBytes)} / ${formatBytes(totalBytes)})\n`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function finish() {
|
|
51
|
+
if (isInteractive) {
|
|
52
|
+
process.stderr.write(`\rUploaded ${formatBytes(totalBytes)} (100%)\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
process.stderr.write(`Uploaded ${formatBytes(totalBytes)} (100%)\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { report, finish };
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const SYSTEM_CA_FLAG = "--use-system-ca";
|
|
4
|
+
const REEXEC_GUARD_VARIABLE = "VEE3_UPLOAD_SYSTEM_CA_REEXEC";
|
|
5
|
+
|
|
6
|
+
function systemCaFlagAlreadyActive() {
|
|
7
|
+
if (process.execArgv.includes(SYSTEM_CA_FLAG)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
11
|
+
return nodeOptions.includes(SYSTEM_CA_FLAG);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function systemCaFlagSupported() {
|
|
15
|
+
try {
|
|
16
|
+
return process.allowedNodeEnvironmentFlags.has(SYSTEM_CA_FLAG);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Networks that perform TLS inspection (corporate proxies, some antivirus
|
|
24
|
+
* products) present certificates signed by a private root CA that Node does not
|
|
25
|
+
* trust by default, so HTTPS requests fail with UNABLE_TO_VERIFY_LEAF_SIGNATURE.
|
|
26
|
+
* Re-executing with --use-system-ca makes Node trust the same roots the
|
|
27
|
+
* operating system already trusts, which is what browsers and OS HTTP clients
|
|
28
|
+
* use. On older Node versions without the flag we fall back to whatever
|
|
29
|
+
* NODE_EXTRA_CA_CERTS provides.
|
|
30
|
+
*/
|
|
31
|
+
export function ensureSystemCertificateTrust() {
|
|
32
|
+
if (process.env[REEXEC_GUARD_VARIABLE] === "1") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (systemCaFlagAlreadyActive()) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!systemCaFlagSupported()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entryScript = process.argv[1];
|
|
43
|
+
if (!entryScript) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = spawnSync(
|
|
48
|
+
process.execPath,
|
|
49
|
+
[SYSTEM_CA_FLAG, entryScript, ...process.argv.slice(2)],
|
|
50
|
+
{
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: { ...process.env, [REEXEC_GUARD_VARIABLE]: "1" }
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (result.error) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
process.exit(result.status ?? 0);
|
|
61
|
+
}
|
package/src/uploadFile.js
CHANGED
|
@@ -1,25 +1,52 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { Transform } from "node:stream";
|
|
3
|
+
import { UploadError, describeNetworkError } from "./errors.js";
|
|
4
|
+
import { createUploadProgressReporter } from "./progress.js";
|
|
5
|
+
|
|
6
|
+
export async function uploadFileToSignedUrl({
|
|
7
|
+
uploadUrl,
|
|
8
|
+
filePath,
|
|
9
|
+
fileSizeBytes,
|
|
10
|
+
requiredHeaders
|
|
11
|
+
}) {
|
|
12
|
+
const progressReporter = createUploadProgressReporter(fileSizeBytes);
|
|
13
|
+
let uploadedBytes = 0;
|
|
14
|
+
|
|
15
|
+
const progressStream = new Transform({
|
|
16
|
+
transform(chunk, _encoding, callback) {
|
|
17
|
+
uploadedBytes += chunk.length;
|
|
18
|
+
progressReporter.report(uploadedBytes);
|
|
19
|
+
callback(null, chunk);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const fileStream = createReadStream(filePath);
|
|
24
|
+
fileStream.on("error", (error) => {
|
|
25
|
+
progressStream.destroy(error);
|
|
26
|
+
});
|
|
27
|
+
fileStream.pipe(progressStream);
|
|
28
|
+
|
|
29
|
+
const headers = {
|
|
30
|
+
...requiredHeaders,
|
|
31
|
+
"Content-Length": String(fileSizeBytes)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetch(uploadUrl, {
|
|
37
|
+
method: "PUT",
|
|
38
|
+
headers,
|
|
39
|
+
body: progressStream,
|
|
40
|
+
duplex: "half"
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new UploadError(describeNetworkError(error, "Vee3 storage"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const responseText = (await response.text()).trim();
|
|
48
|
+
throw new UploadError(responseText || `Upload failed with HTTP ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
progressReporter.finish();
|
|
52
|
+
}
|