@useupup/core 3.3.0
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/LICENSE +21 -0
- package/NOTICE +8 -0
- package/README.md +75 -0
- package/dist/browser-SXR426OQ.cjs +10 -0
- package/dist/browser-WD6T5ABJ.js +1 -0
- package/dist/chunk-2H5ICKFT.cjs +122 -0
- package/dist/chunk-6HF3I2JS.cjs +39 -0
- package/dist/chunk-6S5DQDQH.js +212 -0
- package/dist/chunk-BMQC27OE.cjs +92 -0
- package/dist/chunk-BSGQGINR.cjs +219 -0
- package/dist/chunk-CPKZ2CXD.cjs +179 -0
- package/dist/chunk-DTKDIMKT.cjs +573 -0
- package/dist/chunk-DYE5XJ6Z.cjs +2134 -0
- package/dist/chunk-E3ZCNOXG.cjs +74 -0
- package/dist/chunk-FEQY2WYY.cjs +2377 -0
- package/dist/chunk-FMURBCZ7.js +120 -0
- package/dist/chunk-FYQQRULL.js +37 -0
- package/dist/chunk-GE4LIVXP.js +133 -0
- package/dist/chunk-HFR5HZJZ.js +56 -0
- package/dist/chunk-I7ULPCA2.js +72 -0
- package/dist/chunk-MHYXDLZR.js +569 -0
- package/dist/chunk-N2SLHVAW.cjs +61 -0
- package/dist/chunk-OOPDEOTM.js +169 -0
- package/dist/chunk-PMBR3OUW.js +2359 -0
- package/dist/chunk-QK7HZIVY.cjs +144 -0
- package/dist/chunk-T2AQHY7V.cjs +59 -0
- package/dist/chunk-TRCP5FUR.js +90 -0
- package/dist/chunk-U7KL5M2H.cjs +374 -0
- package/dist/chunk-UXNPM54W.js +2108 -0
- package/dist/chunk-VV6QCQLL.js +365 -0
- package/dist/chunk-ZMS2HXLB.js +56 -0
- package/dist/context-shapes-123dd3b6.d.cts +207 -0
- package/dist/context-shapes-27b24c8f.d.ts +207 -0
- package/dist/contracts-pipeline-cf177270.d.cts +78 -0
- package/dist/contracts-pipeline-cf177270.d.ts +78 -0
- package/dist/contracts-strategies-378a16b7.d.cts +168 -0
- package/dist/contracts-strategies-378a16b7.d.ts +168 -0
- package/dist/contracts.cjs +181 -0
- package/dist/contracts.d.cts +11 -0
- package/dist/contracts.d.ts +11 -0
- package/dist/contracts.js +4 -0
- package/dist/core-141335ac.d.ts +651 -0
- package/dist/core-bea2ceca.d.cts +651 -0
- package/dist/create-worker-provider-AA5SRII6.js +58 -0
- package/dist/create-worker-provider-KJNUSSTW.cjs +60 -0
- package/dist/eligibility-ONLNJ6FB.cjs +10 -0
- package/dist/eligibility-ULFC3E4G.js +8 -0
- package/dist/error-code-map-9c44a425.d.ts +62 -0
- package/dist/error-code-map-dd5becfa.d.cts +62 -0
- package/dist/i18n/index.cjs +90 -0
- package/dist/i18n/index.d.cts +3 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +1 -0
- package/dist/index.cjs +1770 -0
- package/dist/index.d.cts +280 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +1589 -0
- package/dist/internal.cjs +2407 -0
- package/dist/internal.d.cts +595 -0
- package/dist/internal.d.ts +595 -0
- package/dist/internal.js +2275 -0
- package/dist/pipeline/index.cjs +10 -0
- package/dist/pipeline/index.d.cts +11 -0
- package/dist/pipeline/index.d.ts +11 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline-worker.cjs +199 -0
- package/dist/pipeline-worker.d.cts +2 -0
- package/dist/pipeline-worker.d.ts +2 -0
- package/dist/pipeline-worker.js +197 -0
- package/dist/resolve-locale-81feef21.d.ts +20 -0
- package/dist/resolve-locale-9cb63600.d.cts +20 -0
- package/dist/resolve-theme-01758105.d.ts +53 -0
- package/dist/resolve-theme-66cf8d98.d.cts +53 -0
- package/dist/server-mode-drive-controller-b5f8ee12.d.cts +180 -0
- package/dist/server-mode-drive-controller-b5f8ee12.d.ts +180 -0
- package/dist/steps/compress.cjs +66 -0
- package/dist/steps/compress.d.cts +10 -0
- package/dist/steps/compress.d.ts +10 -0
- package/dist/steps/compress.js +64 -0
- package/dist/steps/exif.cjs +44 -0
- package/dist/steps/exif.d.cts +5 -0
- package/dist/steps/exif.d.ts +5 -0
- package/dist/steps/exif.js +42 -0
- package/dist/steps/hash.cjs +44 -0
- package/dist/steps/hash.d.cts +5 -0
- package/dist/steps/hash.d.ts +5 -0
- package/dist/steps/hash.js +42 -0
- package/dist/steps/heic.cjs +59 -0
- package/dist/steps/heic.d.cts +5 -0
- package/dist/steps/heic.d.ts +5 -0
- package/dist/steps/heic.js +57 -0
- package/dist/steps/thumbnail.cjs +54 -0
- package/dist/steps/thumbnail.d.cts +10 -0
- package/dist/steps/thumbnail.d.ts +10 -0
- package/dist/steps/thumbnail.js +52 -0
- package/dist/strategies/index.cjs +82 -0
- package/dist/strategies/index.d.cts +187 -0
- package/dist/strategies/index.d.ts +187 -0
- package/dist/strategies/index.js +60 -0
- package/dist/strategies/tus-upload.cjs +14 -0
- package/dist/strategies/tus-upload.d.cts +15 -0
- package/dist/strategies/tus-upload.d.ts +15 -0
- package/dist/strategies/tus-upload.js +5 -0
- package/dist/theme/index.cjs +38 -0
- package/dist/theme/index.d.cts +3 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.js +1 -0
- package/dist/theme-store-2e56a674.d.cts +39 -0
- package/dist/theme-store-f13e0315.d.ts +39 -0
- package/dist/token-endpoint-5fdb4a35.d.ts +20 -0
- package/dist/token-endpoint-a7d15ab6.d.cts +20 -0
- package/dist/ui-translations-c5d00392.d.cts +455 -0
- package/dist/ui-translations-c5d00392.d.ts +455 -0
- package/dist/uploader-props-72242002.d.ts +340 -0
- package/dist/uploader-props-7f956e60.d.cts +340 -0
- package/dist/vars-d3421f6c.d.cts +240 -0
- package/dist/vars-d3421f6c.d.ts +240 -0
- package/package.json +182 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { fileFingerprint, loadSession, removeSession, updateSessionToken, updateSessionProgress, saveSession } from './chunk-OOPDEOTM.js';
|
|
2
|
+
import { uploadErrorFromResponse, UpupConfigError, UpupNetworkError, UpupError, UpupStorageError } from './chunk-GE4LIVXP.js';
|
|
3
|
+
|
|
4
|
+
// src/strategies/server-credentials.ts
|
|
5
|
+
function operationForPath(path) {
|
|
6
|
+
if (path.startsWith("/presign")) return "presign";
|
|
7
|
+
if (path.startsWith("/multipart/init")) return "multipart-init";
|
|
8
|
+
if (path.startsWith("/multipart/sign-part")) return "multipart-sign-part";
|
|
9
|
+
if (path.startsWith("/multipart/complete")) return "multipart-complete";
|
|
10
|
+
if (path.startsWith("/multipart/abort")) return "multipart-abort";
|
|
11
|
+
if (path.startsWith("/multipart/resume")) return "multipart-resume";
|
|
12
|
+
return "upload";
|
|
13
|
+
}
|
|
14
|
+
var ServerCredentials = class {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.serverUrl = options.serverUrl.replace(/\/$/, "");
|
|
17
|
+
this.headers = {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
...options.headers
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async post(path, body) {
|
|
23
|
+
const response = await fetch(`${this.serverUrl}${path}`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: this.headers,
|
|
26
|
+
body: JSON.stringify(body)
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const responseBody = await response.text().catch(() => "");
|
|
30
|
+
const err = uploadErrorFromResponse({
|
|
31
|
+
status: response.status,
|
|
32
|
+
statusText: response.statusText,
|
|
33
|
+
body: responseBody,
|
|
34
|
+
kind: "storage",
|
|
35
|
+
operation: operationForPath(path)
|
|
36
|
+
});
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
async getPresignedUrl(file) {
|
|
42
|
+
return this.post("/presign", {
|
|
43
|
+
name: file.name,
|
|
44
|
+
size: file.size,
|
|
45
|
+
type: file.type,
|
|
46
|
+
metadata: file.metadata ?? {}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async initMultipartUpload(file) {
|
|
50
|
+
return this.post("/multipart/init", {
|
|
51
|
+
name: file.name,
|
|
52
|
+
size: file.size,
|
|
53
|
+
type: file.type,
|
|
54
|
+
metadata: file.metadata ?? {}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async signPart(params) {
|
|
58
|
+
return this.post(
|
|
59
|
+
"/multipart/sign-part",
|
|
60
|
+
params
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
async completeMultipartUpload(params) {
|
|
64
|
+
return this.post(
|
|
65
|
+
"/multipart/complete",
|
|
66
|
+
params
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
async abortMultipartUpload(params) {
|
|
70
|
+
await this.post("/multipart/abort", params);
|
|
71
|
+
}
|
|
72
|
+
/** Re-attach to an upload the client left in flight (page reload, pause,
|
|
73
|
+
* retry) or whose token aged past its 1h TTL. The presented token is the
|
|
74
|
+
* ONLY input — key/uploadId are never sent by the client — and the
|
|
75
|
+
* response carries a freshly-signed replacement plus the parts storage
|
|
76
|
+
* already holds, each with its byte size. */
|
|
77
|
+
async resumeMultipartUpload(params) {
|
|
78
|
+
return this.post("/multipart/resume", params);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/strategies/multipart-upload.ts
|
|
83
|
+
var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
84
|
+
var DEFAULT_MAX_CONCURRENT = 3;
|
|
85
|
+
var DEFAULT_RETRY_DELAYS = [0, 1e3, 3e3, 5e3];
|
|
86
|
+
var DEFAULT_PART_TIMEOUT_MS = 18e4;
|
|
87
|
+
var MATERIALIZE_PART_MAX_BYTES = 16 * 1024 * 1024;
|
|
88
|
+
var MAX_PARTS = 1e4;
|
|
89
|
+
function isAborted(signal) {
|
|
90
|
+
return signal.aborted;
|
|
91
|
+
}
|
|
92
|
+
function isClientError(error) {
|
|
93
|
+
const status = error instanceof UpupError ? error.status : void 0;
|
|
94
|
+
return status !== void 0 && status >= 400 && status < 500;
|
|
95
|
+
}
|
|
96
|
+
function isExpiredTokenError(error) {
|
|
97
|
+
return error instanceof UpupError && error.status === 403 && error.code === "expired";
|
|
98
|
+
}
|
|
99
|
+
function isUnresumableFailure(error) {
|
|
100
|
+
return error instanceof UpupStorageError && (error.operation === "multipart-init" || error.operation === "multipart-complete") && isClientError(error);
|
|
101
|
+
}
|
|
102
|
+
function partStallError(partNumber, timeoutMs) {
|
|
103
|
+
return new UpupError(
|
|
104
|
+
`Part ${partNumber} stalled: no response within ${timeoutMs}ms`,
|
|
105
|
+
"TIMEOUT" /* TIMEOUT */,
|
|
106
|
+
true
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
function isRetryablePartFailure(error) {
|
|
110
|
+
if (error instanceof UpupError) {
|
|
111
|
+
return error.retryable || error.status === 429 || error.status !== void 0 && error.status >= 500;
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return error instanceof TypeError;
|
|
117
|
+
}
|
|
118
|
+
function abortableDelay(ms, signal) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const wake = () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
signal.removeEventListener("abort", wake);
|
|
123
|
+
resolve();
|
|
124
|
+
};
|
|
125
|
+
const timer = setTimeout(wake, ms);
|
|
126
|
+
signal.addEventListener("abort", wake, { once: true });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function readContentHash(file) {
|
|
130
|
+
const metadata = file.metadata;
|
|
131
|
+
return typeof metadata?.originalContentHash === "string" ? metadata.originalContentHash : void 0;
|
|
132
|
+
}
|
|
133
|
+
function abortPersistedMultipartSessions(files, credentials) {
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
if (!(file instanceof File)) continue;
|
|
136
|
+
try {
|
|
137
|
+
const fingerprint = fileFingerprint(file);
|
|
138
|
+
const session = loadSession(fingerprint);
|
|
139
|
+
if (!session) continue;
|
|
140
|
+
removeSession(fingerprint);
|
|
141
|
+
void credentials.abortMultipartUpload?.({ token: session.token }).catch(() => {
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var MultipartUpload = class {
|
|
148
|
+
constructor(options) {
|
|
149
|
+
const { credentials } = options;
|
|
150
|
+
if (!credentials.initMultipartUpload || !credentials.signPart || !credentials.completeMultipartUpload) {
|
|
151
|
+
throw new UpupConfigError(
|
|
152
|
+
"CredentialStrategy must implement multipart methods (initMultipartUpload, signPart, completeMultipartUpload)"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
this.credentials = credentials;
|
|
156
|
+
this.initMultipartUpload = credentials.initMultipartUpload.bind(credentials);
|
|
157
|
+
this.signPart = credentials.signPart.bind(credentials);
|
|
158
|
+
this.completeMultipartUpload = credentials.completeMultipartUpload.bind(credentials);
|
|
159
|
+
this.resumeMultipartUpload = credentials.resumeMultipartUpload?.bind(credentials);
|
|
160
|
+
this.chunkSizeBytes = options.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE;
|
|
161
|
+
const requestedConcurrency = Math.floor(
|
|
162
|
+
options.maxConcurrentParts ?? DEFAULT_MAX_CONCURRENT
|
|
163
|
+
);
|
|
164
|
+
this.maxConcurrentParts = Number.isFinite(requestedConcurrency) ? Math.max(1, requestedConcurrency) : DEFAULT_MAX_CONCURRENT;
|
|
165
|
+
this.persist = options.persist ?? false;
|
|
166
|
+
this.sessionScope = options.sessionScope;
|
|
167
|
+
this.retryDelays = options.retryDelays ?? DEFAULT_RETRY_DELAYS;
|
|
168
|
+
this.partTimeoutMs = options.partTimeoutMs ?? DEFAULT_PART_TIMEOUT_MS;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Each stored part must be byte-for-byte where this file's own chunking
|
|
172
|
+
* would put it: exactly `partSize`, except the file's TRUE final part
|
|
173
|
+
* (`partNumber === totalParts`), which must be exactly the remaining tail.
|
|
174
|
+
*
|
|
175
|
+
* The "highest part number we happen to hold" is NOT the exemption — the
|
|
176
|
+
* server may hold a sparse prefix (say parts 1, 2, 5 of 10), and letting
|
|
177
|
+
* part 5 be short would splice a short block into the MIDDLE of the object.
|
|
178
|
+
* S3 only enforces the ≥5 MiB floor on non-final parts, so it would accept
|
|
179
|
+
* the complete call and hand back a silently corrupt object. A part with no
|
|
180
|
+
* reported `size` is unverifiable and therefore equally unusable.
|
|
181
|
+
*/
|
|
182
|
+
partsMatchSession(parts, partSize, fileSize) {
|
|
183
|
+
if (partSize <= 0) return false;
|
|
184
|
+
const totalParts = Math.ceil(fileSize / partSize);
|
|
185
|
+
const finalPartSize = fileSize - (totalParts - 1) * partSize;
|
|
186
|
+
return parts.every((part) => {
|
|
187
|
+
if (typeof part.size !== "number") return false;
|
|
188
|
+
if (part.partNumber < 1 || part.partNumber > totalParts) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return part.partNumber === totalParts ? part.size === finalPartSize : part.size === partSize;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Try to re-attach to the upload a previous session left in flight.
|
|
196
|
+
* Returns `null` for every "just start fresh" outcome — a resume failure
|
|
197
|
+
* must never fail an upload that a fresh init could still serve.
|
|
198
|
+
*/
|
|
199
|
+
async tryResume(fingerprint, fileSize, contentHash) {
|
|
200
|
+
const resume = this.resumeMultipartUpload;
|
|
201
|
+
if (!resume) return null;
|
|
202
|
+
const session = loadSession(fingerprint);
|
|
203
|
+
if (!session) return null;
|
|
204
|
+
if (session.scope !== this.sessionScope) {
|
|
205
|
+
removeSession(fingerprint);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (contentHash !== void 0 && session.contentHash !== void 0 && session.contentHash !== contentHash) {
|
|
209
|
+
removeSession(fingerprint);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
let response;
|
|
213
|
+
try {
|
|
214
|
+
response = await resume({ token: session.token });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (isClientError(error)) removeSession(fingerprint);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const parts = [...response.parts].sort(
|
|
220
|
+
(a, b) => a.partNumber - b.partNumber
|
|
221
|
+
);
|
|
222
|
+
if (!this.partsMatchSession(parts, session.partSize, fileSize)) {
|
|
223
|
+
removeSession(fingerprint);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const uploadedBytes = parts.reduce(
|
|
227
|
+
(sum, part) => sum + (part.size ?? 0),
|
|
228
|
+
0
|
|
229
|
+
);
|
|
230
|
+
updateSessionToken(fingerprint, response.token);
|
|
231
|
+
updateSessionProgress(fingerprint, uploadedBytes);
|
|
232
|
+
return {
|
|
233
|
+
token: response.token,
|
|
234
|
+
partSize: session.partSize,
|
|
235
|
+
// Drop `size`: the complete call ignores it, and the wire payload
|
|
236
|
+
// stays byte-identical to a non-resumed upload's.
|
|
237
|
+
parts: parts.map(({ partNumber, eTag }) => ({ partNumber, eTag })),
|
|
238
|
+
uploadedBytes
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Failure teardown. THE behavior change of cross-reload resume: with
|
|
243
|
+
* `persist` on, an error or abort deliberately leaves the server-side
|
|
244
|
+
* upload alive so the next attempt can resume into it — pause(), a dropped
|
|
245
|
+
* connection, and UploadManager's own retries all re-enter `upload()` and
|
|
246
|
+
* pick the session back up. Without persist this is exactly the historical
|
|
247
|
+
* best-effort abort. Either way, a 4xx from init/complete kills the
|
|
248
|
+
* session: the server has already torn the upload down.
|
|
249
|
+
*/
|
|
250
|
+
async handleUploadFailure(error, token, fingerprint) {
|
|
251
|
+
if (fingerprint && isUnresumableFailure(error)) {
|
|
252
|
+
removeSession(fingerprint);
|
|
253
|
+
}
|
|
254
|
+
if (this.persist && this.resumeMultipartUpload) return;
|
|
255
|
+
await this.credentials.abortMultipartUpload?.({ token }).catch(() => {
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* PUT one signed part over XMLHttpRequest, under an INACTIVITY watchdog.
|
|
260
|
+
*
|
|
261
|
+
* The watchdog resets on every `upload` progress event and fires only after
|
|
262
|
+
* `partTimeoutMs` with no forward motion at all. That distinction is the
|
|
263
|
+
* point: `fetch()` for a PUT resolves only once the whole body is sent, so a
|
|
264
|
+
* flat ceiling on total time aborts a large part on a slow-but-healthy link
|
|
265
|
+
* (a 5 MiB part under ~233 kbit/s crosses the 180s default) even though it
|
|
266
|
+
* is uploading fine. An inactivity timer kills the connection the watchdog
|
|
267
|
+
* was built for — one that has genuinely stopped moving bytes — without
|
|
268
|
+
* penalizing the slow one. Mirrors the XHR PUT in direct-upload.ts.
|
|
269
|
+
*
|
|
270
|
+
* Resolves with the part's ETag. Rejects with: the retryable stall on
|
|
271
|
+
* inactivity (`partStallError`) — covering both the slice READ and the PUT;
|
|
272
|
+
* a retryable `UPLOAD_FAILED` when the slice read itself rejects;
|
|
273
|
+
* `UpupNetworkError('Upload aborted')` when
|
|
274
|
+
* the caller's signal aborts; a retryable `TypeError` on a network-level
|
|
275
|
+
* failure (the shape `fetch` produced, which `isRetryablePartFailure`
|
|
276
|
+
* already classifies); or an `uploadErrorFromResponse` on a non-2xx — its
|
|
277
|
+
* body read synchronously from `xhr.responseText`, so the whole request
|
|
278
|
+
* including the error body stays inside the one abort/inactivity window (the
|
|
279
|
+
* old `await response.text()` ran AFTER the watchdog was cleared and could
|
|
280
|
+
* hang a half-dead connection forever on the body read).
|
|
281
|
+
*/
|
|
282
|
+
async putPart(signed, chunk, partNumber, signal) {
|
|
283
|
+
if (isAborted(signal)) {
|
|
284
|
+
throw new UpupNetworkError("Upload aborted");
|
|
285
|
+
}
|
|
286
|
+
let body = chunk;
|
|
287
|
+
if (chunk.size <= MATERIALIZE_PART_MAX_BYTES) {
|
|
288
|
+
let readTimer;
|
|
289
|
+
let onReadAbort;
|
|
290
|
+
body = await Promise.race([
|
|
291
|
+
chunk.arrayBuffer().catch((cause) => {
|
|
292
|
+
const readErr = new UpupError(
|
|
293
|
+
`Part ${partNumber} source read failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
294
|
+
"UPLOAD_FAILED" /* UPLOAD_FAILED */,
|
|
295
|
+
true
|
|
296
|
+
);
|
|
297
|
+
throw readErr;
|
|
298
|
+
}),
|
|
299
|
+
new Promise((_, reject) => {
|
|
300
|
+
readTimer = setTimeout(() => {
|
|
301
|
+
reject(partStallError(partNumber, this.partTimeoutMs));
|
|
302
|
+
}, this.partTimeoutMs);
|
|
303
|
+
}),
|
|
304
|
+
new Promise((_, reject) => {
|
|
305
|
+
onReadAbort = () => {
|
|
306
|
+
const abortErr = new UpupNetworkError("Upload aborted");
|
|
307
|
+
reject(abortErr);
|
|
308
|
+
};
|
|
309
|
+
signal.addEventListener("abort", onReadAbort, {
|
|
310
|
+
once: true
|
|
311
|
+
});
|
|
312
|
+
})
|
|
313
|
+
]).finally(() => {
|
|
314
|
+
clearTimeout(readTimer);
|
|
315
|
+
if (onReadAbort) {
|
|
316
|
+
signal.removeEventListener("abort", onReadAbort);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (isAborted(signal)) {
|
|
321
|
+
throw new UpupNetworkError("Upload aborted");
|
|
322
|
+
}
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const xhr = new XMLHttpRequest();
|
|
325
|
+
let stalled = false;
|
|
326
|
+
let watchdog;
|
|
327
|
+
const armWatchdog = () => {
|
|
328
|
+
clearTimeout(watchdog);
|
|
329
|
+
watchdog = setTimeout(() => {
|
|
330
|
+
stalled = true;
|
|
331
|
+
xhr.abort();
|
|
332
|
+
}, this.partTimeoutMs);
|
|
333
|
+
};
|
|
334
|
+
const forwardAbort = () => {
|
|
335
|
+
xhr.abort();
|
|
336
|
+
};
|
|
337
|
+
const cleanup = () => {
|
|
338
|
+
clearTimeout(watchdog);
|
|
339
|
+
signal.removeEventListener("abort", forwardAbort);
|
|
340
|
+
};
|
|
341
|
+
xhr.upload.addEventListener("progress", () => {
|
|
342
|
+
armWatchdog();
|
|
343
|
+
});
|
|
344
|
+
xhr.addEventListener("load", () => {
|
|
345
|
+
cleanup();
|
|
346
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
347
|
+
const eTag = xhr.getResponseHeader("ETag") ?? `"part-${partNumber}"`;
|
|
348
|
+
resolve(eTag);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const err = uploadErrorFromResponse({
|
|
352
|
+
status: xhr.status,
|
|
353
|
+
statusText: xhr.statusText,
|
|
354
|
+
body: xhr.responseText,
|
|
355
|
+
kind: "storage",
|
|
356
|
+
operation: "multipart-sign-part"
|
|
357
|
+
});
|
|
358
|
+
reject(err);
|
|
359
|
+
});
|
|
360
|
+
xhr.addEventListener("error", () => {
|
|
361
|
+
cleanup();
|
|
362
|
+
const err = new TypeError("Network error during part upload");
|
|
363
|
+
reject(err);
|
|
364
|
+
});
|
|
365
|
+
xhr.addEventListener("abort", () => {
|
|
366
|
+
cleanup();
|
|
367
|
+
if (stalled && !isAborted(signal)) {
|
|
368
|
+
const stallErr = partStallError(
|
|
369
|
+
partNumber,
|
|
370
|
+
this.partTimeoutMs
|
|
371
|
+
);
|
|
372
|
+
reject(stallErr);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const abortErr = new UpupNetworkError("Upload aborted");
|
|
376
|
+
reject(abortErr);
|
|
377
|
+
});
|
|
378
|
+
signal.addEventListener("abort", forwardAbort, { once: true });
|
|
379
|
+
xhr.open("PUT", signed.uploadUrl);
|
|
380
|
+
for (const [key, value] of Object.entries(
|
|
381
|
+
signed.uploadHeaders ?? {}
|
|
382
|
+
)) {
|
|
383
|
+
xhr.setRequestHeader(key, value);
|
|
384
|
+
}
|
|
385
|
+
armWatchdog();
|
|
386
|
+
xhr.send(body);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async upload(file, _credentials, options) {
|
|
390
|
+
const fileSize = file.size;
|
|
391
|
+
const fileName = file instanceof File ? file.name : "blob";
|
|
392
|
+
const fileType = file.type || "application/octet-stream";
|
|
393
|
+
const fingerprint = this.persist && file instanceof File ? fileFingerprint(file) : null;
|
|
394
|
+
const contentHash = readContentHash(file);
|
|
395
|
+
const resumed = fingerprint ? await this.tryResume(fingerprint, fileSize, contentHash) : null;
|
|
396
|
+
let currentToken;
|
|
397
|
+
let partSize;
|
|
398
|
+
const completedParts = [];
|
|
399
|
+
let totalUploaded = 0;
|
|
400
|
+
if (resumed) {
|
|
401
|
+
currentToken = resumed.token;
|
|
402
|
+
partSize = resumed.partSize;
|
|
403
|
+
completedParts.push(...resumed.parts);
|
|
404
|
+
totalUploaded = resumed.uploadedBytes;
|
|
405
|
+
options.onProgress(totalUploaded, fileSize);
|
|
406
|
+
} else {
|
|
407
|
+
let init;
|
|
408
|
+
try {
|
|
409
|
+
init = await this.initMultipartUpload({
|
|
410
|
+
name: fileName,
|
|
411
|
+
size: fileSize,
|
|
412
|
+
type: fileType
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (fingerprint && isClientError(error)) {
|
|
416
|
+
removeSession(fingerprint);
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
if (!init.token) {
|
|
421
|
+
throw new UpupNetworkError(
|
|
422
|
+
"Multipart init did not return an upload token (server too old or misconfigured)"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
currentToken = init.token;
|
|
426
|
+
partSize = init.partSize || Math.max(this.chunkSizeBytes, Math.ceil(fileSize / MAX_PARTS));
|
|
427
|
+
if (fingerprint) {
|
|
428
|
+
saveSession(fingerprint, {
|
|
429
|
+
token: currentToken,
|
|
430
|
+
key: init.key,
|
|
431
|
+
partSize,
|
|
432
|
+
updatedAt: Date.now(),
|
|
433
|
+
uploadedBytes: 0,
|
|
434
|
+
...this.sessionScope !== void 0 ? { scope: this.sessionScope } : {},
|
|
435
|
+
...contentHash !== void 0 ? { contentHash } : {}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const totalParts = Math.ceil(fileSize / partSize);
|
|
440
|
+
let refreshInFlight = null;
|
|
441
|
+
const refreshToken = async (staleToken) => {
|
|
442
|
+
const resume = this.resumeMultipartUpload;
|
|
443
|
+
if (!resume) return null;
|
|
444
|
+
if (currentToken !== staleToken) return currentToken;
|
|
445
|
+
const pending = refreshInFlight ?? resume({ token: staleToken }).then((response) => {
|
|
446
|
+
currentToken = response.token;
|
|
447
|
+
if (fingerprint) {
|
|
448
|
+
updateSessionToken(fingerprint, response.token);
|
|
449
|
+
}
|
|
450
|
+
return response.token;
|
|
451
|
+
}).catch(() => null).finally(() => {
|
|
452
|
+
refreshInFlight = null;
|
|
453
|
+
});
|
|
454
|
+
refreshInFlight = pending;
|
|
455
|
+
return pending;
|
|
456
|
+
};
|
|
457
|
+
const withTokenRefresh = async (call) => {
|
|
458
|
+
const attemptToken = currentToken;
|
|
459
|
+
try {
|
|
460
|
+
return await call(attemptToken);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (!isExpiredTokenError(error)) throw error;
|
|
463
|
+
const freshToken = await refreshToken(attemptToken);
|
|
464
|
+
if (!freshToken) throw error;
|
|
465
|
+
return call(freshToken);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
const alreadyUploaded = new Set(
|
|
470
|
+
completedParts.map((part) => part.partNumber)
|
|
471
|
+
);
|
|
472
|
+
const partQueue = Array.from(
|
|
473
|
+
{ length: totalParts },
|
|
474
|
+
(_, i) => i + 1
|
|
475
|
+
).filter((partNumber) => !alreadyUploaded.has(partNumber));
|
|
476
|
+
const activeParts = [];
|
|
477
|
+
const attemptPart = async (partNumber) => {
|
|
478
|
+
const start = (partNumber - 1) * partSize;
|
|
479
|
+
const end = Math.min(start + partSize, fileSize);
|
|
480
|
+
const chunk = file.slice(start, end);
|
|
481
|
+
let signTimer;
|
|
482
|
+
const signed = await Promise.race([
|
|
483
|
+
withTokenRefresh(
|
|
484
|
+
(token) => this.signPart({
|
|
485
|
+
token,
|
|
486
|
+
partNumber
|
|
487
|
+
})
|
|
488
|
+
),
|
|
489
|
+
new Promise((_, reject) => {
|
|
490
|
+
signTimer = setTimeout(() => {
|
|
491
|
+
reject(
|
|
492
|
+
partStallError(partNumber, this.partTimeoutMs)
|
|
493
|
+
);
|
|
494
|
+
}, this.partTimeoutMs);
|
|
495
|
+
})
|
|
496
|
+
]).finally(() => {
|
|
497
|
+
clearTimeout(signTimer);
|
|
498
|
+
});
|
|
499
|
+
if (isAborted(options.signal)) {
|
|
500
|
+
throw new UpupNetworkError("Upload aborted");
|
|
501
|
+
}
|
|
502
|
+
const eTag = await this.putPart(
|
|
503
|
+
signed,
|
|
504
|
+
chunk,
|
|
505
|
+
partNumber,
|
|
506
|
+
options.signal
|
|
507
|
+
);
|
|
508
|
+
completedParts.push({ partNumber, eTag });
|
|
509
|
+
totalUploaded += end - start;
|
|
510
|
+
options.onProgress(totalUploaded, fileSize);
|
|
511
|
+
if (fingerprint) {
|
|
512
|
+
updateSessionProgress(fingerprint, totalUploaded);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const uploadPart = async (partNumber) => {
|
|
516
|
+
for (let attempt = 0; ; attempt++) {
|
|
517
|
+
if (isAborted(options.signal)) {
|
|
518
|
+
throw new UpupNetworkError("Upload aborted");
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await attemptPart(partNumber);
|
|
522
|
+
return;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (isAborted(options.signal)) throw error;
|
|
525
|
+
const delay = this.retryDelays[attempt];
|
|
526
|
+
if (delay === void 0 || !isRetryablePartFailure(error)) {
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
await abortableDelay(delay, options.signal);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
for (const partNumber of partQueue) {
|
|
534
|
+
if (options.signal.aborted) break;
|
|
535
|
+
const partPromise = uploadPart(partNumber).then(() => {
|
|
536
|
+
const idx = activeParts.indexOf(partPromise);
|
|
537
|
+
if (idx !== -1) void activeParts.splice(idx, 1);
|
|
538
|
+
});
|
|
539
|
+
activeParts.push(partPromise);
|
|
540
|
+
if (activeParts.length >= this.maxConcurrentParts) {
|
|
541
|
+
await Promise.race(activeParts);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
await Promise.all(activeParts);
|
|
545
|
+
if (isAborted(options.signal)) {
|
|
546
|
+
throw new UpupNetworkError("Upload aborted");
|
|
547
|
+
}
|
|
548
|
+
completedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
549
|
+
const result = await withTokenRefresh(
|
|
550
|
+
(token) => this.completeMultipartUpload({
|
|
551
|
+
token,
|
|
552
|
+
parts: completedParts
|
|
553
|
+
})
|
|
554
|
+
);
|
|
555
|
+
if (fingerprint) removeSession(fingerprint);
|
|
556
|
+
return {
|
|
557
|
+
key: result.key,
|
|
558
|
+
publicUrl: result.publicUrl,
|
|
559
|
+
downloadUrl: result.downloadUrl,
|
|
560
|
+
etag: result.etag
|
|
561
|
+
};
|
|
562
|
+
} catch (error) {
|
|
563
|
+
await this.handleUploadFailure(error, currentToken, fingerprint);
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
export { MultipartUpload, ServerCredentials, abortPersistedMultipartSessions };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/types/file-source.ts
|
|
4
|
+
var FileSource = /* @__PURE__ */ ((FileSource2) => {
|
|
5
|
+
FileSource2["LOCAL"] = "local";
|
|
6
|
+
FileSource2["GOOGLE_DRIVE"] = "googleDrive";
|
|
7
|
+
FileSource2["ONE_DRIVE"] = "oneDrive";
|
|
8
|
+
FileSource2["DROPBOX"] = "dropbox";
|
|
9
|
+
FileSource2["BOX"] = "box";
|
|
10
|
+
FileSource2["URL"] = "url";
|
|
11
|
+
FileSource2["CAMERA"] = "camera";
|
|
12
|
+
FileSource2["MICROPHONE"] = "microphone";
|
|
13
|
+
FileSource2["SCREEN"] = "screen";
|
|
14
|
+
return FileSource2;
|
|
15
|
+
})(FileSource || {});
|
|
16
|
+
|
|
17
|
+
// src/types/storage-provider.ts
|
|
18
|
+
var StorageProvider = /* @__PURE__ */ ((StorageProvider2) => {
|
|
19
|
+
StorageProvider2["AWS"] = "aws";
|
|
20
|
+
StorageProvider2["Azure"] = "azure";
|
|
21
|
+
StorageProvider2["BackBlaze"] = "backblaze";
|
|
22
|
+
StorageProvider2["DigitalOcean"] = "digitalocean";
|
|
23
|
+
StorageProvider2["CloudflareR2"] = "r2";
|
|
24
|
+
StorageProvider2["Wasabi"] = "wasabi";
|
|
25
|
+
StorageProvider2["MinIO"] = "minio";
|
|
26
|
+
StorageProvider2["GCS"] = "gcs";
|
|
27
|
+
StorageProvider2["Supabase"] = "supabase";
|
|
28
|
+
StorageProvider2["Hetzner"] = "hetzner";
|
|
29
|
+
StorageProvider2["Linode"] = "linode";
|
|
30
|
+
StorageProvider2["Vultr"] = "vultr";
|
|
31
|
+
StorageProvider2["UpCloud"] = "upcloud";
|
|
32
|
+
StorageProvider2["Scaleway"] = "scaleway";
|
|
33
|
+
StorageProvider2["OVHcloud"] = "ovhcloud";
|
|
34
|
+
StorageProvider2["Alibaba"] = "alibaba";
|
|
35
|
+
StorageProvider2["Oracle"] = "oracle";
|
|
36
|
+
StorageProvider2["Contabo"] = "contabo";
|
|
37
|
+
StorageProvider2["Storj"] = "storj";
|
|
38
|
+
StorageProvider2["IDrive"] = "idrive";
|
|
39
|
+
StorageProvider2["Ceph"] = "ceph";
|
|
40
|
+
return StorageProvider2;
|
|
41
|
+
})(StorageProvider || {});
|
|
42
|
+
var NON_S3_STORAGE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
43
|
+
"azure" /* Azure */
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// src/types/upload-status.ts
|
|
47
|
+
var UploadStatus = /* @__PURE__ */ ((UploadStatus2) => {
|
|
48
|
+
UploadStatus2["IDLE"] = "IDLE";
|
|
49
|
+
UploadStatus2["PROCESSING"] = "PROCESSING";
|
|
50
|
+
UploadStatus2["READY"] = "READY";
|
|
51
|
+
UploadStatus2["UPLOADING"] = "UPLOADING";
|
|
52
|
+
UploadStatus2["PAUSED"] = "PAUSED";
|
|
53
|
+
UploadStatus2["SUCCESSFUL"] = "SUCCESSFUL";
|
|
54
|
+
UploadStatus2["FAILED"] = "FAILED";
|
|
55
|
+
return UploadStatus2;
|
|
56
|
+
})(UploadStatus || {});
|
|
57
|
+
|
|
58
|
+
exports.FileSource = FileSource;
|
|
59
|
+
exports.NON_S3_STORAGE_PROVIDERS = NON_S3_STORAGE_PROVIDERS;
|
|
60
|
+
exports.StorageProvider = StorageProvider;
|
|
61
|
+
exports.UploadStatus = UploadStatus;
|