@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,2377 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkQK7HZIVY_cjs = require('./chunk-QK7HZIVY.cjs');
|
|
4
|
+
|
|
5
|
+
// src/events.ts
|
|
6
|
+
var EventEmitter = class {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
9
|
+
}
|
|
10
|
+
on(event, handler) {
|
|
11
|
+
let handlers = this.handlers.get(event);
|
|
12
|
+
if (!handlers) {
|
|
13
|
+
handlers = /* @__PURE__ */ new Set();
|
|
14
|
+
this.handlers.set(event, handlers);
|
|
15
|
+
}
|
|
16
|
+
handlers.add(handler);
|
|
17
|
+
return () => {
|
|
18
|
+
this.off(event, handler);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
off(event, handler) {
|
|
22
|
+
this.handlers.get(event)?.delete(handler);
|
|
23
|
+
}
|
|
24
|
+
emit(event, payload) {
|
|
25
|
+
const handlers = this.handlers.get(event);
|
|
26
|
+
if (handlers) {
|
|
27
|
+
for (const handler of handlers) {
|
|
28
|
+
try {
|
|
29
|
+
handler(payload);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
32
|
+
console.error(
|
|
33
|
+
`[upup] listener for "${event}" threw:`,
|
|
34
|
+
err
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
removeAllListeners(event) {
|
|
42
|
+
if (event) {
|
|
43
|
+
this.handlers.delete(event);
|
|
44
|
+
} else {
|
|
45
|
+
this.handlers.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/plugin.ts
|
|
51
|
+
var PluginManager = class {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.plugins = /* @__PURE__ */ new Map();
|
|
54
|
+
this.extensions = /* @__PURE__ */ new Map();
|
|
55
|
+
}
|
|
56
|
+
register(plugin) {
|
|
57
|
+
if (this.plugins.has(plugin.name)) {
|
|
58
|
+
throw new chunkQK7HZIVY_cjs.UpupConfigError(
|
|
59
|
+
`Plugin "${plugin.name}" is already registered`,
|
|
60
|
+
"PLUGIN_ALREADY_REGISTERED"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
this.plugins.set(plugin.name, plugin);
|
|
64
|
+
}
|
|
65
|
+
registerExtension(name, methods) {
|
|
66
|
+
if (this.extensions.has(name)) {
|
|
67
|
+
throw new chunkQK7HZIVY_cjs.UpupConfigError(
|
|
68
|
+
`Extension "${name}" is already registered`,
|
|
69
|
+
"EXTENSION_ALREADY_REGISTERED"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
this.extensions.set(name, methods);
|
|
73
|
+
}
|
|
74
|
+
getPlugin(name) {
|
|
75
|
+
return this.plugins.get(name);
|
|
76
|
+
}
|
|
77
|
+
getExtension(name) {
|
|
78
|
+
return this.extensions.get(name);
|
|
79
|
+
}
|
|
80
|
+
getExtensions() {
|
|
81
|
+
return Object.fromEntries(this.extensions);
|
|
82
|
+
}
|
|
83
|
+
destroy() {
|
|
84
|
+
this.plugins.clear();
|
|
85
|
+
this.extensions.clear();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/validate-file-restrictions.ts
|
|
90
|
+
function fileSizeInBytes(size) {
|
|
91
|
+
const units = {
|
|
92
|
+
B: 1,
|
|
93
|
+
KB: 1024,
|
|
94
|
+
MB: 1024 ** 2,
|
|
95
|
+
GB: 1024 ** 3
|
|
96
|
+
};
|
|
97
|
+
return size.size * (units[size.unit] ?? 1);
|
|
98
|
+
}
|
|
99
|
+
function matchesAccept(file, accept) {
|
|
100
|
+
const types = accept.split(",").map((t) => t.trim()).filter(Boolean);
|
|
101
|
+
if (types.length === 0 || types.some((type) => type === "*" || type === "*/*")) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return types.some((type) => {
|
|
105
|
+
if (type.endsWith("/*")) {
|
|
106
|
+
return file.type.startsWith(type.replace("/*", "/"));
|
|
107
|
+
}
|
|
108
|
+
if (type.startsWith(".")) {
|
|
109
|
+
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
return file.type === type;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function validateFileRestrictions(file, options) {
|
|
115
|
+
const errors = [];
|
|
116
|
+
if (options.allowedFileTypes && !matchesAccept(file, options.allowedFileTypes)) {
|
|
117
|
+
errors.push({
|
|
118
|
+
code: "TYPE_MISMATCH" /* TYPE_MISMATCH */,
|
|
119
|
+
message: `File type "${file.type}" is not accepted`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (options.maxFileSize) {
|
|
123
|
+
const maxBytes = fileSizeInBytes(options.maxFileSize);
|
|
124
|
+
if (file.size > maxBytes) {
|
|
125
|
+
errors.push({
|
|
126
|
+
code: "FILE_TOO_LARGE" /* FILE_TOO_LARGE */,
|
|
127
|
+
message: `File "${file.name}" exceeds maximum size`
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (options.minFileSize) {
|
|
132
|
+
const minBytes = fileSizeInBytes(options.minFileSize);
|
|
133
|
+
if (file.size < minBytes) {
|
|
134
|
+
errors.push({
|
|
135
|
+
code: "FILE_TOO_SMALL" /* FILE_TOO_SMALL */,
|
|
136
|
+
message: `File "${file.name}" is below minimum size`
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return errors;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/file-manager.ts
|
|
144
|
+
var fileIdCounter = 0;
|
|
145
|
+
function generateFileId() {
|
|
146
|
+
return `upup-${Date.now()}-${++fileIdCounter}`;
|
|
147
|
+
}
|
|
148
|
+
async function computeContentHash(file) {
|
|
149
|
+
const buffer = await file.arrayBuffer();
|
|
150
|
+
const bytes = new Uint8Array(buffer);
|
|
151
|
+
const subtleCrypto = typeof crypto !== "undefined" ? crypto.subtle : void 0;
|
|
152
|
+
if (subtleCrypto) {
|
|
153
|
+
const hashBuffer = await subtleCrypto.digest("SHA-256", buffer);
|
|
154
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
155
|
+
}
|
|
156
|
+
let hash = 2166136261;
|
|
157
|
+
for (const byte of bytes) {
|
|
158
|
+
hash ^= byte;
|
|
159
|
+
hash = Math.imul(hash, 16777619);
|
|
160
|
+
}
|
|
161
|
+
return `fnv1a-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
162
|
+
}
|
|
163
|
+
function storedContentHash(file) {
|
|
164
|
+
const metadata = file.metadata;
|
|
165
|
+
return (typeof metadata?.originalContentHash === "string" ? metadata.originalContentHash : void 0) ?? (typeof metadata?.checksum === "string" ? metadata.checksum : void 0) ?? // eslint-disable-next-line @typescript-eslint/no-deprecated -- v3 removal tracked
|
|
166
|
+
file.checksumSHA256 ?? // eslint-disable-next-line @typescript-eslint/no-deprecated -- v3 removal tracked
|
|
167
|
+
file.fileHash;
|
|
168
|
+
}
|
|
169
|
+
function applyContentHash(file, hash) {
|
|
170
|
+
const fileRecord = file;
|
|
171
|
+
fileRecord.fileHash = hash;
|
|
172
|
+
file.metadata = {
|
|
173
|
+
...file.metadata,
|
|
174
|
+
originalContentHash: hash
|
|
175
|
+
};
|
|
176
|
+
return file;
|
|
177
|
+
}
|
|
178
|
+
function contentHashPatch(file, hash) {
|
|
179
|
+
const patch = {
|
|
180
|
+
fileHash: hash,
|
|
181
|
+
metadata: { ...file.metadata, originalContentHash: hash }
|
|
182
|
+
};
|
|
183
|
+
return patch;
|
|
184
|
+
}
|
|
185
|
+
function nativeToUploadFile(file, source = "local" /* LOCAL */) {
|
|
186
|
+
const url = typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
|
|
187
|
+
const extendedFile = file;
|
|
188
|
+
const relativePath = typeof extendedFile.relativePath === "string" && extendedFile.relativePath.trim() !== "" ? extendedFile.relativePath : typeof extendedFile.webkitRelativePath === "string" && extendedFile.webkitRelativePath.trim() !== "" ? extendedFile.webkitRelativePath : void 0;
|
|
189
|
+
const existingMetadata = extendedFile.metadata && typeof extendedFile.metadata === "object" && !Array.isArray(extendedFile.metadata) ? extendedFile.metadata : {};
|
|
190
|
+
const uploadFile = Object.assign(file, {
|
|
191
|
+
id: generateFileId(),
|
|
192
|
+
source,
|
|
193
|
+
status: "IDLE" /* IDLE */,
|
|
194
|
+
metadata: {
|
|
195
|
+
...existingMetadata,
|
|
196
|
+
...relativePath ? { relativePath } : {}
|
|
197
|
+
},
|
|
198
|
+
...url !== void 0 ? { url } : {}
|
|
199
|
+
});
|
|
200
|
+
if (relativePath) {
|
|
201
|
+
try {
|
|
202
|
+
Object.defineProperty(uploadFile, "relativePath", {
|
|
203
|
+
value: relativePath,
|
|
204
|
+
configurable: true,
|
|
205
|
+
enumerable: false,
|
|
206
|
+
writable: true
|
|
207
|
+
});
|
|
208
|
+
} catch {
|
|
209
|
+
try {
|
|
210
|
+
uploadFile.relativePath = relativePath;
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return uploadFile;
|
|
216
|
+
}
|
|
217
|
+
function revokeObjectUrl(file) {
|
|
218
|
+
if (file?.url && file.url.startsWith("blob:") && typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
219
|
+
URL.revokeObjectURL(file.url);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
var FileManager = class {
|
|
223
|
+
constructor(options) {
|
|
224
|
+
this.files = /* @__PURE__ */ new Map();
|
|
225
|
+
this.options = options;
|
|
226
|
+
}
|
|
227
|
+
updateOptions(options) {
|
|
228
|
+
this.options = {
|
|
229
|
+
...this.options,
|
|
230
|
+
...options
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
getFiles() {
|
|
234
|
+
return new Map(this.files);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Immutable file-status transition: produce a NEW UploadFile carrying the patch,
|
|
238
|
+
* store and return it. FileManager is the sole writer of a file's status/key/metadata
|
|
239
|
+
* (F-144), and the new reference lets the orchestrator's ref-diff projection track the
|
|
240
|
+
* transition (F-145).
|
|
241
|
+
*
|
|
242
|
+
* Constraint: object-spread (`{ ...prev }`) would strip File's blob data and methods —
|
|
243
|
+
* they live in internal slots / on the prototype, not as own-enumerable props — leaving
|
|
244
|
+
* a plain object that breaks `xhr.send(file)` in the direct-upload strategy. So we
|
|
245
|
+
* re-wrap the same backing bytes into a fresh File (a view, not a copy) and copy the
|
|
246
|
+
* UploadFile fields onto it — mirroring `cloneUploadFile` (steps/image-utils.ts). Do
|
|
247
|
+
* NOT "simplify" this back to a spread.
|
|
248
|
+
*/
|
|
249
|
+
updateFile(id, patch) {
|
|
250
|
+
const prev = this.files.get(id);
|
|
251
|
+
if (!prev) return void 0;
|
|
252
|
+
const next = new File([prev], prev.name, {
|
|
253
|
+
type: prev.type,
|
|
254
|
+
lastModified: prev.lastModified
|
|
255
|
+
});
|
|
256
|
+
Object.assign(next, prev, { relativePath: prev.relativePath }, patch);
|
|
257
|
+
const updated = next;
|
|
258
|
+
this.files.set(id, updated);
|
|
259
|
+
return updated;
|
|
260
|
+
}
|
|
261
|
+
validateFile(file) {
|
|
262
|
+
const [violation] = validateFileRestrictions(file, this.options);
|
|
263
|
+
if (violation) {
|
|
264
|
+
throw new chunkQK7HZIVY_cjs.UpupValidationError(
|
|
265
|
+
violation.message,
|
|
266
|
+
violation.code,
|
|
267
|
+
file
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async collectAcceptedFiles(nativeFiles) {
|
|
272
|
+
const accepted = [];
|
|
273
|
+
for (const nativeFile of nativeFiles) {
|
|
274
|
+
let candidate = nativeFile;
|
|
275
|
+
if (this.options.onBeforeFileAdded) {
|
|
276
|
+
const result = await this.options.onBeforeFileAdded(nativeFile);
|
|
277
|
+
if (result === false) continue;
|
|
278
|
+
if (result instanceof File) {
|
|
279
|
+
candidate = result;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.validateFile(candidate);
|
|
283
|
+
accepted.push(candidate);
|
|
284
|
+
}
|
|
285
|
+
return accepted;
|
|
286
|
+
}
|
|
287
|
+
async deduplicateByContent(nativeFiles, existingFiles) {
|
|
288
|
+
if (!this.options.contentDeduplication) {
|
|
289
|
+
return nativeFiles.map((file) => ({ file }));
|
|
290
|
+
}
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
for (const existing of existingFiles) {
|
|
293
|
+
const nativeExisting = existing;
|
|
294
|
+
const hash = storedContentHash(existing) ?? (typeof nativeExisting.arrayBuffer === "function" ? await computeContentHash(nativeExisting) : void 0);
|
|
295
|
+
if (hash) {
|
|
296
|
+
if (storedContentHash(existing) !== hash) {
|
|
297
|
+
this.updateFile(
|
|
298
|
+
existing.id,
|
|
299
|
+
contentHashPatch(existing, hash)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
seen.add(hash);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const accepted = [];
|
|
306
|
+
for (const file of nativeFiles) {
|
|
307
|
+
const hash = await computeContentHash(file);
|
|
308
|
+
if (seen.has(hash)) continue;
|
|
309
|
+
seen.add(hash);
|
|
310
|
+
accepted.push({ file, hash });
|
|
311
|
+
}
|
|
312
|
+
return accepted;
|
|
313
|
+
}
|
|
314
|
+
assertBatchFits(files, existingFiles, fallbackFile) {
|
|
315
|
+
if (files.length === 0) return;
|
|
316
|
+
const violatingFile = files[0] ?? fallbackFile;
|
|
317
|
+
if (!violatingFile) {
|
|
318
|
+
throw new chunkQK7HZIVY_cjs.UpupError(
|
|
319
|
+
"assertBatchFits: no file available to attach to the violation error",
|
|
320
|
+
"BAD_REQUEST" /* BAD_REQUEST */
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const existing = [...existingFiles];
|
|
324
|
+
if (this.options.limit) {
|
|
325
|
+
const remainingSlots = this.options.limit - existing.length;
|
|
326
|
+
if (remainingSlots <= 0 || files.length > remainingSlots) {
|
|
327
|
+
throw new chunkQK7HZIVY_cjs.UpupValidationError(
|
|
328
|
+
`Adding ${files.length} files would exceed the limit of ${this.options.limit}`,
|
|
329
|
+
"LIMIT_EXCEEDED" /* LIMIT_EXCEEDED */,
|
|
330
|
+
violatingFile
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (this.options.maxTotalFileSize) {
|
|
335
|
+
const maxTotal = fileSizeInBytes(this.options.maxTotalFileSize);
|
|
336
|
+
const currentTotal = existing.reduce((sum, f) => sum + f.size, 0);
|
|
337
|
+
const newTotal = files.reduce((sum, f) => sum + f.size, 0);
|
|
338
|
+
if (currentTotal + newTotal > maxTotal) {
|
|
339
|
+
throw new chunkQK7HZIVY_cjs.UpupValidationError(
|
|
340
|
+
"Total file size exceeds maximum",
|
|
341
|
+
"TOTAL_SIZE_EXCEEDED" /* TOTAL_SIZE_EXCEEDED */,
|
|
342
|
+
violatingFile
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async addFiles(nativeFiles) {
|
|
348
|
+
const acceptedNativeFiles = await this.collectAcceptedFiles(nativeFiles);
|
|
349
|
+
const dedupedFiles = await this.deduplicateByContent(
|
|
350
|
+
acceptedNativeFiles,
|
|
351
|
+
this.files.values()
|
|
352
|
+
);
|
|
353
|
+
this.assertBatchFits(
|
|
354
|
+
dedupedFiles.map((item) => item.file),
|
|
355
|
+
this.files.values(),
|
|
356
|
+
nativeFiles[0]
|
|
357
|
+
);
|
|
358
|
+
const accepted = dedupedFiles.map(({ file, hash }) => {
|
|
359
|
+
const uploadFile = nativeToUploadFile(file);
|
|
360
|
+
return hash ? applyContentHash(uploadFile, hash) : uploadFile;
|
|
361
|
+
});
|
|
362
|
+
for (const file of accepted) {
|
|
363
|
+
this.files.set(file.id, file);
|
|
364
|
+
}
|
|
365
|
+
return accepted;
|
|
366
|
+
}
|
|
367
|
+
removeFile(id) {
|
|
368
|
+
const file = this.files.get(id);
|
|
369
|
+
if (file) {
|
|
370
|
+
revokeObjectUrl(file);
|
|
371
|
+
this.files.delete(id);
|
|
372
|
+
}
|
|
373
|
+
return file;
|
|
374
|
+
}
|
|
375
|
+
replaceFile(id, file) {
|
|
376
|
+
const current = this.files.get(id);
|
|
377
|
+
if (!current) {
|
|
378
|
+
throw new chunkQK7HZIVY_cjs.UpupError(
|
|
379
|
+
`replaceFile: unknown file ID "${id}"`,
|
|
380
|
+
"BAD_REQUEST" /* BAD_REQUEST */
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const next = "metadata" in file && "source" in file && "status" in file ? Object.assign(file, { id }) : Object.assign(nativeToUploadFile(file, current.source), {
|
|
384
|
+
id
|
|
385
|
+
});
|
|
386
|
+
if (current.url !== next.url) {
|
|
387
|
+
revokeObjectUrl(current);
|
|
388
|
+
}
|
|
389
|
+
this.files.set(id, next);
|
|
390
|
+
return next;
|
|
391
|
+
}
|
|
392
|
+
removeAll() {
|
|
393
|
+
for (const file of this.files.values()) {
|
|
394
|
+
revokeObjectUrl(file);
|
|
395
|
+
}
|
|
396
|
+
this.files.clear();
|
|
397
|
+
}
|
|
398
|
+
/** Bulk-apply pipeline-produced files (already UploadFile with status/key set). */
|
|
399
|
+
applyProcessed(files) {
|
|
400
|
+
for (const file of files) {
|
|
401
|
+
this.files.set(file.id, file);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** Re-instate a prior snapshot: clear and repopulate from entries. */
|
|
405
|
+
restore(entries) {
|
|
406
|
+
this.files.clear();
|
|
407
|
+
for (const [id, file] of entries) {
|
|
408
|
+
this.files.set(id, file);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async setFiles(nativeFiles) {
|
|
412
|
+
const acceptedNativeFiles = await this.collectAcceptedFiles(nativeFiles);
|
|
413
|
+
const dedupedFiles = await this.deduplicateByContent(
|
|
414
|
+
acceptedNativeFiles,
|
|
415
|
+
[]
|
|
416
|
+
);
|
|
417
|
+
this.assertBatchFits(
|
|
418
|
+
dedupedFiles.map((item) => item.file),
|
|
419
|
+
[],
|
|
420
|
+
nativeFiles[0]
|
|
421
|
+
);
|
|
422
|
+
const accepted = dedupedFiles.map(({ file, hash }) => {
|
|
423
|
+
const uploadFile = nativeToUploadFile(file);
|
|
424
|
+
return hash ? applyContentHash(uploadFile, hash) : uploadFile;
|
|
425
|
+
});
|
|
426
|
+
for (const file of this.files.values()) {
|
|
427
|
+
revokeObjectUrl(file);
|
|
428
|
+
}
|
|
429
|
+
this.files.clear();
|
|
430
|
+
for (const file of accepted) {
|
|
431
|
+
this.files.set(file.id, file);
|
|
432
|
+
}
|
|
433
|
+
return accepted;
|
|
434
|
+
}
|
|
435
|
+
reorderFiles(fileIds) {
|
|
436
|
+
if (fileIds.length !== this.files.size) {
|
|
437
|
+
throw new chunkQK7HZIVY_cjs.UpupError(
|
|
438
|
+
`reorderFiles: expected ${this.files.size} IDs but received ${fileIds.length}`,
|
|
439
|
+
"BAD_REQUEST" /* BAD_REQUEST */
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
443
|
+
for (const id of fileIds) {
|
|
444
|
+
const file = this.files.get(id);
|
|
445
|
+
if (!file) {
|
|
446
|
+
throw new chunkQK7HZIVY_cjs.UpupError(
|
|
447
|
+
`reorderFiles: unknown file ID "${id}"`,
|
|
448
|
+
"BAD_REQUEST" /* BAD_REQUEST */
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
newMap.set(id, file);
|
|
452
|
+
}
|
|
453
|
+
this.files = newMap;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/upload-manager.ts
|
|
458
|
+
var UpupUploadBatchError = class extends Error {
|
|
459
|
+
constructor(message, errors) {
|
|
460
|
+
super(message);
|
|
461
|
+
this.name = "UpupUploadBatchError";
|
|
462
|
+
this.errors = errors;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
var UploadManager = class {
|
|
466
|
+
constructor(options) {
|
|
467
|
+
this.abortController = new AbortController();
|
|
468
|
+
this.options = options;
|
|
469
|
+
}
|
|
470
|
+
async uploadAll(files) {
|
|
471
|
+
const { maxConcurrentUploads } = this.options;
|
|
472
|
+
const results = [];
|
|
473
|
+
const errors = [];
|
|
474
|
+
const queue = [...files];
|
|
475
|
+
let active = 0;
|
|
476
|
+
await new Promise((resolve) => {
|
|
477
|
+
const tryNext = () => {
|
|
478
|
+
if (queue.length === 0 && active === 0) {
|
|
479
|
+
resolve();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
while (active < maxConcurrentUploads && queue.length > 0) {
|
|
483
|
+
const file = queue.shift();
|
|
484
|
+
if (!file) break;
|
|
485
|
+
active++;
|
|
486
|
+
this.uploadFile(file).then((result) => {
|
|
487
|
+
results.push(result);
|
|
488
|
+
this.options.onFileComplete(file, result);
|
|
489
|
+
}).catch((err) => {
|
|
490
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
491
|
+
if (this.options.onFileError) {
|
|
492
|
+
this.options.onFileError(file, error);
|
|
493
|
+
}
|
|
494
|
+
errors.push({ file, error });
|
|
495
|
+
}).finally(() => {
|
|
496
|
+
active--;
|
|
497
|
+
tryNext();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
tryNext();
|
|
502
|
+
if (files.length === 0) {
|
|
503
|
+
resolve();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
if (errors.length > 0) {
|
|
507
|
+
throw new UpupUploadBatchError(
|
|
508
|
+
`${errors.length} file upload(s) failed`,
|
|
509
|
+
errors
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return results;
|
|
513
|
+
}
|
|
514
|
+
// A method (rather than inlining `this.abortController.signal.aborted`,
|
|
515
|
+
// or a getter — TS's control-flow narrowing treats a no-setter accessor
|
|
516
|
+
// like a readonly field) so each call is re-evaluated fresh:
|
|
517
|
+
// `abortController` can be replaced concurrently by `pause()`/`abort()`
|
|
518
|
+
// while an attempt is in flight, across the `await`s below.
|
|
519
|
+
checkAborted() {
|
|
520
|
+
return this.abortController.signal.aborted;
|
|
521
|
+
}
|
|
522
|
+
async uploadFile(file) {
|
|
523
|
+
const {
|
|
524
|
+
credentials,
|
|
525
|
+
uploadStrategy,
|
|
526
|
+
maxRetries = 3,
|
|
527
|
+
isSuccessfulCall
|
|
528
|
+
} = this.options;
|
|
529
|
+
const maxAttempts = maxRetries + 1;
|
|
530
|
+
let lastError = null;
|
|
531
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
532
|
+
if (this.checkAborted()) {
|
|
533
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError("Upload aborted");
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
this.options.onFileStart?.(file);
|
|
537
|
+
const plan = this.options.resolveUploadStrategy?.(file) ?? {
|
|
538
|
+
uploadStrategy,
|
|
539
|
+
presign: true
|
|
540
|
+
};
|
|
541
|
+
const currentMetadata = file.metadata;
|
|
542
|
+
const metadata = {
|
|
543
|
+
...currentMetadata,
|
|
544
|
+
...file.relativePath && currentMetadata.relativePath == null ? { relativePath: file.relativePath } : {}
|
|
545
|
+
};
|
|
546
|
+
const fileMetadata = {
|
|
547
|
+
name: file.name,
|
|
548
|
+
size: file.size,
|
|
549
|
+
type: file.type,
|
|
550
|
+
metadata
|
|
551
|
+
};
|
|
552
|
+
const presigned = plan.presign ? await credentials.getPresignedUrl(fileMetadata) : {
|
|
553
|
+
key: file.id,
|
|
554
|
+
uploadUrl: "",
|
|
555
|
+
expiresIn: 0
|
|
556
|
+
};
|
|
557
|
+
const result = await plan.uploadStrategy.upload(
|
|
558
|
+
file,
|
|
559
|
+
presigned,
|
|
560
|
+
{
|
|
561
|
+
onProgress: (loaded, total) => {
|
|
562
|
+
this.options.onProgress(file.id, loaded, total);
|
|
563
|
+
},
|
|
564
|
+
signal: this.abortController.signal
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
if (isSuccessfulCall) {
|
|
568
|
+
const syntheticResponse = {
|
|
569
|
+
status: 200,
|
|
570
|
+
headers: {},
|
|
571
|
+
body: result
|
|
572
|
+
};
|
|
573
|
+
const ok = await isSuccessfulCall(syntheticResponse);
|
|
574
|
+
if (!ok) {
|
|
575
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
576
|
+
"isSuccessfulCall returned false",
|
|
577
|
+
0
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
} catch (err) {
|
|
583
|
+
if (this.checkAborted()) {
|
|
584
|
+
throw err;
|
|
585
|
+
}
|
|
586
|
+
const isNetwork = err instanceof chunkQK7HZIVY_cjs.UpupNetworkError;
|
|
587
|
+
if (isNetwork && err.status !== void 0 && err.status >= 400 && err.status < 500) {
|
|
588
|
+
throw err;
|
|
589
|
+
}
|
|
590
|
+
if (isNetwork && err.status === 0) {
|
|
591
|
+
throw err;
|
|
592
|
+
}
|
|
593
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
594
|
+
if (attempt < maxAttempts - 1) {
|
|
595
|
+
await new Promise(
|
|
596
|
+
(r) => setTimeout(r, 100 * Math.pow(2, attempt))
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const finalError = lastError ?? new chunkQK7HZIVY_cjs.UpupNetworkError("Upload failed after retries");
|
|
602
|
+
throw finalError;
|
|
603
|
+
}
|
|
604
|
+
abort() {
|
|
605
|
+
this.abortController.abort();
|
|
606
|
+
}
|
|
607
|
+
pause() {
|
|
608
|
+
this.abortController.abort();
|
|
609
|
+
this.abortController = new AbortController();
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/utils/status-helpers.ts
|
|
614
|
+
function isUploadActive(status) {
|
|
615
|
+
return status === "UPLOADING" /* UPLOADING */ || status === "PROCESSING" /* PROCESSING */;
|
|
616
|
+
}
|
|
617
|
+
function isUploadIdle(status) {
|
|
618
|
+
return status === "IDLE" /* IDLE */ || status === "READY" /* READY */;
|
|
619
|
+
}
|
|
620
|
+
function isFileRemovalLocked(progress, fileStatus) {
|
|
621
|
+
return !!progress && fileStatus !== "PAUSED" /* PAUSED */;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/crash-recovery.ts
|
|
625
|
+
var STORAGE_KEY = "upup-crash-recovery";
|
|
626
|
+
var CrashRecoveryManager = class {
|
|
627
|
+
constructor(storage) {
|
|
628
|
+
this.storage = storage;
|
|
629
|
+
}
|
|
630
|
+
async save(snapshot) {
|
|
631
|
+
await this.storage.set(STORAGE_KEY, snapshot);
|
|
632
|
+
}
|
|
633
|
+
async restore() {
|
|
634
|
+
const data = await this.storage.get(STORAGE_KEY);
|
|
635
|
+
return data ?? null;
|
|
636
|
+
}
|
|
637
|
+
async clear() {
|
|
638
|
+
await this.storage.delete(STORAGE_KEY);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
var IndexedDBStorage = class {
|
|
642
|
+
constructor(dbName = "upup-crash-recovery") {
|
|
643
|
+
this.storeName = "upup-store";
|
|
644
|
+
/** ONE cached connection for the storage's lifetime — never closed after
|
|
645
|
+
* an operation. Firefox ties Blob/File handles read from IndexedDB to
|
|
646
|
+
* the connection they were read over: with the old open-per-operation /
|
|
647
|
+
* close-on-complete lifecycle, the File revived by the crash-recovery
|
|
648
|
+
* restore lost its backing store shortly after the restore's transaction
|
|
649
|
+
* closed, and mid-resume slice reads rejected with AbortError ("The
|
|
650
|
+
* operation was aborted") — a Firefox-only reload-resume stall reproduced
|
|
651
|
+
* against both MinIO and Backblaze B2 (Chromium materializes IndexedDB
|
|
652
|
+
* blobs independently of the connection and never failed). The cache
|
|
653
|
+
* still yields: `versionchange` (a deleteDatabase/upgrade elsewhere)
|
|
654
|
+
* closes and drops it so the other context is never blocked, and a
|
|
655
|
+
* browser-initiated `close` drops it so the next operation reopens. */
|
|
656
|
+
this.dbPromise = null;
|
|
657
|
+
this.dbName = dbName;
|
|
658
|
+
}
|
|
659
|
+
getDB() {
|
|
660
|
+
if (this.dbPromise) return this.dbPromise;
|
|
661
|
+
const promise = new Promise((resolve, reject) => {
|
|
662
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
663
|
+
request.onupgradeneeded = () => {
|
|
664
|
+
request.result.createObjectStore(this.storeName);
|
|
665
|
+
};
|
|
666
|
+
request.onsuccess = () => {
|
|
667
|
+
const db = request.result;
|
|
668
|
+
db.onversionchange = () => {
|
|
669
|
+
db.close();
|
|
670
|
+
if (this.dbPromise === promise) {
|
|
671
|
+
this.dbPromise = null;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
db.onclose = () => {
|
|
675
|
+
if (this.dbPromise === promise) {
|
|
676
|
+
this.dbPromise = null;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
resolve(db);
|
|
680
|
+
};
|
|
681
|
+
request.onerror = () => {
|
|
682
|
+
if (this.dbPromise === promise) {
|
|
683
|
+
this.dbPromise = null;
|
|
684
|
+
}
|
|
685
|
+
reject(request.error ?? new Error("IndexedDB open failed"));
|
|
686
|
+
};
|
|
687
|
+
});
|
|
688
|
+
this.dbPromise = promise;
|
|
689
|
+
return promise;
|
|
690
|
+
}
|
|
691
|
+
/** Close and release the cached connection. Safe at any time — the next
|
|
692
|
+
* operation reopens. `UpupCore.destroy()` calls this so an unmounted
|
|
693
|
+
* uploader doesn't keep the database held open (pending transactions
|
|
694
|
+
* still drain first per the IndexedDB spec, so a queued snapshot write
|
|
695
|
+
* is never cut off mid-transaction). */
|
|
696
|
+
close() {
|
|
697
|
+
const promise = this.dbPromise;
|
|
698
|
+
if (!promise) return;
|
|
699
|
+
this.dbPromise = null;
|
|
700
|
+
void promise.then(
|
|
701
|
+
(db) => {
|
|
702
|
+
db.close();
|
|
703
|
+
},
|
|
704
|
+
() => {
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
async get(key) {
|
|
709
|
+
try {
|
|
710
|
+
const db = await this.getDB();
|
|
711
|
+
return await new Promise((resolve, reject) => {
|
|
712
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
713
|
+
const store = tx.objectStore(this.storeName);
|
|
714
|
+
const request = store.get(key);
|
|
715
|
+
request.onsuccess = () => {
|
|
716
|
+
resolve(request.result);
|
|
717
|
+
};
|
|
718
|
+
request.onerror = () => {
|
|
719
|
+
reject(request.error ?? new Error("IndexedDB get failed"));
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
} catch {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async set(key, value) {
|
|
727
|
+
try {
|
|
728
|
+
const db = await this.getDB();
|
|
729
|
+
await new Promise((resolve, reject) => {
|
|
730
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
731
|
+
const store = tx.objectStore(this.storeName);
|
|
732
|
+
const request = store.put(value, key);
|
|
733
|
+
request.onsuccess = () => {
|
|
734
|
+
resolve();
|
|
735
|
+
};
|
|
736
|
+
request.onerror = () => {
|
|
737
|
+
reject(request.error ?? new Error("IndexedDB put failed"));
|
|
738
|
+
};
|
|
739
|
+
});
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async delete(key) {
|
|
744
|
+
try {
|
|
745
|
+
const db = await this.getDB();
|
|
746
|
+
await new Promise((resolve, reject) => {
|
|
747
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
748
|
+
const store = tx.objectStore(this.storeName);
|
|
749
|
+
const request = store.delete(key);
|
|
750
|
+
request.onsuccess = () => {
|
|
751
|
+
resolve();
|
|
752
|
+
};
|
|
753
|
+
request.onerror = () => {
|
|
754
|
+
reject(
|
|
755
|
+
request.error ?? new Error("IndexedDB delete failed")
|
|
756
|
+
);
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// src/drives/pkce.ts
|
|
765
|
+
function generateCodeVerifier(length = 128) {
|
|
766
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
767
|
+
const values = crypto.getRandomValues(new Uint8Array(length));
|
|
768
|
+
return Array.from(values, (b) => charset[b % charset.length]).join("");
|
|
769
|
+
}
|
|
770
|
+
async function generateCodeChallenge(verifier) {
|
|
771
|
+
const data = new TextEncoder().encode(verifier);
|
|
772
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
773
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/drives/session-storage.ts
|
|
777
|
+
function storageGet(key) {
|
|
778
|
+
if (typeof window === "undefined") return null;
|
|
779
|
+
try {
|
|
780
|
+
return sessionStorage.getItem(key);
|
|
781
|
+
} catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function storageSet(key, value) {
|
|
786
|
+
if (typeof window === "undefined") return;
|
|
787
|
+
try {
|
|
788
|
+
sessionStorage.setItem(key, value);
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function storageDel(key) {
|
|
793
|
+
if (typeof window === "undefined") return;
|
|
794
|
+
try {
|
|
795
|
+
sessionStorage.removeItem(key);
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/drives/popup-oauth-plugin.ts
|
|
801
|
+
var PopupOAuthPlugin = class {
|
|
802
|
+
constructor() {
|
|
803
|
+
this.emitter = null;
|
|
804
|
+
this.config = { clientId: "" };
|
|
805
|
+
this.accessToken = null;
|
|
806
|
+
this.refreshTokenValue = null;
|
|
807
|
+
this.tokenExpiry = 0;
|
|
808
|
+
this.state = "idle";
|
|
809
|
+
this.codeVerifier = null;
|
|
810
|
+
// Popup polling references
|
|
811
|
+
this.popupWindow = null;
|
|
812
|
+
this.pollTimer = null;
|
|
813
|
+
}
|
|
814
|
+
get id() {
|
|
815
|
+
return this.spec.id;
|
|
816
|
+
}
|
|
817
|
+
get name() {
|
|
818
|
+
return this.spec.id;
|
|
819
|
+
}
|
|
820
|
+
// ── Plugin lifecycle ──
|
|
821
|
+
configure(config) {
|
|
822
|
+
this.config = config;
|
|
823
|
+
return this;
|
|
824
|
+
}
|
|
825
|
+
getConfig() {
|
|
826
|
+
return this.config;
|
|
827
|
+
}
|
|
828
|
+
init(emitter) {
|
|
829
|
+
this.emitter = emitter;
|
|
830
|
+
}
|
|
831
|
+
destroy() {
|
|
832
|
+
this.cleanupPopup();
|
|
833
|
+
this.emitter = null;
|
|
834
|
+
}
|
|
835
|
+
// ── State management ──
|
|
836
|
+
getState() {
|
|
837
|
+
return this.state;
|
|
838
|
+
}
|
|
839
|
+
setState(newState) {
|
|
840
|
+
this.state = newState;
|
|
841
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:state-change`, {
|
|
842
|
+
state: newState
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
// ── Auth: build the OAuth URL with PKCE ──
|
|
846
|
+
async getAuthUrl() {
|
|
847
|
+
const clientId = this.config.clientId;
|
|
848
|
+
if (!clientId) {
|
|
849
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
850
|
+
`${this.spec.displayName} client_id is not configured`,
|
|
851
|
+
this.spec.displayName
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
const redirectUri = this.getRedirectUri();
|
|
855
|
+
this.codeVerifier = generateCodeVerifier();
|
|
856
|
+
const challenge = await generateCodeChallenge(this.codeVerifier);
|
|
857
|
+
const params = new URLSearchParams({
|
|
858
|
+
client_id: clientId,
|
|
859
|
+
response_type: "code",
|
|
860
|
+
redirect_uri: redirectUri,
|
|
861
|
+
code_challenge: challenge,
|
|
862
|
+
code_challenge_method: "S256",
|
|
863
|
+
...this.spec.authParams ?? {}
|
|
864
|
+
});
|
|
865
|
+
if (this.spec.scopes) {
|
|
866
|
+
params.set("scope", this.spec.scopes);
|
|
867
|
+
}
|
|
868
|
+
return `${this.spec.authUrl}?${params.toString()}`;
|
|
869
|
+
}
|
|
870
|
+
// ── Auth: exchange authorization code for tokens ──
|
|
871
|
+
async authenticate(code) {
|
|
872
|
+
const clientId = this.config.clientId;
|
|
873
|
+
if (!clientId) {
|
|
874
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
875
|
+
`${this.spec.displayName} client_id is not configured`,
|
|
876
|
+
this.spec.displayName
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (!this.codeVerifier) {
|
|
880
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
881
|
+
"No PKCE code verifier \u2014 call getAuthUrl() first",
|
|
882
|
+
this.spec.displayName
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
this.setState("authenticating");
|
|
886
|
+
try {
|
|
887
|
+
const body = new URLSearchParams({
|
|
888
|
+
code,
|
|
889
|
+
client_id: clientId,
|
|
890
|
+
code_verifier: this.codeVerifier,
|
|
891
|
+
grant_type: "authorization_code",
|
|
892
|
+
redirect_uri: this.getRedirectUri()
|
|
893
|
+
});
|
|
894
|
+
if (this.spec.scopes) {
|
|
895
|
+
body.set("scope", this.spec.scopes);
|
|
896
|
+
}
|
|
897
|
+
const res = await fetch(this.spec.tokenUrl, {
|
|
898
|
+
method: "POST",
|
|
899
|
+
headers: {
|
|
900
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
901
|
+
},
|
|
902
|
+
body
|
|
903
|
+
});
|
|
904
|
+
if (!res.ok) {
|
|
905
|
+
const text = await res.text();
|
|
906
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
907
|
+
`Token exchange failed: ${text}`,
|
|
908
|
+
this.spec.displayName
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
const data = await res.json();
|
|
912
|
+
this.setTokens(
|
|
913
|
+
data.access_token,
|
|
914
|
+
data.refresh_token ?? null,
|
|
915
|
+
data.expires_in
|
|
916
|
+
);
|
|
917
|
+
let user;
|
|
918
|
+
try {
|
|
919
|
+
user = await this.fetchUserProfile();
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
this.setState("authenticated");
|
|
923
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:authenticated`, {
|
|
924
|
+
user
|
|
925
|
+
});
|
|
926
|
+
} catch (err) {
|
|
927
|
+
this.setState("idle");
|
|
928
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
|
|
929
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
930
|
+
action: "authenticate"
|
|
931
|
+
});
|
|
932
|
+
throw err;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// ── Auth: popup-based flow (open popup, poll for redirect code) ──
|
|
936
|
+
async authenticateViaPopup() {
|
|
937
|
+
if (typeof window === "undefined") {
|
|
938
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
939
|
+
"authenticateViaPopup requires a browser environment",
|
|
940
|
+
this.spec.displayName
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const url = await this.getAuthUrl();
|
|
944
|
+
this.cleanupPopup();
|
|
945
|
+
const left = (typeof window !== "undefined" ? window.screenX : 0) + 60;
|
|
946
|
+
const top = (typeof window !== "undefined" ? window.screenY : 0) + 60;
|
|
947
|
+
this.popupWindow = window.open(
|
|
948
|
+
url,
|
|
949
|
+
this.spec.popupName,
|
|
950
|
+
`width=800,height=600,left=${left},top=${top},toolbar=0,scrollbars=1,status=1,resizable=1`
|
|
951
|
+
);
|
|
952
|
+
if (!this.popupWindow) {
|
|
953
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
|
|
954
|
+
error: new Error("Popup was blocked by the browser"),
|
|
955
|
+
action: "authenticateViaPopup"
|
|
956
|
+
});
|
|
957
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
958
|
+
"Popup was blocked by the browser",
|
|
959
|
+
this.spec.displayName
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
this.setState("authenticating");
|
|
963
|
+
return new Promise((resolve, reject) => {
|
|
964
|
+
const redirectUri = this.getRedirectUri();
|
|
965
|
+
this.pollTimer = setInterval(() => {
|
|
966
|
+
void (async () => {
|
|
967
|
+
try {
|
|
968
|
+
if (!this.popupWindow || this.popupWindow.closed) {
|
|
969
|
+
this.cleanupPopup();
|
|
970
|
+
this.setState("idle");
|
|
971
|
+
resolve();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
let href;
|
|
975
|
+
try {
|
|
976
|
+
href = this.popupWindow.location.href;
|
|
977
|
+
} catch {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (!href.startsWith(redirectUri) || !href.includes("code=")) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
this.cleanupPollTimer();
|
|
984
|
+
const code = new URL(href).searchParams.get("code");
|
|
985
|
+
this.popupWindow.close();
|
|
986
|
+
this.popupWindow = null;
|
|
987
|
+
if (!code) {
|
|
988
|
+
const err = new Error(
|
|
989
|
+
"No authorization code found in redirect URL"
|
|
990
|
+
);
|
|
991
|
+
this.setState("idle");
|
|
992
|
+
this.emitter?.emit(
|
|
993
|
+
`${this.spec.eventPrefix}:error`,
|
|
994
|
+
{
|
|
995
|
+
error: err,
|
|
996
|
+
action: "authenticateViaPopup"
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
reject(err);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
await this.authenticate(code);
|
|
1003
|
+
resolve();
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1006
|
+
if (message.includes("cross-origin") || message.includes("Cross-Origin") || message.includes("Permission denied") || message.includes("Failed to read")) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
this.cleanupPopup();
|
|
1010
|
+
this.setState("idle");
|
|
1011
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
|
|
1012
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1013
|
+
action: "authenticateViaPopup"
|
|
1014
|
+
});
|
|
1015
|
+
reject(err instanceof Error ? err : new Error(message));
|
|
1016
|
+
}
|
|
1017
|
+
})();
|
|
1018
|
+
}, 500);
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
// ── Auth: refresh access token ──
|
|
1022
|
+
async refreshAccessToken() {
|
|
1023
|
+
const clientId = this.config.clientId;
|
|
1024
|
+
if (!clientId || !this.refreshTokenValue) return null;
|
|
1025
|
+
try {
|
|
1026
|
+
const body = new URLSearchParams({
|
|
1027
|
+
grant_type: "refresh_token",
|
|
1028
|
+
refresh_token: this.refreshTokenValue,
|
|
1029
|
+
client_id: clientId
|
|
1030
|
+
});
|
|
1031
|
+
if (this.spec.scopes) {
|
|
1032
|
+
body.set("scope", this.spec.scopes);
|
|
1033
|
+
}
|
|
1034
|
+
const res = await fetch(this.spec.tokenUrl, {
|
|
1035
|
+
method: "POST",
|
|
1036
|
+
headers: {
|
|
1037
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1038
|
+
},
|
|
1039
|
+
body
|
|
1040
|
+
});
|
|
1041
|
+
if (!res.ok) {
|
|
1042
|
+
const text = await res.text();
|
|
1043
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
1044
|
+
`Token refresh failed: ${text}`,
|
|
1045
|
+
this.spec.displayName
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const data = await res.json();
|
|
1049
|
+
this.setTokens(
|
|
1050
|
+
data.access_token,
|
|
1051
|
+
data.refresh_token ?? this.refreshTokenValue,
|
|
1052
|
+
data.expires_in
|
|
1053
|
+
);
|
|
1054
|
+
return data.access_token;
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:session-expired`, {});
|
|
1057
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
|
|
1058
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1059
|
+
action: "refreshAccessToken"
|
|
1060
|
+
});
|
|
1061
|
+
this.clearTokens();
|
|
1062
|
+
this.setState("session-expired");
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
// ── Auth: sign out ──
|
|
1067
|
+
signOut() {
|
|
1068
|
+
this.clearTokens();
|
|
1069
|
+
this.cleanupPopup();
|
|
1070
|
+
this.setState("idle");
|
|
1071
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:signed-out`, {});
|
|
1072
|
+
}
|
|
1073
|
+
// ── Auth: restore session from sessionStorage ──
|
|
1074
|
+
restoreSession() {
|
|
1075
|
+
const token = storageGet(this.spec.storageKeys.access);
|
|
1076
|
+
const refresh = storageGet(this.spec.storageKeys.refresh);
|
|
1077
|
+
const expiry = storageGet(this.spec.storageKeys.expiry);
|
|
1078
|
+
if (!token) return false;
|
|
1079
|
+
this.accessToken = token;
|
|
1080
|
+
this.refreshTokenValue = refresh;
|
|
1081
|
+
this.tokenExpiry = expiry ? parseInt(expiry, 10) : 0;
|
|
1082
|
+
this.setState("authenticated");
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
// ── Auth helpers ──
|
|
1086
|
+
isAuthenticated() {
|
|
1087
|
+
return this.state === "authenticated" && this.accessToken !== null;
|
|
1088
|
+
}
|
|
1089
|
+
getAccessToken() {
|
|
1090
|
+
return this.accessToken;
|
|
1091
|
+
}
|
|
1092
|
+
// Guarded public entry point the controller drives (F-123): swallow-to-null
|
|
1093
|
+
// instead of throwing, so a failed profile fetch never strands the restore
|
|
1094
|
+
// flow in an unhandled rejection. Requires isAuthenticated() — unlike
|
|
1095
|
+
// authenticate()'s internal call, which fetches before the flag flips.
|
|
1096
|
+
async getUserInfo() {
|
|
1097
|
+
if (!this.isAuthenticated()) return null;
|
|
1098
|
+
try {
|
|
1099
|
+
return await this.fetchUserProfile();
|
|
1100
|
+
} catch {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// ── Private: authenticated API request with proactive + reactive refresh ──
|
|
1105
|
+
async apiRequest(url, options, isRetry = false) {
|
|
1106
|
+
await this.ensureValidToken();
|
|
1107
|
+
const headers = new Headers(options.headers ?? {});
|
|
1108
|
+
headers.set("Authorization", `Bearer ${this.accessToken}`);
|
|
1109
|
+
const res = await fetch(url, { ...options, headers });
|
|
1110
|
+
if (res.ok) return res;
|
|
1111
|
+
const errorText = await res.text();
|
|
1112
|
+
if (res.status === 401 && !isRetry) {
|
|
1113
|
+
if (this.refreshTokenValue) {
|
|
1114
|
+
const newToken = await this.refreshAccessToken();
|
|
1115
|
+
if (newToken) {
|
|
1116
|
+
return this.apiRequest(url, options, true);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
this.emitter?.emit(`${this.spec.eventPrefix}:session-expired`, {});
|
|
1120
|
+
this.clearTokens();
|
|
1121
|
+
this.setState("session-expired");
|
|
1122
|
+
}
|
|
1123
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
1124
|
+
`${this.spec.displayName} API error (${res.status}): ${errorText}`,
|
|
1125
|
+
res.status
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
// ── Private: token management ──
|
|
1129
|
+
setTokens(access, refresh, expiresIn) {
|
|
1130
|
+
this.accessToken = access;
|
|
1131
|
+
this.refreshTokenValue = refresh;
|
|
1132
|
+
this.tokenExpiry = expiresIn ? Date.now() + expiresIn * 1e3 : 0;
|
|
1133
|
+
storageSet(this.spec.storageKeys.access, access);
|
|
1134
|
+
if (refresh) storageSet(this.spec.storageKeys.refresh, refresh);
|
|
1135
|
+
if (this.tokenExpiry) {
|
|
1136
|
+
storageSet(this.spec.storageKeys.expiry, String(this.tokenExpiry));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
clearTokens() {
|
|
1140
|
+
this.accessToken = null;
|
|
1141
|
+
this.refreshTokenValue = null;
|
|
1142
|
+
this.tokenExpiry = 0;
|
|
1143
|
+
storageDel(this.spec.storageKeys.access);
|
|
1144
|
+
storageDel(this.spec.storageKeys.refresh);
|
|
1145
|
+
storageDel(this.spec.storageKeys.expiry);
|
|
1146
|
+
}
|
|
1147
|
+
async ensureValidToken() {
|
|
1148
|
+
if (!this.accessToken) {
|
|
1149
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
1150
|
+
"Not authenticated \u2014 no access token",
|
|
1151
|
+
this.spec.displayName
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (this.tokenExpiry > 0 && Date.now() > this.tokenExpiry - 6e4 && this.refreshTokenValue) {
|
|
1155
|
+
await this.refreshAccessToken();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// ── Private: redirect URI ──
|
|
1159
|
+
getRedirectUri() {
|
|
1160
|
+
if (this.config.redirectUri) {
|
|
1161
|
+
return this.config.redirectUri;
|
|
1162
|
+
}
|
|
1163
|
+
if (typeof window !== "undefined") {
|
|
1164
|
+
return `${window.location.origin}${this.spec.redirectPath}`;
|
|
1165
|
+
}
|
|
1166
|
+
return "";
|
|
1167
|
+
}
|
|
1168
|
+
// ── Private: popup cleanup ──
|
|
1169
|
+
cleanupPopup() {
|
|
1170
|
+
this.cleanupPollTimer();
|
|
1171
|
+
if (this.popupWindow && !this.popupWindow.closed) {
|
|
1172
|
+
this.popupWindow.close();
|
|
1173
|
+
}
|
|
1174
|
+
this.popupWindow = null;
|
|
1175
|
+
}
|
|
1176
|
+
cleanupPollTimer() {
|
|
1177
|
+
if (this.pollTimer) {
|
|
1178
|
+
clearInterval(this.pollTimer);
|
|
1179
|
+
this.pollTimer = null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// src/drives/mime.ts
|
|
1185
|
+
function guessMimeType(name) {
|
|
1186
|
+
if (!name) return "application/octet-stream";
|
|
1187
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
1188
|
+
const map = {
|
|
1189
|
+
jpg: "image/jpeg",
|
|
1190
|
+
jpeg: "image/jpeg",
|
|
1191
|
+
png: "image/png",
|
|
1192
|
+
gif: "image/gif",
|
|
1193
|
+
webp: "image/webp",
|
|
1194
|
+
svg: "image/svg+xml",
|
|
1195
|
+
pdf: "application/pdf",
|
|
1196
|
+
doc: "application/msword",
|
|
1197
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1198
|
+
xls: "application/vnd.ms-excel",
|
|
1199
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1200
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
1201
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1202
|
+
txt: "text/plain",
|
|
1203
|
+
csv: "text/csv",
|
|
1204
|
+
json: "application/json",
|
|
1205
|
+
xml: "application/xml",
|
|
1206
|
+
zip: "application/zip",
|
|
1207
|
+
mp3: "audio/mpeg",
|
|
1208
|
+
mp4: "video/mp4",
|
|
1209
|
+
mov: "video/quicktime",
|
|
1210
|
+
avi: "video/x-msvideo",
|
|
1211
|
+
html: "text/html",
|
|
1212
|
+
css: "text/css",
|
|
1213
|
+
js: "application/javascript",
|
|
1214
|
+
ts: "application/typescript"
|
|
1215
|
+
};
|
|
1216
|
+
return map[ext] ?? "application/octet-stream";
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/drives/dropbox-plugin.ts
|
|
1220
|
+
var AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
|
|
1221
|
+
var TOKEN_URL = "https://api.dropbox.com/oauth2/token";
|
|
1222
|
+
var LIST_FOLDER_URL = "https://api.dropbox.com/2/files/list_folder";
|
|
1223
|
+
var LIST_FOLDER_CONTINUE_URL = "https://api.dropbox.com/2/files/list_folder/continue";
|
|
1224
|
+
var SEARCH_URL = "https://api.dropbox.com/2/files/search_v2";
|
|
1225
|
+
var USER_INFO_URL = "https://api.dropbox.com/2/users/get_current_account";
|
|
1226
|
+
var TEMP_LINK_URL = "https://api.dropbox.com/2/files/get_temporary_link";
|
|
1227
|
+
var OAUTH_SCOPES = "files.metadata.read files.content.read files.content.write account_info.read";
|
|
1228
|
+
function mapEntry(entry) {
|
|
1229
|
+
const tag = entry[".tag"];
|
|
1230
|
+
const isFolder = tag === "folder";
|
|
1231
|
+
return {
|
|
1232
|
+
id: entry.id ?? "",
|
|
1233
|
+
name: entry.name ?? "",
|
|
1234
|
+
path: entry.path_display ?? "",
|
|
1235
|
+
size: isFolder ? 0 : entry.size ?? 0,
|
|
1236
|
+
mimeType: isFolder ? "folder" : guessMimeType(entry.name),
|
|
1237
|
+
isFolder,
|
|
1238
|
+
thumbnail: void 0,
|
|
1239
|
+
modifiedAt: entry.server_modified ?? void 0
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
var DropboxPlugin = class extends PopupOAuthPlugin {
|
|
1243
|
+
constructor() {
|
|
1244
|
+
super(...arguments);
|
|
1245
|
+
this.spec = {
|
|
1246
|
+
id: "dropbox",
|
|
1247
|
+
displayName: "Dropbox",
|
|
1248
|
+
eventPrefix: "dropbox",
|
|
1249
|
+
popupName: "UpupDropboxAuth",
|
|
1250
|
+
authUrl: AUTH_URL,
|
|
1251
|
+
tokenUrl: TOKEN_URL,
|
|
1252
|
+
redirectPath: "/dp_redirect",
|
|
1253
|
+
storageKeys: {
|
|
1254
|
+
access: "upup_dropbox_access_token",
|
|
1255
|
+
refresh: "upup_dropbox_refresh_token",
|
|
1256
|
+
expiry: "upup_dropbox_token_expiry"
|
|
1257
|
+
},
|
|
1258
|
+
scopes: OAUTH_SCOPES,
|
|
1259
|
+
authParams: { token_access_type: "offline" }
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
configure(config) {
|
|
1263
|
+
return super.configure(config);
|
|
1264
|
+
}
|
|
1265
|
+
getConfig() {
|
|
1266
|
+
return this.config;
|
|
1267
|
+
}
|
|
1268
|
+
// ── File operations: list folder ──
|
|
1269
|
+
async loadFiles(path = "") {
|
|
1270
|
+
this.setState("browsing");
|
|
1271
|
+
try {
|
|
1272
|
+
const res = await this.apiRequest(LIST_FOLDER_URL, {
|
|
1273
|
+
method: "POST",
|
|
1274
|
+
headers: { "Content-Type": "application/json" },
|
|
1275
|
+
body: JSON.stringify({
|
|
1276
|
+
path,
|
|
1277
|
+
recursive: false,
|
|
1278
|
+
include_media_info: true,
|
|
1279
|
+
include_deleted: false,
|
|
1280
|
+
include_has_explicit_shared_members: false
|
|
1281
|
+
})
|
|
1282
|
+
});
|
|
1283
|
+
const data = await res.json();
|
|
1284
|
+
const files = (data.entries ?? []).map(mapEntry);
|
|
1285
|
+
this.setState("authenticated");
|
|
1286
|
+
this.emitter?.emit("dropbox:files-loaded", {
|
|
1287
|
+
files,
|
|
1288
|
+
path,
|
|
1289
|
+
hasMore: !!data.has_more,
|
|
1290
|
+
cursor: data.cursor
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
files,
|
|
1294
|
+
hasMore: !!data.has_more,
|
|
1295
|
+
...data.cursor !== void 0 ? { cursor: data.cursor } : {}
|
|
1296
|
+
};
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
this.setState("authenticated");
|
|
1299
|
+
this.emitter?.emit("dropbox:error", {
|
|
1300
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1301
|
+
action: "loadFiles"
|
|
1302
|
+
});
|
|
1303
|
+
throw err;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// ── File operations: continue listing (pagination) ──
|
|
1307
|
+
async loadMoreFiles(cursor) {
|
|
1308
|
+
try {
|
|
1309
|
+
const res = await this.apiRequest(LIST_FOLDER_CONTINUE_URL, {
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
headers: { "Content-Type": "application/json" },
|
|
1312
|
+
body: JSON.stringify({ cursor })
|
|
1313
|
+
});
|
|
1314
|
+
const data = await res.json();
|
|
1315
|
+
const files = (data.entries ?? []).map(mapEntry);
|
|
1316
|
+
return {
|
|
1317
|
+
files,
|
|
1318
|
+
hasMore: !!data.has_more,
|
|
1319
|
+
...data.cursor !== void 0 ? { cursor: data.cursor } : {}
|
|
1320
|
+
};
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
this.emitter?.emit("dropbox:error", {
|
|
1323
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1324
|
+
action: "loadMoreFiles"
|
|
1325
|
+
});
|
|
1326
|
+
throw err;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// ── File operations: load all files in folder recursively ──
|
|
1330
|
+
async loadAllFilesInFolder(folderPath) {
|
|
1331
|
+
const allFiles = [];
|
|
1332
|
+
try {
|
|
1333
|
+
const initialRes = await this.apiRequest(LIST_FOLDER_URL, {
|
|
1334
|
+
method: "POST",
|
|
1335
|
+
headers: { "Content-Type": "application/json" },
|
|
1336
|
+
body: JSON.stringify({
|
|
1337
|
+
path: folderPath,
|
|
1338
|
+
recursive: true,
|
|
1339
|
+
include_media_info: true
|
|
1340
|
+
})
|
|
1341
|
+
});
|
|
1342
|
+
let data = await initialRes.json();
|
|
1343
|
+
const entries = data.entries ?? [];
|
|
1344
|
+
while (data.has_more) {
|
|
1345
|
+
const contRes = await this.apiRequest(
|
|
1346
|
+
LIST_FOLDER_CONTINUE_URL,
|
|
1347
|
+
{
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
headers: { "Content-Type": "application/json" },
|
|
1350
|
+
body: JSON.stringify({ cursor: data.cursor })
|
|
1351
|
+
}
|
|
1352
|
+
);
|
|
1353
|
+
data = await contRes.json();
|
|
1354
|
+
entries.push(...data.entries ?? []);
|
|
1355
|
+
}
|
|
1356
|
+
for (const entry of entries) {
|
|
1357
|
+
const file = mapEntry(entry);
|
|
1358
|
+
if (!file.isFolder) allFiles.push(file);
|
|
1359
|
+
}
|
|
1360
|
+
return allFiles;
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
this.emitter?.emit("dropbox:error", {
|
|
1363
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1364
|
+
action: "loadAllFilesInFolder"
|
|
1365
|
+
});
|
|
1366
|
+
throw err;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// ── File operations: download files ──
|
|
1370
|
+
async downloadFiles(driveFiles) {
|
|
1371
|
+
const results = [];
|
|
1372
|
+
for (const driveFile of driveFiles) {
|
|
1373
|
+
if (driveFile.isFolder) continue;
|
|
1374
|
+
try {
|
|
1375
|
+
const file = await this.downloadSingleFile(driveFile);
|
|
1376
|
+
if (file) {
|
|
1377
|
+
results.push(file);
|
|
1378
|
+
}
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
this.emitter?.emit("dropbox:error", {
|
|
1381
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1382
|
+
action: "downloadFiles"
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return results;
|
|
1387
|
+
}
|
|
1388
|
+
// ── File operations: search ──
|
|
1389
|
+
async searchFiles(query, path) {
|
|
1390
|
+
try {
|
|
1391
|
+
const body = {
|
|
1392
|
+
query,
|
|
1393
|
+
options: {
|
|
1394
|
+
max_results: 100,
|
|
1395
|
+
file_status: { ".tag": "active" }
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
if (path) {
|
|
1399
|
+
body.options = {
|
|
1400
|
+
...body.options,
|
|
1401
|
+
path
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
const res = await this.apiRequest(SEARCH_URL, {
|
|
1405
|
+
method: "POST",
|
|
1406
|
+
headers: { "Content-Type": "application/json" },
|
|
1407
|
+
body: JSON.stringify(body)
|
|
1408
|
+
});
|
|
1409
|
+
const data = await res.json();
|
|
1410
|
+
const matches = data.matches ?? [];
|
|
1411
|
+
return matches.map((m) => mapEntry(m.metadata.metadata));
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
this.emitter?.emit("dropbox:error", {
|
|
1414
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1415
|
+
action: "searchFiles"
|
|
1416
|
+
});
|
|
1417
|
+
throw err;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// ── Provider hooks required by the base ──
|
|
1421
|
+
mapEntry(entry) {
|
|
1422
|
+
return mapEntry(entry);
|
|
1423
|
+
}
|
|
1424
|
+
// ── Private: download a single file via temporary link ──
|
|
1425
|
+
async downloadSingleFile(driveFile) {
|
|
1426
|
+
const linkRes = await this.apiRequest(TEMP_LINK_URL, {
|
|
1427
|
+
method: "POST",
|
|
1428
|
+
headers: { "Content-Type": "application/json" },
|
|
1429
|
+
body: JSON.stringify({ path: driveFile.path })
|
|
1430
|
+
});
|
|
1431
|
+
const linkData = await linkRes.json();
|
|
1432
|
+
const downloadLink = linkData.link;
|
|
1433
|
+
if (!downloadLink) {
|
|
1434
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
1435
|
+
`No download link returned for ${driveFile.name}`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
const downloadRes = await fetch(downloadLink, { method: "GET" });
|
|
1439
|
+
if (!downloadRes.ok) {
|
|
1440
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
1441
|
+
`Download failed (${downloadRes.status}) for ${driveFile.name}`,
|
|
1442
|
+
downloadRes.status
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
const blob = await downloadRes.blob();
|
|
1446
|
+
return new File([blob], driveFile.name, {
|
|
1447
|
+
type: blob.type || driveFile.mimeType || "application/octet-stream"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
// ── Private: fetch user profile ──
|
|
1451
|
+
async fetchUserProfile() {
|
|
1452
|
+
const res = await this.apiRequest(USER_INFO_URL, {
|
|
1453
|
+
method: "POST",
|
|
1454
|
+
headers: { "Content-Type": "application/json" },
|
|
1455
|
+
body: null
|
|
1456
|
+
});
|
|
1457
|
+
const data = await res.json();
|
|
1458
|
+
return {
|
|
1459
|
+
name: data.name?.display_name ?? "",
|
|
1460
|
+
email: data.email ?? ""
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// src/drives/google-drive-plugin.ts
|
|
1466
|
+
var SK_ACCESS = "upup_gdrive_access_token";
|
|
1467
|
+
var SK_EXPIRY = "upup_gdrive_token_expiry";
|
|
1468
|
+
var FILES_URL = "https://www.googleapis.com/drive/v3/files";
|
|
1469
|
+
var USER_INFO_URL2 = "https://www.googleapis.com/oauth2/v3/userinfo";
|
|
1470
|
+
var WORKSPACE_EXPORT_MAP = {
|
|
1471
|
+
"application/vnd.google-apps.document": {
|
|
1472
|
+
exportMime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1473
|
+
ext: "docx",
|
|
1474
|
+
docType: "document"
|
|
1475
|
+
},
|
|
1476
|
+
"application/vnd.google-apps.spreadsheet": {
|
|
1477
|
+
exportMime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1478
|
+
ext: "xlsx",
|
|
1479
|
+
docType: "spreadsheets"
|
|
1480
|
+
},
|
|
1481
|
+
"application/vnd.google-apps.presentation": {
|
|
1482
|
+
exportMime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1483
|
+
ext: "pptx",
|
|
1484
|
+
docType: "presentation"
|
|
1485
|
+
},
|
|
1486
|
+
"application/vnd.google-apps.drawing": {
|
|
1487
|
+
exportMime: "image/png",
|
|
1488
|
+
ext: "png",
|
|
1489
|
+
docType: "drawings"
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
var FORMAT_MAP = {
|
|
1493
|
+
docx: "docx",
|
|
1494
|
+
xlsx: "xlsx",
|
|
1495
|
+
pptx: "pptx",
|
|
1496
|
+
png: "png"
|
|
1497
|
+
};
|
|
1498
|
+
function isWorkspaceFile(mimeType) {
|
|
1499
|
+
return mimeType in WORKSPACE_EXPORT_MAP;
|
|
1500
|
+
}
|
|
1501
|
+
function getExportUrl(fileId, mimeType) {
|
|
1502
|
+
const mapping = WORKSPACE_EXPORT_MAP[mimeType];
|
|
1503
|
+
if (!mapping) return null;
|
|
1504
|
+
const format = FORMAT_MAP[mapping.ext];
|
|
1505
|
+
return `https://docs.google.com/${mapping.docType}/d/${fileId}/export?format=${format}`;
|
|
1506
|
+
}
|
|
1507
|
+
function mapGoogleEntry(entry) {
|
|
1508
|
+
const mimeType = entry.mimeType ?? "";
|
|
1509
|
+
const isFolder = mimeType === "application/vnd.google-apps.folder";
|
|
1510
|
+
return {
|
|
1511
|
+
id: entry.id ?? "",
|
|
1512
|
+
name: entry.name ?? "",
|
|
1513
|
+
path: "",
|
|
1514
|
+
// Google Drive doesn't return a path, uses parent IDs
|
|
1515
|
+
size: isFolder ? 0 : typeof entry.size === "number" ? entry.size : typeof entry.size === "string" ? parseInt(entry.size, 10) || 0 : 0,
|
|
1516
|
+
mimeType: isFolder ? "folder" : mimeType,
|
|
1517
|
+
isFolder,
|
|
1518
|
+
thumbnail: entry.thumbnailLink ?? void 0,
|
|
1519
|
+
modifiedAt: void 0
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
var GoogleDrivePlugin = class {
|
|
1523
|
+
constructor() {
|
|
1524
|
+
this.id = "google-drive";
|
|
1525
|
+
this.name = "google-drive";
|
|
1526
|
+
this.emitter = null;
|
|
1527
|
+
this.config = {
|
|
1528
|
+
apiKey: "",
|
|
1529
|
+
appId: "",
|
|
1530
|
+
clientId: ""
|
|
1531
|
+
};
|
|
1532
|
+
this.accessToken = null;
|
|
1533
|
+
this.tokenExpiry = 0;
|
|
1534
|
+
this.state = "idle";
|
|
1535
|
+
}
|
|
1536
|
+
// ── Plugin lifecycle ──
|
|
1537
|
+
configure(config) {
|
|
1538
|
+
this.config = config;
|
|
1539
|
+
return this;
|
|
1540
|
+
}
|
|
1541
|
+
getConfig() {
|
|
1542
|
+
return this.config;
|
|
1543
|
+
}
|
|
1544
|
+
init(emitter) {
|
|
1545
|
+
this.emitter = emitter;
|
|
1546
|
+
}
|
|
1547
|
+
destroy() {
|
|
1548
|
+
this.emitter = null;
|
|
1549
|
+
}
|
|
1550
|
+
// ── State management ──
|
|
1551
|
+
getState() {
|
|
1552
|
+
return this.state;
|
|
1553
|
+
}
|
|
1554
|
+
setState(newState) {
|
|
1555
|
+
this.state = newState;
|
|
1556
|
+
this.emitter?.emit("google-drive:state-change", { state: newState });
|
|
1557
|
+
}
|
|
1558
|
+
// ── Auth: accept token from GIS popup (called by React thin hook) ──
|
|
1559
|
+
setAccessToken(token, expiresIn) {
|
|
1560
|
+
const expiry = expiresIn ? Date.now() + expiresIn * 1e3 : 0;
|
|
1561
|
+
this.accessToken = token;
|
|
1562
|
+
this.tokenExpiry = expiry;
|
|
1563
|
+
storageSet(SK_ACCESS, token);
|
|
1564
|
+
if (expiry) storageSet(SK_EXPIRY, String(expiry));
|
|
1565
|
+
this.setState("authenticated");
|
|
1566
|
+
}
|
|
1567
|
+
// ── Auth: authenticate with token + fetch user info ──
|
|
1568
|
+
async authenticate(token, expiresIn) {
|
|
1569
|
+
this.setState("authenticating");
|
|
1570
|
+
try {
|
|
1571
|
+
this.setAccessToken(token, expiresIn);
|
|
1572
|
+
let user;
|
|
1573
|
+
try {
|
|
1574
|
+
user = await this.getUserInfo();
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
this.setState("authenticated");
|
|
1578
|
+
this.emitter?.emit("google-drive:authenticated", { user });
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
this.setState("idle");
|
|
1581
|
+
this.emitter?.emit("google-drive:error", {
|
|
1582
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1583
|
+
action: "authenticate"
|
|
1584
|
+
});
|
|
1585
|
+
throw err;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// ── Auth: sign out ──
|
|
1589
|
+
signOut() {
|
|
1590
|
+
this.clearTokens();
|
|
1591
|
+
this.setState("idle");
|
|
1592
|
+
this.emitter?.emit("google-drive:signed-out", {});
|
|
1593
|
+
}
|
|
1594
|
+
// ── Auth: restore session from sessionStorage ──
|
|
1595
|
+
restoreSession() {
|
|
1596
|
+
const token = storageGet(SK_ACCESS);
|
|
1597
|
+
const expiry = storageGet(SK_EXPIRY);
|
|
1598
|
+
if (!token) return false;
|
|
1599
|
+
this.accessToken = token;
|
|
1600
|
+
this.tokenExpiry = expiry ? parseInt(expiry, 10) : 0;
|
|
1601
|
+
if (this.tokenExpiry > 0 && Date.now() > this.tokenExpiry) {
|
|
1602
|
+
this.clearTokens();
|
|
1603
|
+
this.emitter?.emit("google-drive:session-expired", {});
|
|
1604
|
+
this.setState("session-expired");
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
this.setState("authenticated");
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
// ── Auth helpers ──
|
|
1611
|
+
isAuthenticated() {
|
|
1612
|
+
return this.state === "authenticated" && this.accessToken !== null;
|
|
1613
|
+
}
|
|
1614
|
+
getAccessToken() {
|
|
1615
|
+
return this.accessToken;
|
|
1616
|
+
}
|
|
1617
|
+
// ── User info ──
|
|
1618
|
+
async getUserInfo() {
|
|
1619
|
+
const res = await this.apiRequest(USER_INFO_URL2, { method: "GET" });
|
|
1620
|
+
const data = await res.json();
|
|
1621
|
+
return {
|
|
1622
|
+
name: data.name ?? "",
|
|
1623
|
+
email: data.email ?? "",
|
|
1624
|
+
...data.picture !== void 0 ? { picture: data.picture } : {}
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
// ── File operations: list files ──
|
|
1628
|
+
async loadFiles(folderId) {
|
|
1629
|
+
this.setState("browsing");
|
|
1630
|
+
try {
|
|
1631
|
+
const parentId = folderId || "root";
|
|
1632
|
+
const q = `'${parentId}' in parents and trashed = false`;
|
|
1633
|
+
const params = new URLSearchParams({
|
|
1634
|
+
q,
|
|
1635
|
+
fields: "nextPageToken,files(fileExtension,id,mimeType,name,parents,size,thumbnailLink)",
|
|
1636
|
+
key: this.config.apiKey,
|
|
1637
|
+
pageSize: "1000"
|
|
1638
|
+
});
|
|
1639
|
+
const res = await this.apiRequest(
|
|
1640
|
+
`${FILES_URL}?${params.toString()}`,
|
|
1641
|
+
{ method: "GET" }
|
|
1642
|
+
);
|
|
1643
|
+
const data = await res.json();
|
|
1644
|
+
const files = (data.files ?? []).map(mapGoogleEntry);
|
|
1645
|
+
const hasMore = !!data.nextPageToken;
|
|
1646
|
+
const cursor = hasMore ? JSON.stringify({
|
|
1647
|
+
folderId: parentId,
|
|
1648
|
+
pageToken: data.nextPageToken
|
|
1649
|
+
}) : void 0;
|
|
1650
|
+
this.setState("authenticated");
|
|
1651
|
+
this.emitter?.emit("google-drive:files-loaded", {
|
|
1652
|
+
files,
|
|
1653
|
+
folderId: parentId,
|
|
1654
|
+
hasMore,
|
|
1655
|
+
cursor
|
|
1656
|
+
});
|
|
1657
|
+
return {
|
|
1658
|
+
files,
|
|
1659
|
+
folderId: parentId,
|
|
1660
|
+
hasMore,
|
|
1661
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1662
|
+
};
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
this.setState("authenticated");
|
|
1665
|
+
this.emitter?.emit("google-drive:error", {
|
|
1666
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1667
|
+
action: "loadFiles"
|
|
1668
|
+
});
|
|
1669
|
+
throw err;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
// ── File operations: continue listing (pagination) ──
|
|
1673
|
+
async loadMoreFiles(cursor) {
|
|
1674
|
+
try {
|
|
1675
|
+
const { folderId, pageToken } = JSON.parse(cursor);
|
|
1676
|
+
const q = `'${folderId}' in parents and trashed = false`;
|
|
1677
|
+
const params = new URLSearchParams({
|
|
1678
|
+
q,
|
|
1679
|
+
fields: "nextPageToken,files(fileExtension,id,mimeType,name,parents,size,thumbnailLink)",
|
|
1680
|
+
key: this.config.apiKey,
|
|
1681
|
+
pageSize: "1000",
|
|
1682
|
+
pageToken
|
|
1683
|
+
});
|
|
1684
|
+
const res = await this.apiRequest(
|
|
1685
|
+
`${FILES_URL}?${params.toString()}`,
|
|
1686
|
+
{ method: "GET" }
|
|
1687
|
+
);
|
|
1688
|
+
const data = await res.json();
|
|
1689
|
+
const files = (data.files ?? []).map(mapGoogleEntry);
|
|
1690
|
+
const hasMore = !!data.nextPageToken;
|
|
1691
|
+
const nextCursor = hasMore ? JSON.stringify({ folderId, pageToken: data.nextPageToken }) : void 0;
|
|
1692
|
+
return {
|
|
1693
|
+
files,
|
|
1694
|
+
hasMore,
|
|
1695
|
+
...nextCursor !== void 0 ? { cursor: nextCursor } : {}
|
|
1696
|
+
};
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
this.emitter?.emit("google-drive:error", {
|
|
1699
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1700
|
+
action: "loadMoreFiles"
|
|
1701
|
+
});
|
|
1702
|
+
throw err;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
// ── File operations: download multiple files ──
|
|
1706
|
+
async downloadFiles(driveFiles) {
|
|
1707
|
+
const results = [];
|
|
1708
|
+
for (const driveFile of driveFiles) {
|
|
1709
|
+
if (driveFile.isFolder) continue;
|
|
1710
|
+
try {
|
|
1711
|
+
const file = await this.downloadFile(driveFile);
|
|
1712
|
+
if (file) {
|
|
1713
|
+
results.push(file);
|
|
1714
|
+
}
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
this.emitter?.emit("google-drive:error", {
|
|
1717
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1718
|
+
action: "downloadFiles"
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return results;
|
|
1723
|
+
}
|
|
1724
|
+
// ── File operations: download single file ──
|
|
1725
|
+
async downloadFile(driveFile) {
|
|
1726
|
+
if (isWorkspaceFile(driveFile.mimeType)) {
|
|
1727
|
+
return this.downloadWorkspaceFile(driveFile);
|
|
1728
|
+
}
|
|
1729
|
+
return this.downloadRegularFile(driveFile);
|
|
1730
|
+
}
|
|
1731
|
+
// ── Private: download regular (non-Workspace) file ──
|
|
1732
|
+
async downloadRegularFile(driveFile) {
|
|
1733
|
+
const params = new URLSearchParams({
|
|
1734
|
+
key: this.config.apiKey,
|
|
1735
|
+
alt: "media"
|
|
1736
|
+
});
|
|
1737
|
+
const res = await this.apiRequest(
|
|
1738
|
+
`${FILES_URL}/${driveFile.id}?${params.toString()}`,
|
|
1739
|
+
{ method: "GET" }
|
|
1740
|
+
);
|
|
1741
|
+
const blob = await res.blob();
|
|
1742
|
+
return new File([blob], driveFile.name, {
|
|
1743
|
+
type: blob.type || driveFile.mimeType || "application/octet-stream"
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
// ── Private: download Google Workspace file via export ──
|
|
1747
|
+
async downloadWorkspaceFile(driveFile) {
|
|
1748
|
+
const exportUrl = getExportUrl(driveFile.id, driveFile.mimeType);
|
|
1749
|
+
if (!exportUrl) return null;
|
|
1750
|
+
const mapping = WORKSPACE_EXPORT_MAP[driveFile.mimeType];
|
|
1751
|
+
if (!mapping) return null;
|
|
1752
|
+
const res = await this.apiRequest(exportUrl, { method: "GET" });
|
|
1753
|
+
const blob = await res.blob();
|
|
1754
|
+
const baseName = driveFile.name.replace(/\.[^.]+$/, "");
|
|
1755
|
+
const fileName = `${baseName}.${mapping.ext}`;
|
|
1756
|
+
return new File([blob], fileName, {
|
|
1757
|
+
type: mapping.exportMime
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
// ── Private: authenticated API request ──
|
|
1761
|
+
async apiRequest(url, options) {
|
|
1762
|
+
this.ensureValidToken();
|
|
1763
|
+
const headers = new Headers(options.headers ?? {});
|
|
1764
|
+
headers.set("Authorization", `Bearer ${this.accessToken}`);
|
|
1765
|
+
const res = await fetch(url, { ...options, headers });
|
|
1766
|
+
if (res.ok) return res;
|
|
1767
|
+
const errorText = await res.text();
|
|
1768
|
+
if (res.status === 401) {
|
|
1769
|
+
this.emitter?.emit("google-drive:session-expired", {});
|
|
1770
|
+
this.clearTokens();
|
|
1771
|
+
this.setState("session-expired");
|
|
1772
|
+
}
|
|
1773
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
1774
|
+
`Google Drive API error (${res.status}): ${errorText}`,
|
|
1775
|
+
res.status
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
// ── Private: token management ──
|
|
1779
|
+
clearTokens() {
|
|
1780
|
+
this.accessToken = null;
|
|
1781
|
+
this.tokenExpiry = 0;
|
|
1782
|
+
storageDel(SK_ACCESS);
|
|
1783
|
+
storageDel(SK_EXPIRY);
|
|
1784
|
+
}
|
|
1785
|
+
ensureValidToken() {
|
|
1786
|
+
if (!this.accessToken) {
|
|
1787
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError(
|
|
1788
|
+
"Not authenticated \u2014 no access token",
|
|
1789
|
+
"Google Drive"
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
if (this.tokenExpiry > 0 && Date.now() > this.tokenExpiry) {
|
|
1793
|
+
this.clearTokens();
|
|
1794
|
+
this.emitter?.emit("google-drive:session-expired", {});
|
|
1795
|
+
this.setState("session-expired");
|
|
1796
|
+
throw new chunkQK7HZIVY_cjs.UpupAuthError("Access token has expired", "Google Drive");
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
// src/drives/box-plugin.ts
|
|
1802
|
+
var AUTH_URL2 = "https://account.box.com/api/oauth2/authorize";
|
|
1803
|
+
var TOKEN_URL2 = "https://api.box.com/oauth2/token";
|
|
1804
|
+
var USER_URL = "https://api.box.com/2.0/users/me";
|
|
1805
|
+
var FOLDERS_URL = "https://api.box.com/2.0/folders";
|
|
1806
|
+
var FILES_URL2 = "https://api.box.com/2.0/files";
|
|
1807
|
+
var SEARCH_URL2 = "https://api.box.com/2.0/search";
|
|
1808
|
+
function mapEntry2(entry) {
|
|
1809
|
+
const type = entry.type;
|
|
1810
|
+
const isFolder = type === "folder";
|
|
1811
|
+
return {
|
|
1812
|
+
id: entry.id ?? "",
|
|
1813
|
+
name: entry.name ?? "",
|
|
1814
|
+
path: entry.id ?? "",
|
|
1815
|
+
// Box uses IDs for navigation, not paths
|
|
1816
|
+
size: isFolder ? 0 : entry.size ?? 0,
|
|
1817
|
+
mimeType: isFolder ? "folder" : guessMimeType(entry.name),
|
|
1818
|
+
isFolder,
|
|
1819
|
+
thumbnail: void 0,
|
|
1820
|
+
modifiedAt: entry.modified_at ?? void 0
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
var BoxPlugin = class extends PopupOAuthPlugin {
|
|
1824
|
+
constructor() {
|
|
1825
|
+
super(...arguments);
|
|
1826
|
+
this.spec = {
|
|
1827
|
+
id: "box",
|
|
1828
|
+
displayName: "Box",
|
|
1829
|
+
eventPrefix: "box",
|
|
1830
|
+
popupName: "UpupBoxAuth",
|
|
1831
|
+
authUrl: AUTH_URL2,
|
|
1832
|
+
tokenUrl: TOKEN_URL2,
|
|
1833
|
+
redirectPath: "/box_redirect",
|
|
1834
|
+
storageKeys: {
|
|
1835
|
+
access: "upup_box_access_token",
|
|
1836
|
+
refresh: "upup_box_refresh_token",
|
|
1837
|
+
expiry: "upup_box_token_expiry"
|
|
1838
|
+
}
|
|
1839
|
+
// Box scopes are configured on the Box app, not sent on the authorize URL.
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
configure(config) {
|
|
1843
|
+
return super.configure(config);
|
|
1844
|
+
}
|
|
1845
|
+
getConfig() {
|
|
1846
|
+
return this.config;
|
|
1847
|
+
}
|
|
1848
|
+
// ── File operations: list folder ──
|
|
1849
|
+
async loadFiles(folderId = "0", offset = "0") {
|
|
1850
|
+
this.setState("browsing");
|
|
1851
|
+
try {
|
|
1852
|
+
const params = new URLSearchParams({
|
|
1853
|
+
fields: "id,name,type,size,modified_at",
|
|
1854
|
+
limit: "1000",
|
|
1855
|
+
offset
|
|
1856
|
+
});
|
|
1857
|
+
const res = await this.apiRequest(
|
|
1858
|
+
`${FOLDERS_URL}/${folderId}/items?${params.toString()}`,
|
|
1859
|
+
{ method: "GET" }
|
|
1860
|
+
);
|
|
1861
|
+
const data = await res.json();
|
|
1862
|
+
const files = (data.entries ?? []).map(mapEntry2);
|
|
1863
|
+
const off = Number(data.offset ?? offset);
|
|
1864
|
+
const loaded = off + files.length;
|
|
1865
|
+
const hasMore = loaded < (data.total_count ?? loaded);
|
|
1866
|
+
const cursor = hasMore ? `${folderId}:${loaded}` : void 0;
|
|
1867
|
+
this.setState("authenticated");
|
|
1868
|
+
this.emitter?.emit("box:files-loaded", {
|
|
1869
|
+
files,
|
|
1870
|
+
folderId,
|
|
1871
|
+
hasMore,
|
|
1872
|
+
cursor
|
|
1873
|
+
});
|
|
1874
|
+
return {
|
|
1875
|
+
files,
|
|
1876
|
+
folderId,
|
|
1877
|
+
hasMore,
|
|
1878
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1879
|
+
};
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
this.setState("authenticated");
|
|
1882
|
+
this.emitter?.emit("box:error", {
|
|
1883
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1884
|
+
action: "loadFiles"
|
|
1885
|
+
});
|
|
1886
|
+
throw err;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
// ── File operations: continue listing (pagination) ──
|
|
1890
|
+
async loadMoreFiles(cursor) {
|
|
1891
|
+
const [folderId, offsetStr] = cursor.split(":");
|
|
1892
|
+
try {
|
|
1893
|
+
const params = new URLSearchParams({
|
|
1894
|
+
fields: "id,name,type,size,modified_at",
|
|
1895
|
+
limit: "1000",
|
|
1896
|
+
offset: offsetStr ?? "0"
|
|
1897
|
+
});
|
|
1898
|
+
const res = await this.apiRequest(
|
|
1899
|
+
`${FOLDERS_URL}/${folderId}/items?${params.toString()}`,
|
|
1900
|
+
{ method: "GET" }
|
|
1901
|
+
);
|
|
1902
|
+
const data = await res.json();
|
|
1903
|
+
const files = (data.entries ?? []).map(mapEntry2);
|
|
1904
|
+
const off = Number(data.offset ?? offsetStr ?? 0);
|
|
1905
|
+
const loaded = off + files.length;
|
|
1906
|
+
const hasMore = loaded < (data.total_count ?? loaded);
|
|
1907
|
+
const nextCursor = hasMore ? `${folderId}:${loaded}` : void 0;
|
|
1908
|
+
return {
|
|
1909
|
+
files,
|
|
1910
|
+
hasMore,
|
|
1911
|
+
...nextCursor !== void 0 ? { cursor: nextCursor } : {}
|
|
1912
|
+
};
|
|
1913
|
+
} catch (err) {
|
|
1914
|
+
this.emitter?.emit("box:error", {
|
|
1915
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1916
|
+
action: "loadMoreFiles"
|
|
1917
|
+
});
|
|
1918
|
+
throw err;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
// ── File operations: download files ──
|
|
1922
|
+
async downloadFiles(driveFiles) {
|
|
1923
|
+
const results = [];
|
|
1924
|
+
for (const driveFile of driveFiles) {
|
|
1925
|
+
if (driveFile.isFolder) continue;
|
|
1926
|
+
try {
|
|
1927
|
+
const file = await this.downloadSingleFile(driveFile);
|
|
1928
|
+
if (file) {
|
|
1929
|
+
results.push(file);
|
|
1930
|
+
}
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
this.emitter?.emit("box:error", {
|
|
1933
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1934
|
+
action: "downloadFiles"
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return results;
|
|
1939
|
+
}
|
|
1940
|
+
// ── File operations: load all files in folder recursively ──
|
|
1941
|
+
async loadAllFilesInFolder(folderId) {
|
|
1942
|
+
const allFiles = [];
|
|
1943
|
+
try {
|
|
1944
|
+
const { files } = await this.loadFiles(folderId);
|
|
1945
|
+
for (const file of files) {
|
|
1946
|
+
if (file.isFolder) {
|
|
1947
|
+
const nested = await this.loadAllFilesInFolder(file.id);
|
|
1948
|
+
allFiles.push(...nested);
|
|
1949
|
+
} else {
|
|
1950
|
+
allFiles.push(file);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return allFiles;
|
|
1954
|
+
} catch (err) {
|
|
1955
|
+
this.emitter?.emit("box:error", {
|
|
1956
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1957
|
+
action: "loadAllFilesInFolder"
|
|
1958
|
+
});
|
|
1959
|
+
throw err;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
// ── File operations: search ──
|
|
1963
|
+
async searchFiles(query) {
|
|
1964
|
+
try {
|
|
1965
|
+
const params = new URLSearchParams({
|
|
1966
|
+
query,
|
|
1967
|
+
fields: "id,name,type,size,modified_at",
|
|
1968
|
+
limit: "100"
|
|
1969
|
+
});
|
|
1970
|
+
const res = await this.apiRequest(
|
|
1971
|
+
`${SEARCH_URL2}?${params.toString()}`,
|
|
1972
|
+
{ method: "GET" }
|
|
1973
|
+
);
|
|
1974
|
+
const data = await res.json();
|
|
1975
|
+
const entries = data.entries ?? [];
|
|
1976
|
+
return entries.map(mapEntry2);
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
this.emitter?.emit("box:error", {
|
|
1979
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1980
|
+
action: "searchFiles"
|
|
1981
|
+
});
|
|
1982
|
+
throw err;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
// ── Provider hooks required by the base ──
|
|
1986
|
+
mapEntry(entry) {
|
|
1987
|
+
return mapEntry2(entry);
|
|
1988
|
+
}
|
|
1989
|
+
// ── Private: download a single file ──
|
|
1990
|
+
async downloadSingleFile(driveFile) {
|
|
1991
|
+
const res = await this.apiRequest(
|
|
1992
|
+
`${FILES_URL2}/${driveFile.id}/content`,
|
|
1993
|
+
{ method: "GET" }
|
|
1994
|
+
);
|
|
1995
|
+
const blob = await res.blob();
|
|
1996
|
+
return new File([blob], driveFile.name, {
|
|
1997
|
+
type: blob.type || driveFile.mimeType || "application/octet-stream"
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
// ── Private: fetch user profile ──
|
|
2001
|
+
async fetchUserProfile() {
|
|
2002
|
+
const res = await this.apiRequest(USER_URL, { method: "GET" });
|
|
2003
|
+
const data = await res.json();
|
|
2004
|
+
return {
|
|
2005
|
+
name: data.name ?? "",
|
|
2006
|
+
email: data.login ?? ""
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
// src/drives/one-drive-plugin.ts
|
|
2012
|
+
var AUTH_URL3 = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
|
|
2013
|
+
var TOKEN_URL3 = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
|
2014
|
+
var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
2015
|
+
var OAUTH_SCOPES2 = "user.read files.readwrite.all offline_access";
|
|
2016
|
+
var FILE_SELECT = "id,name,folder,file,size,@microsoft.graph.downloadUrl";
|
|
2017
|
+
var FILE_EXPAND = "thumbnails";
|
|
2018
|
+
function mapGraphItem(item) {
|
|
2019
|
+
const isFolder = !!item.folder;
|
|
2020
|
+
const file = item.file;
|
|
2021
|
+
const mimeType = isFolder ? "folder" : file?.mimeType ?? guessMimeType(item.name);
|
|
2022
|
+
const thumbnails = item.thumbnails;
|
|
2023
|
+
let thumbnail;
|
|
2024
|
+
if (thumbnails && thumbnails.length > 0) {
|
|
2025
|
+
const thumbSet = thumbnails[0];
|
|
2026
|
+
const medium = thumbSet?.medium;
|
|
2027
|
+
thumbnail = medium?.url ?? void 0;
|
|
2028
|
+
}
|
|
2029
|
+
return {
|
|
2030
|
+
id: item.id ?? "",
|
|
2031
|
+
name: item.name ?? "",
|
|
2032
|
+
path: item.id ?? "",
|
|
2033
|
+
// OneDrive uses id-based navigation
|
|
2034
|
+
size: isFolder ? 0 : item.size ?? 0,
|
|
2035
|
+
mimeType,
|
|
2036
|
+
isFolder,
|
|
2037
|
+
thumbnail,
|
|
2038
|
+
modifiedAt: item.lastModifiedDateTime ?? void 0
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
var OneDrivePlugin = class extends PopupOAuthPlugin {
|
|
2042
|
+
constructor() {
|
|
2043
|
+
super(...arguments);
|
|
2044
|
+
this.spec = {
|
|
2045
|
+
id: "one-drive",
|
|
2046
|
+
displayName: "OneDrive",
|
|
2047
|
+
eventPrefix: "one-drive",
|
|
2048
|
+
popupName: "UpupOneDriveAuth",
|
|
2049
|
+
authUrl: AUTH_URL3,
|
|
2050
|
+
tokenUrl: TOKEN_URL3,
|
|
2051
|
+
redirectPath: "/od_redirect",
|
|
2052
|
+
storageKeys: {
|
|
2053
|
+
access: "upup_one-drive_access_token",
|
|
2054
|
+
refresh: "upup_one-drive_refresh_token",
|
|
2055
|
+
expiry: "upup_one-drive_token_expiry"
|
|
2056
|
+
},
|
|
2057
|
+
scopes: OAUTH_SCOPES2,
|
|
2058
|
+
authParams: { response_mode: "query" }
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
configure(config) {
|
|
2062
|
+
return super.configure(config);
|
|
2063
|
+
}
|
|
2064
|
+
getConfig() {
|
|
2065
|
+
return this.config;
|
|
2066
|
+
}
|
|
2067
|
+
// ── File operations: list files ──
|
|
2068
|
+
async loadFiles(folderId) {
|
|
2069
|
+
this.setState("browsing");
|
|
2070
|
+
try {
|
|
2071
|
+
const path = folderId ? `/me/drive/items/${folderId}/children` : "/me/drive/root/children";
|
|
2072
|
+
const params = new URLSearchParams({
|
|
2073
|
+
$select: FILE_SELECT,
|
|
2074
|
+
$expand: FILE_EXPAND,
|
|
2075
|
+
$top: "200"
|
|
2076
|
+
});
|
|
2077
|
+
const data = await this.graphRequest(`${path}?${params.toString()}`);
|
|
2078
|
+
const items = Array.isArray(data.value) ? data.value : [];
|
|
2079
|
+
const files = items.map(mapGraphItem);
|
|
2080
|
+
const nextLink = data["@odata.nextLink"];
|
|
2081
|
+
const hasMore = !!nextLink;
|
|
2082
|
+
this.setState("authenticated");
|
|
2083
|
+
this.emitter?.emit("one-drive:files-loaded", {
|
|
2084
|
+
files,
|
|
2085
|
+
folderId: folderId ?? "root",
|
|
2086
|
+
hasMore,
|
|
2087
|
+
cursor: nextLink
|
|
2088
|
+
});
|
|
2089
|
+
return {
|
|
2090
|
+
files,
|
|
2091
|
+
folderId: folderId ?? "root",
|
|
2092
|
+
hasMore,
|
|
2093
|
+
...nextLink !== void 0 ? { cursor: nextLink } : {}
|
|
2094
|
+
};
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
this.setState("authenticated");
|
|
2097
|
+
this.emitter?.emit("one-drive:error", {
|
|
2098
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2099
|
+
action: "loadFiles"
|
|
2100
|
+
});
|
|
2101
|
+
throw err;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
// ── File operations: continue listing (pagination) ──
|
|
2105
|
+
async loadMoreFiles(cursor) {
|
|
2106
|
+
try {
|
|
2107
|
+
const data = await this.graphRequest(cursor);
|
|
2108
|
+
const items = Array.isArray(data.value) ? data.value : [];
|
|
2109
|
+
const files = items.map(mapGraphItem);
|
|
2110
|
+
const nextLink = data["@odata.nextLink"];
|
|
2111
|
+
return {
|
|
2112
|
+
files,
|
|
2113
|
+
hasMore: !!nextLink,
|
|
2114
|
+
...nextLink !== void 0 ? { cursor: nextLink } : {}
|
|
2115
|
+
};
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
this.emitter?.emit("one-drive:error", {
|
|
2118
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2119
|
+
action: "loadMoreFiles"
|
|
2120
|
+
});
|
|
2121
|
+
throw err;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
// ── File operations: download files ──
|
|
2125
|
+
async downloadFiles(driveFiles) {
|
|
2126
|
+
const results = [];
|
|
2127
|
+
for (const driveFile of driveFiles) {
|
|
2128
|
+
if (driveFile.isFolder) continue;
|
|
2129
|
+
try {
|
|
2130
|
+
const file = await this.downloadSingleFile(driveFile);
|
|
2131
|
+
if (file) {
|
|
2132
|
+
results.push(file);
|
|
2133
|
+
}
|
|
2134
|
+
} catch (err) {
|
|
2135
|
+
this.emitter?.emit("one-drive:error", {
|
|
2136
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2137
|
+
action: "downloadFiles"
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return results;
|
|
2142
|
+
}
|
|
2143
|
+
// ── File operations: load all files in folder recursively ──
|
|
2144
|
+
async loadAllFilesInFolder(folderId) {
|
|
2145
|
+
const allFiles = [];
|
|
2146
|
+
try {
|
|
2147
|
+
const { files } = await this.loadFiles(folderId);
|
|
2148
|
+
for (const file of files) {
|
|
2149
|
+
if (file.isFolder) {
|
|
2150
|
+
const nested = await this.loadAllFilesInFolder(file.id);
|
|
2151
|
+
allFiles.push(...nested);
|
|
2152
|
+
} else {
|
|
2153
|
+
allFiles.push(file);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return allFiles;
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
this.emitter?.emit("one-drive:error", {
|
|
2159
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2160
|
+
action: "loadAllFilesInFolder"
|
|
2161
|
+
});
|
|
2162
|
+
throw err;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
// ── Provider hooks required by the base ──
|
|
2166
|
+
mapEntry(entry) {
|
|
2167
|
+
return mapGraphItem(entry);
|
|
2168
|
+
}
|
|
2169
|
+
// ── Private: fetch user profile (unguarded — called once tokens are set
|
|
2170
|
+
// but before isAuthenticated() flips, and again via the public getUserInfo) ──
|
|
2171
|
+
async fetchUserProfile() {
|
|
2172
|
+
const data = await this.graphRequest("/me");
|
|
2173
|
+
const displayName = data.displayName;
|
|
2174
|
+
const mail = data.mail;
|
|
2175
|
+
const userPrincipalName = data.userPrincipalName;
|
|
2176
|
+
return {
|
|
2177
|
+
name: typeof displayName === "string" ? displayName : "",
|
|
2178
|
+
email: typeof mail === "string" ? mail : typeof userPrincipalName === "string" ? userPrincipalName : ""
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
// ── Private: Microsoft Graph API request (delegates auth/refresh/retry to
|
|
2182
|
+
// the base apiRequest; returns parsed JSON, tolerating absolute URLs) ──
|
|
2183
|
+
async graphRequest(path, options = {}) {
|
|
2184
|
+
const url = path.startsWith("http") ? path : `${GRAPH_BASE}${path}`;
|
|
2185
|
+
const res = await this.apiRequest(url, options);
|
|
2186
|
+
return await res.json();
|
|
2187
|
+
}
|
|
2188
|
+
// ── Private: download a single file ──
|
|
2189
|
+
async downloadSingleFile(driveFile) {
|
|
2190
|
+
const itemData = await this.graphRequest(
|
|
2191
|
+
`/me/drive/items/${driveFile.id}?select=@microsoft.graph.downloadUrl`
|
|
2192
|
+
);
|
|
2193
|
+
const downloadUrl = itemData["@microsoft.graph.downloadUrl"] ?? itemData["@content.downloadUrl"];
|
|
2194
|
+
if (!downloadUrl) {
|
|
2195
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
2196
|
+
`No download URL available for ${driveFile.name}`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
const downloadRes = await fetch(downloadUrl, { method: "GET" });
|
|
2200
|
+
if (!downloadRes.ok) {
|
|
2201
|
+
throw new chunkQK7HZIVY_cjs.UpupNetworkError(
|
|
2202
|
+
`Download failed (${downloadRes.status}) for ${driveFile.name}`,
|
|
2203
|
+
downloadRes.status
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
const blob = await downloadRes.blob();
|
|
2207
|
+
return new File([blob], driveFile.name, {
|
|
2208
|
+
type: blob.type || driveFile.mimeType || "application/octet-stream"
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
// src/utils/accept-presets.ts
|
|
2214
|
+
var ACCEPT_PRESETS = {
|
|
2215
|
+
images: {
|
|
2216
|
+
label: "Images",
|
|
2217
|
+
accept: "image/*"
|
|
2218
|
+
},
|
|
2219
|
+
video: {
|
|
2220
|
+
label: "Video",
|
|
2221
|
+
accept: "video/*"
|
|
2222
|
+
},
|
|
2223
|
+
audio: {
|
|
2224
|
+
label: "Audio",
|
|
2225
|
+
accept: "audio/*"
|
|
2226
|
+
},
|
|
2227
|
+
documents: {
|
|
2228
|
+
label: "Documents",
|
|
2229
|
+
accept: "application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.oasis.opendocument.text,text/plain,text/rtf"
|
|
2230
|
+
},
|
|
2231
|
+
spreadsheets: {
|
|
2232
|
+
label: "Spreadsheets",
|
|
2233
|
+
accept: "application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,application/vnd.oasis.opendocument.spreadsheet,.tsv"
|
|
2234
|
+
},
|
|
2235
|
+
presentations: {
|
|
2236
|
+
label: "Presentations",
|
|
2237
|
+
accept: "application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.oasis.opendocument.presentation,.key"
|
|
2238
|
+
},
|
|
2239
|
+
archives: {
|
|
2240
|
+
label: "Archives",
|
|
2241
|
+
accept: "application/zip,application/x-rar-compressed,application/x-7z-compressed,application/x-tar,application/gzip,.bz2"
|
|
2242
|
+
},
|
|
2243
|
+
code: {
|
|
2244
|
+
label: "Code",
|
|
2245
|
+
accept: "text/javascript,application/json,text/html,text/css,text/xml,text/yaml,.ts,.tsx,.jsx,.py,.rb,.go,.rs,.java,.cpp,.c,.h,.sh,.sql"
|
|
2246
|
+
},
|
|
2247
|
+
fonts: {
|
|
2248
|
+
label: "Fonts",
|
|
2249
|
+
accept: "font/ttf,font/otf,font/woff,font/woff2,.ttf,.otf,.woff,.woff2"
|
|
2250
|
+
},
|
|
2251
|
+
"3d": {
|
|
2252
|
+
label: "3D",
|
|
2253
|
+
accept: ".obj,.stl,.gltf,.glb,.fbx,.usdz"
|
|
2254
|
+
},
|
|
2255
|
+
design: {
|
|
2256
|
+
label: "Design",
|
|
2257
|
+
accept: "image/svg+xml,.psd,.ai,.sketch,.fig,.xd"
|
|
2258
|
+
},
|
|
2259
|
+
ebooks: {
|
|
2260
|
+
label: "Ebooks",
|
|
2261
|
+
accept: "application/epub+zip,application/pdf,.mobi,.azw"
|
|
2262
|
+
},
|
|
2263
|
+
photography: {
|
|
2264
|
+
label: "Photography",
|
|
2265
|
+
accept: "image/*,.cr2,.cr3,.nef,.arw,.dng,.orf,.rw2,.raf,.raw"
|
|
2266
|
+
},
|
|
2267
|
+
animation: {
|
|
2268
|
+
label: "Animation",
|
|
2269
|
+
accept: "image/gif,image/apng,image/webp,.lottie,.rive"
|
|
2270
|
+
},
|
|
2271
|
+
ar: {
|
|
2272
|
+
label: "AR",
|
|
2273
|
+
accept: ".usdz,.reality,.glb,.gltf"
|
|
2274
|
+
},
|
|
2275
|
+
vector: {
|
|
2276
|
+
label: "Vector",
|
|
2277
|
+
accept: "image/svg+xml,.eps,.ai"
|
|
2278
|
+
},
|
|
2279
|
+
cad: {
|
|
2280
|
+
label: "CAD",
|
|
2281
|
+
accept: ".dwg,.dxf,.step,.stp,.iges,.igs"
|
|
2282
|
+
},
|
|
2283
|
+
gis: {
|
|
2284
|
+
label: "GIS",
|
|
2285
|
+
accept: "application/geo+json,.kml,.kmz,.shp,.gpx,.geojson"
|
|
2286
|
+
},
|
|
2287
|
+
data: {
|
|
2288
|
+
label: "Data",
|
|
2289
|
+
accept: "application/json,text/xml,text/csv,text/yaml,.toml,.parquet,.jsonl,.ndjson"
|
|
2290
|
+
},
|
|
2291
|
+
markup: {
|
|
2292
|
+
label: "Markup",
|
|
2293
|
+
accept: "text/markdown,.md,.rst,.tex,.adoc,.latex"
|
|
2294
|
+
},
|
|
2295
|
+
subtitles: {
|
|
2296
|
+
label: "Subtitles",
|
|
2297
|
+
accept: ".srt,.vtt,.ass,.sub,.ssa"
|
|
2298
|
+
},
|
|
2299
|
+
email: {
|
|
2300
|
+
label: "Email",
|
|
2301
|
+
accept: ".eml,.msg,.mbox"
|
|
2302
|
+
},
|
|
2303
|
+
calendar: {
|
|
2304
|
+
label: "Calendar",
|
|
2305
|
+
accept: "text/calendar,.ics,.ical"
|
|
2306
|
+
},
|
|
2307
|
+
contacts: {
|
|
2308
|
+
label: "Contacts",
|
|
2309
|
+
accept: "text/vcard,.vcf"
|
|
2310
|
+
},
|
|
2311
|
+
disk: {
|
|
2312
|
+
label: "Disk",
|
|
2313
|
+
accept: ".iso,.img,.dmg,.vhd,.vhdx"
|
|
2314
|
+
},
|
|
2315
|
+
ml: {
|
|
2316
|
+
label: "ML",
|
|
2317
|
+
accept: ".onnx,.h5,.pb,.pt,.safetensors,.pkl"
|
|
2318
|
+
},
|
|
2319
|
+
database: {
|
|
2320
|
+
label: "Database",
|
|
2321
|
+
accept: ".sql,.sqlite,.db,.mdb,.accdb"
|
|
2322
|
+
},
|
|
2323
|
+
certificates: {
|
|
2324
|
+
label: "Certificates",
|
|
2325
|
+
accept: ".pem,.crt,.cer,.p12,.pfx,.key"
|
|
2326
|
+
},
|
|
2327
|
+
firmware: {
|
|
2328
|
+
label: "Firmware",
|
|
2329
|
+
accept: ".bin,.hex,.elf,.srec"
|
|
2330
|
+
},
|
|
2331
|
+
executable: {
|
|
2332
|
+
label: "Executable",
|
|
2333
|
+
accept: ".exe,.msi,.dmg,.apk,.deb,.rpm,.appimage"
|
|
2334
|
+
},
|
|
2335
|
+
config: {
|
|
2336
|
+
label: "Config",
|
|
2337
|
+
accept: ".ini,.cfg,.conf,.env,.toml,.properties,.yaml,.yml"
|
|
2338
|
+
},
|
|
2339
|
+
financial: {
|
|
2340
|
+
label: "Financial",
|
|
2341
|
+
accept: ".ofx,.qfx,.qbo,.qif"
|
|
2342
|
+
},
|
|
2343
|
+
scientific: {
|
|
2344
|
+
label: "Scientific",
|
|
2345
|
+
accept: ".fits,.hdf5,.nc,.mat,.netcdf"
|
|
2346
|
+
},
|
|
2347
|
+
medical: {
|
|
2348
|
+
label: "Medical",
|
|
2349
|
+
accept: "application/dicom,.dcm,.nii,.nii.gz"
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
function resolveAccept(input) {
|
|
2353
|
+
const raw = Array.isArray(input) ? input.join(",") : input;
|
|
2354
|
+
return raw.split(",").map((token) => {
|
|
2355
|
+
const trimmed = token.trim();
|
|
2356
|
+
const key = trimmed.toLowerCase();
|
|
2357
|
+
return ACCEPT_PRESETS[key]?.accept ?? trimmed;
|
|
2358
|
+
}).join(",");
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
exports.ACCEPT_PRESETS = ACCEPT_PRESETS;
|
|
2362
|
+
exports.BoxPlugin = BoxPlugin;
|
|
2363
|
+
exports.CrashRecoveryManager = CrashRecoveryManager;
|
|
2364
|
+
exports.DropboxPlugin = DropboxPlugin;
|
|
2365
|
+
exports.EventEmitter = EventEmitter;
|
|
2366
|
+
exports.FileManager = FileManager;
|
|
2367
|
+
exports.GoogleDrivePlugin = GoogleDrivePlugin;
|
|
2368
|
+
exports.IndexedDBStorage = IndexedDBStorage;
|
|
2369
|
+
exports.OneDrivePlugin = OneDrivePlugin;
|
|
2370
|
+
exports.PluginManager = PluginManager;
|
|
2371
|
+
exports.PopupOAuthPlugin = PopupOAuthPlugin;
|
|
2372
|
+
exports.UploadManager = UploadManager;
|
|
2373
|
+
exports.isFileRemovalLocked = isFileRemovalLocked;
|
|
2374
|
+
exports.isUploadActive = isUploadActive;
|
|
2375
|
+
exports.isUploadIdle = isUploadIdle;
|
|
2376
|
+
exports.resolveAccept = resolveAccept;
|
|
2377
|
+
exports.validateFileRestrictions = validateFileRestrictions;
|