@anvilkit/plugin-asset-manager 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -3
- package/dist/adapters/data-url.d.cts +1 -0
- package/dist/adapters/data-url.d.cts.map +1 -1
- package/dist/adapters/data-url.d.ts +1 -0
- package/dist/adapters/data-url.d.ts.map +1 -1
- package/dist/adapters/s3-multipart.cjs +425 -0
- package/dist/adapters/s3-multipart.d.cts +43 -0
- package/dist/adapters/s3-multipart.d.cts.map +1 -0
- package/dist/adapters/s3-multipart.d.ts +43 -0
- package/dist/adapters/s3-multipart.d.ts.map +1 -0
- package/dist/adapters/s3-multipart.js +387 -0
- package/dist/adapters/s3-presigned.d.cts +2 -0
- package/dist/adapters/s3-presigned.d.cts.map +1 -1
- package/dist/adapters/s3-presigned.d.ts +2 -0
- package/dist/adapters/s3-presigned.d.ts.map +1 -1
- package/dist/i18n/provider.d.cts +1 -0
- package/dist/i18n/provider.d.cts.map +1 -1
- package/dist/i18n/provider.d.ts +1 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +9 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/plugin.cjs +152 -12
- package/dist/plugin.d.cts +49 -1
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.ts +49 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +147 -13
- package/dist/sources/composite-source.cjs +3 -0
- package/dist/sources/composite-source.d.cts.map +1 -1
- package/dist/sources/composite-source.d.ts.map +1 -1
- package/dist/sources/composite-source.js +3 -0
- package/dist/sources/federated-search.cjs +45 -7
- package/dist/sources/federated-search.d.cts.map +1 -1
- package/dist/sources/federated-search.d.ts.map +1 -1
- package/dist/sources/federated-search.js +45 -7
- package/dist/sources/provider.d.cts +5 -0
- package/dist/sources/provider.d.cts.map +1 -1
- package/dist/sources/provider.d.ts +5 -0
- package/dist/sources/provider.d.ts.map +1 -1
- package/dist/sources/unsplash/index.d.cts +1 -0
- package/dist/sources/unsplash/index.d.cts.map +1 -1
- package/dist/sources/unsplash/index.d.ts +1 -0
- package/dist/sources/unsplash/index.d.ts.map +1 -1
- package/dist/testing/index.d.cts +4 -0
- package/dist/testing/index.d.cts.map +1 -1
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/types/categories.d.cts +3 -0
- package/dist/types/categories.d.cts.map +1 -1
- package/dist/types/categories.d.ts +3 -0
- package/dist/types/categories.d.ts.map +1 -1
- package/dist/types/data-source.d.cts +9 -0
- package/dist/types/data-source.d.cts.map +1 -1
- package/dist/types/data-source.d.ts +9 -0
- package/dist/types/data-source.d.ts.map +1 -1
- package/dist/types/filter.d.cts +11 -0
- package/dist/types/filter.d.cts.map +1 -1
- package/dist/types/filter.d.ts +11 -0
- package/dist/types/filter.d.ts.map +1 -1
- package/dist/types/folders.d.cts +2 -0
- package/dist/types/folders.d.cts.map +1 -1
- package/dist/types/folders.d.ts +2 -0
- package/dist/types/folders.d.ts.map +1 -1
- package/dist/types/options.d.cts +57 -1
- package/dist/types/options.d.cts.map +1 -1
- package/dist/types/options.d.ts +57 -1
- package/dist/types/options.d.ts.map +1 -1
- package/dist/types/resumable.cjs +42 -0
- package/dist/types/resumable.d.cts +204 -0
- package/dist/types/resumable.d.cts.map +1 -0
- package/dist/types/resumable.d.ts +204 -0
- package/dist/types/resumable.d.ts.map +1 -0
- package/dist/types/resumable.js +4 -0
- package/dist/types/transform.cjs +18 -0
- package/dist/types/transform.d.cts +45 -0
- package/dist/types/transform.d.cts.map +1 -0
- package/dist/types/transform.d.ts +45 -0
- package/dist/types/transform.d.ts.map +1 -0
- package/dist/types/transform.js +1 -0
- package/dist/types/types.d.cts +17 -0
- package/dist/types/types.d.cts.map +1 -1
- package/dist/types/types.d.ts +17 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/unsplash.d.cts +2 -0
- package/dist/types/unsplash.d.cts.map +1 -1
- package/dist/types/unsplash.d.ts +2 -0
- package/dist/types/unsplash.d.ts.map +1 -1
- package/dist/ui/AssetBrowser.d.cts +3 -1
- package/dist/ui/AssetBrowser.d.cts.map +1 -1
- package/dist/ui/AssetBrowser.d.ts +3 -1
- package/dist/ui/AssetBrowser.d.ts.map +1 -1
- package/dist/ui/AssetCommandPalette.d.cts +4 -1
- package/dist/ui/AssetCommandPalette.d.cts.map +1 -1
- package/dist/ui/AssetCommandPalette.d.ts +4 -1
- package/dist/ui/AssetCommandPalette.d.ts.map +1 -1
- package/dist/ui/AssetManagerUI.cjs +3 -1
- package/dist/ui/AssetManagerUI.d.cts +11 -2
- package/dist/ui/AssetManagerUI.d.cts.map +1 -1
- package/dist/ui/AssetManagerUI.d.ts +11 -2
- package/dist/ui/AssetManagerUI.d.ts.map +1 -1
- package/dist/ui/AssetManagerUI.js +3 -1
- package/dist/ui/DeleteAssetDialog.d.cts +4 -1
- package/dist/ui/DeleteAssetDialog.d.cts.map +1 -1
- package/dist/ui/DeleteAssetDialog.d.ts +4 -1
- package/dist/ui/DeleteAssetDialog.d.ts.map +1 -1
- package/dist/ui/DeleteFolderDialog.d.cts +4 -1
- package/dist/ui/DeleteFolderDialog.d.cts.map +1 -1
- package/dist/ui/DeleteFolderDialog.d.ts +4 -1
- package/dist/ui/DeleteFolderDialog.d.ts.map +1 -1
- package/dist/ui/EmptyFolderState.d.cts +4 -1
- package/dist/ui/EmptyFolderState.d.cts.map +1 -1
- package/dist/ui/EmptyFolderState.d.ts +4 -1
- package/dist/ui/EmptyFolderState.d.ts.map +1 -1
- package/dist/ui/FolderBreadcrumb.d.cts +4 -1
- package/dist/ui/FolderBreadcrumb.d.cts.map +1 -1
- package/dist/ui/FolderBreadcrumb.d.ts +4 -1
- package/dist/ui/FolderBreadcrumb.d.ts.map +1 -1
- package/dist/ui/FolderNameDialog.d.cts +3 -1
- package/dist/ui/FolderNameDialog.d.cts.map +1 -1
- package/dist/ui/FolderNameDialog.d.ts +3 -1
- package/dist/ui/FolderNameDialog.d.ts.map +1 -1
- package/dist/ui/FolderTree.d.cts +4 -1
- package/dist/ui/FolderTree.d.cts.map +1 -1
- package/dist/ui/FolderTree.d.ts +4 -1
- package/dist/ui/FolderTree.d.ts.map +1 -1
- package/dist/ui/MetadataPanel.d.cts +4 -1
- package/dist/ui/MetadataPanel.d.cts.map +1 -1
- package/dist/ui/MetadataPanel.d.ts +4 -1
- package/dist/ui/MetadataPanel.d.ts.map +1 -1
- package/dist/ui/MoveTargetPicker.d.cts +3 -1
- package/dist/ui/MoveTargetPicker.d.cts.map +1 -1
- package/dist/ui/MoveTargetPicker.d.ts +3 -1
- package/dist/ui/MoveTargetPicker.d.ts.map +1 -1
- package/dist/ui/ReplaceAssetDialog.cjs +7 -2
- package/dist/ui/ReplaceAssetDialog.d.cts +5 -2
- package/dist/ui/ReplaceAssetDialog.d.cts.map +1 -1
- package/dist/ui/ReplaceAssetDialog.d.ts +5 -2
- package/dist/ui/ReplaceAssetDialog.d.ts.map +1 -1
- package/dist/ui/ReplaceAssetDialog.js +7 -2
- package/dist/ui/UnsplashPanel.d.cts +4 -1
- package/dist/ui/UnsplashPanel.d.cts.map +1 -1
- package/dist/ui/UnsplashPanel.d.ts +4 -1
- package/dist/ui/UnsplashPanel.d.ts.map +1 -1
- package/dist/ui/UploadButton.cjs +7 -2
- package/dist/ui/UploadButton.d.cts +6 -2
- package/dist/ui/UploadButton.d.cts.map +1 -1
- package/dist/ui/UploadButton.d.ts +6 -2
- package/dist/ui/UploadButton.d.ts.map +1 -1
- package/dist/ui/UploadButton.js +7 -2
- package/dist/utils/asset-reference.cjs +87 -4
- package/dist/utils/asset-reference.d.cts +30 -6
- package/dist/utils/asset-reference.d.cts.map +1 -1
- package/dist/utils/asset-reference.d.ts +30 -6
- package/dist/utils/asset-reference.d.ts.map +1 -1
- package/dist/utils/asset-reference.js +83 -3
- package/dist/utils/csp.cjs +16 -0
- package/dist/utils/csp.d.cts +23 -0
- package/dist/utils/csp.d.cts.map +1 -1
- package/dist/utils/csp.d.ts +23 -0
- package/dist/utils/csp.d.ts.map +1 -1
- package/dist/utils/csp.js +16 -0
- package/dist/utils/data-source.cjs +19 -5
- package/dist/utils/data-source.d.cts +5 -1
- package/dist/utils/data-source.d.cts.map +1 -1
- package/dist/utils/data-source.d.ts +5 -1
- package/dist/utils/data-source.d.ts.map +1 -1
- package/dist/utils/data-source.js +19 -5
- package/dist/utils/errors.d.cts +5 -0
- package/dist/utils/errors.d.cts.map +1 -1
- package/dist/utils/errors.d.ts +5 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/query-param-transform.cjs +101 -0
- package/dist/utils/query-param-transform.d.cts +46 -0
- package/dist/utils/query-param-transform.d.cts.map +1 -0
- package/dist/utils/query-param-transform.d.ts +46 -0
- package/dist/utils/query-param-transform.d.ts.map +1 -0
- package/dist/utils/query-param-transform.js +60 -0
- package/dist/utils/registry.cjs +3 -0
- package/dist/utils/registry.d.cts +8 -0
- package/dist/utils/registry.d.cts.map +1 -1
- package/dist/utils/registry.d.ts +8 -0
- package/dist/utils/registry.d.ts.map +1 -1
- package/dist/utils/registry.js +3 -0
- package/dist/utils/resolver.cjs +19 -11
- package/dist/utils/resolver.d.cts +24 -0
- package/dist/utils/resolver.d.cts.map +1 -1
- package/dist/utils/resolver.d.ts +24 -0
- package/dist/utils/resolver.d.ts.map +1 -1
- package/dist/utils/resolver.js +19 -11
- package/dist/utils/retry.d.cts +2 -0
- package/dist/utils/retry.d.cts.map +1 -1
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/run-resumable-upload.cjs +160 -0
- package/dist/utils/run-resumable-upload.d.cts +56 -0
- package/dist/utils/run-resumable-upload.d.cts.map +1 -0
- package/dist/utils/run-resumable-upload.d.ts +56 -0
- package/dist/utils/run-resumable-upload.d.ts.map +1 -0
- package/dist/utils/run-resumable-upload.js +122 -0
- package/dist/utils/sniff-file-type.cjs +209 -0
- package/dist/utils/sniff-file-type.d.cts +25 -0
- package/dist/utils/sniff-file-type.d.cts.map +1 -0
- package/dist/utils/sniff-file-type.d.ts +25 -0
- package/dist/utils/sniff-file-type.d.ts.map +1 -0
- package/dist/utils/sniff-file-type.js +164 -0
- package/dist/utils/studio-asset-source.cjs +11 -6
- package/dist/utils/studio-asset-source.d.cts +5 -1
- package/dist/utils/studio-asset-source.d.cts.map +1 -1
- package/dist/utils/studio-asset-source.d.ts +5 -1
- package/dist/utils/studio-asset-source.d.ts.map +1 -1
- package/dist/utils/studio-asset-source.js +11 -6
- package/dist/utils/upload-session-store.cjs +125 -0
- package/dist/utils/upload-session-store.d.cts +55 -0
- package/dist/utils/upload-session-store.d.cts.map +1 -0
- package/dist/utils/upload-session-store.d.ts +55 -0
- package/dist/utils/upload-session-store.d.ts.map +1 -0
- package/dist/utils/upload-session-store.js +84 -0
- package/dist/utils/validate-upload-result.cjs +9 -1
- package/dist/utils/validate-upload-result.d.cts +1 -0
- package/dist/utils/validate-upload-result.d.cts.map +1 -1
- package/dist/utils/validate-upload-result.d.ts +1 -0
- package/dist/utils/validate-upload-result.d.ts.map +1 -1
- package/dist/utils/validate-upload-result.js +9 -1
- package/dist/version.cjs +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.cts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/meta/config.json +1 -1
- package/package.json +42 -12
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
|
+
var define = (defs, kind)=>{
|
|
6
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
[kind]: defs[key]
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
define(getters, "get");
|
|
12
|
+
define(values, "value");
|
|
13
|
+
};
|
|
14
|
+
})();
|
|
15
|
+
(()=>{
|
|
16
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
17
|
+
})();
|
|
18
|
+
(()=>{
|
|
19
|
+
__webpack_require__.r = (exports1)=>{
|
|
20
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
21
|
+
value: 'Module'
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
24
|
+
value: true
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
})();
|
|
28
|
+
var __webpack_exports__ = {};
|
|
29
|
+
__webpack_require__.r(__webpack_exports__);
|
|
30
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
31
|
+
runResumableUpload: ()=>runResumableUpload
|
|
32
|
+
});
|
|
33
|
+
const external_errors_cjs_namespaceObject = require("./errors.cjs");
|
|
34
|
+
const external_retry_cjs_namespaceObject = require("./retry.cjs");
|
|
35
|
+
const external_upload_session_store_cjs_namespaceObject = require("./upload-session-store.cjs");
|
|
36
|
+
const DEFAULT_PART_SIZE = 8388608;
|
|
37
|
+
async function runResumableUpload(adapter, file, options = {}) {
|
|
38
|
+
const sessionStore = options.sessionStore ?? (0, external_upload_session_store_cjs_namespaceObject.createUploadSessionStore)();
|
|
39
|
+
const { signal } = options;
|
|
40
|
+
const callOptions = signal ? {
|
|
41
|
+
signal
|
|
42
|
+
} : void 0;
|
|
43
|
+
throwIfAborted(signal);
|
|
44
|
+
const persisted = await sessionStore.load(file) ?? void 0;
|
|
45
|
+
const session = await adapter.begin(file, persisted, callOptions);
|
|
46
|
+
const resumedParts = session.parts ?? [];
|
|
47
|
+
let effectivePartSize;
|
|
48
|
+
if (void 0 !== persisted && resumedParts.length > 0) {
|
|
49
|
+
const locked = persisted.partSize;
|
|
50
|
+
if (void 0 !== session.partSize && session.partSize !== locked) throw new external_errors_cjs_namespaceObject.AssetValidationError("PART_SIZE_MISMATCH", `runResumableUpload: resumed session changed part size (${locked} → ${session.partSize}); refusing to corrupt already-uploaded parts.`);
|
|
51
|
+
effectivePartSize = locked;
|
|
52
|
+
} else effectivePartSize = session.partSize ?? options.partSize ?? DEFAULT_PART_SIZE;
|
|
53
|
+
if (!Number.isSafeInteger(effectivePartSize) || effectivePartSize <= 0) throw new external_errors_cjs_namespaceObject.AssetValidationError("INVALID_PART_SIZE", `runResumableUpload: part size must be a positive integer (got ${effectivePartSize}).`);
|
|
54
|
+
const plan = planParts(file, effectivePartSize);
|
|
55
|
+
const sizeByPart = new Map(plan.map((p)=>[
|
|
56
|
+
p.partNumber,
|
|
57
|
+
p.end - p.start
|
|
58
|
+
]));
|
|
59
|
+
const completed = new Map();
|
|
60
|
+
for (const tag of resumedParts)if (sizeByPart.has(tag.partNumber)) completed.set(tag.partNumber, tag);
|
|
61
|
+
const emitProgress = ()=>{
|
|
62
|
+
options.onProgress?.({
|
|
63
|
+
uploadedBytes: sumCompletedBytes(completed, sizeByPart),
|
|
64
|
+
totalBytes: file.size,
|
|
65
|
+
uploadedParts: completed.size,
|
|
66
|
+
totalParts: plan.length
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
emitProgress();
|
|
70
|
+
try {
|
|
71
|
+
for (const part of plan){
|
|
72
|
+
throwIfAborted(signal);
|
|
73
|
+
if (completed.has(part.partNumber)) continue;
|
|
74
|
+
const tag = await (0, external_retry_cjs_namespaceObject.withRetry)(()=>adapter.uploadPart(session, part, callOptions), {
|
|
75
|
+
...options.retry ?? {},
|
|
76
|
+
...signal ? {
|
|
77
|
+
signal
|
|
78
|
+
} : {}
|
|
79
|
+
});
|
|
80
|
+
completed.set(part.partNumber, tag);
|
|
81
|
+
await sessionStore.save(file, toPersisted(session, effectivePartSize, completed));
|
|
82
|
+
emitProgress();
|
|
83
|
+
}
|
|
84
|
+
const orderedTags = plan.map((part)=>{
|
|
85
|
+
const tag = completed.get(part.partNumber);
|
|
86
|
+
if (void 0 === tag) throw new Error(`runResumableUpload: missing tag for part ${part.partNumber}.`);
|
|
87
|
+
return tag;
|
|
88
|
+
});
|
|
89
|
+
const result = await adapter.complete(session, orderedTags, callOptions);
|
|
90
|
+
await sessionStore.clear(file);
|
|
91
|
+
return result;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isAbort(error, signal)) {
|
|
94
|
+
await safeAbort(adapter, session);
|
|
95
|
+
await sessionStore.clear(file);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function planParts(file, partSize) {
|
|
101
|
+
const parts = [];
|
|
102
|
+
const size = file.size;
|
|
103
|
+
let partNumber = 1;
|
|
104
|
+
for(let start = 0; start < size; start += partSize){
|
|
105
|
+
const end = Math.min(start + partSize, size);
|
|
106
|
+
parts.push({
|
|
107
|
+
partNumber,
|
|
108
|
+
start,
|
|
109
|
+
end,
|
|
110
|
+
blob: file.slice(start, end)
|
|
111
|
+
});
|
|
112
|
+
partNumber += 1;
|
|
113
|
+
}
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
116
|
+
function sumCompletedBytes(completed, sizeByPart) {
|
|
117
|
+
let total = 0;
|
|
118
|
+
for (const partNumber of completed.keys())total += sizeByPart.get(partNumber) ?? 0;
|
|
119
|
+
return total;
|
|
120
|
+
}
|
|
121
|
+
function toPersisted(session, partSize, completed) {
|
|
122
|
+
const parts = [
|
|
123
|
+
...completed.values()
|
|
124
|
+
].sort((a, b)=>a.partNumber - b.partNumber);
|
|
125
|
+
return {
|
|
126
|
+
uploadId: session.uploadId,
|
|
127
|
+
partSize,
|
|
128
|
+
parts,
|
|
129
|
+
...session.meta ? {
|
|
130
|
+
meta: session.meta
|
|
131
|
+
} : {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function safeAbort(adapter, session) {
|
|
135
|
+
try {
|
|
136
|
+
await adapter.abort(session);
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
function isAbort(error, signal) {
|
|
140
|
+
if (signal?.aborted) return true;
|
|
141
|
+
return null !== error && "object" == typeof error && "AbortError" === error.name;
|
|
142
|
+
}
|
|
143
|
+
function throwIfAborted(signal) {
|
|
144
|
+
if (signal?.aborted) throw toAbortError(signal);
|
|
145
|
+
}
|
|
146
|
+
function toAbortError(signal) {
|
|
147
|
+
const reason = signal.reason;
|
|
148
|
+
if (reason instanceof Error) return reason;
|
|
149
|
+
if ("u" > typeof DOMException) return new DOMException("Aborted", "AbortError");
|
|
150
|
+
const error = new Error("Aborted");
|
|
151
|
+
error.name = "AbortError";
|
|
152
|
+
return error;
|
|
153
|
+
}
|
|
154
|
+
exports.runResumableUpload = __webpack_exports__.runResumableUpload;
|
|
155
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
156
|
+
"runResumableUpload"
|
|
157
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
158
|
+
Object.defineProperty(exports, '__esModule', {
|
|
159
|
+
value: true
|
|
160
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Adapter-agnostic resumable-upload runner (PRD 0004 §5.1 — M3).
|
|
3
|
+
*
|
|
4
|
+
* Drives any {@link ResumableUploadAdapter} through a full multipart lifecycle:
|
|
5
|
+
* slice the file into ordered parts, resume the ones a prior run already
|
|
6
|
+
* uploaded, upload the rest with per-part retry, emit progress, finalize, and
|
|
7
|
+
* tear down on abort. It is deliberately backend-neutral — it never assumes S3;
|
|
8
|
+
* the S3 specifics live in the adapter (M4).
|
|
9
|
+
*
|
|
10
|
+
* Trust boundary: this returns the adapter's raw {@link UploadResult}. The
|
|
11
|
+
* pipeline (M5) runs it through `validateUploadResult()` exactly as it does for
|
|
12
|
+
* single-shot uploads, so the runner stays a pure orchestrator.
|
|
13
|
+
*
|
|
14
|
+
* @experimental Public surface may change before v1.0.
|
|
15
|
+
*/
|
|
16
|
+
import type { ResumableUploadAdapter, UploadSessionStore } from "../types/resumable.js";
|
|
17
|
+
import type { UploadResult } from "../types/types.js";
|
|
18
|
+
import { type RetryOptions } from "./retry.js";
|
|
19
|
+
/** Progress snapshot emitted as parts complete. */
|
|
20
|
+
export interface ResumableUploadProgress {
|
|
21
|
+
/** Bytes confirmed stored so far (sum of completed part sizes). */
|
|
22
|
+
readonly uploadedBytes: number;
|
|
23
|
+
/** Total bytes in the source file. */
|
|
24
|
+
readonly totalBytes: number;
|
|
25
|
+
/** Number of parts confirmed stored. */
|
|
26
|
+
readonly uploadedParts: number;
|
|
27
|
+
/** Total number of parts in the plan. */
|
|
28
|
+
readonly totalParts: number;
|
|
29
|
+
}
|
|
30
|
+
/** Options for {@link runResumableUpload}. */
|
|
31
|
+
export interface RunResumableUploadOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Bytes per part for a *fresh* upload. Ignored when resuming — the locked
|
|
34
|
+
* size from the persisted/echoed session is used instead. Defaults to 8 MiB.
|
|
35
|
+
*/
|
|
36
|
+
readonly partSize?: number;
|
|
37
|
+
/** Aborts the upload (and any in-flight part + retry sleeps). */
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
/** Called after `begin` (with resumed progress) and after each part completes. */
|
|
40
|
+
readonly onProgress?: (progress: ResumableUploadProgress) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Where progress is persisted for resume. Defaults to the built-in
|
|
43
|
+
* `createUploadSessionStore()` (localStorage, in-memory fallback).
|
|
44
|
+
*/
|
|
45
|
+
readonly sessionStore?: UploadSessionStore;
|
|
46
|
+
/** Forwarded to `withRetry()` for each part upload (retry-by-part). */
|
|
47
|
+
readonly retry?: RetryOptions;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Upload `file` through `adapter` as a resumable multipart upload. Returns the
|
|
51
|
+
* adapter's `UploadResult` (unvalidated — the caller validates). Rejects on
|
|
52
|
+
* abort (after best-effort backend teardown) or when a part's retries are
|
|
53
|
+
* exhausted (leaving the session persisted so a later call can resume).
|
|
54
|
+
*/
|
|
55
|
+
export declare function runResumableUpload(adapter: ResumableUploadAdapter, file: File, options?: RunResumableUploadOptions): Promise<UploadResult>;
|
|
56
|
+
//# sourceMappingURL=run-resumable-upload.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-resumable-upload.d.cts","sourceRoot":"","sources":["../../src/utils/run-resumable-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAGX,sBAAsB,EAGtB,kBAAkB,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,KAAK,YAAY,EAAa,MAAM,YAAY,CAAC;AAM1D,mDAAmD;AACnD,MAAM,WAAW,uBAAuB;IACvC,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED,8CAA8C;AAC9C,MAAM,WAAW,yBAAyB;IACzC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAClE;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAC3C,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,yBAA8B,GACrC,OAAO,CAAC,YAAY,CAAC,CAwGvB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Adapter-agnostic resumable-upload runner (PRD 0004 §5.1 — M3).
|
|
3
|
+
*
|
|
4
|
+
* Drives any {@link ResumableUploadAdapter} through a full multipart lifecycle:
|
|
5
|
+
* slice the file into ordered parts, resume the ones a prior run already
|
|
6
|
+
* uploaded, upload the rest with per-part retry, emit progress, finalize, and
|
|
7
|
+
* tear down on abort. It is deliberately backend-neutral — it never assumes S3;
|
|
8
|
+
* the S3 specifics live in the adapter (M4).
|
|
9
|
+
*
|
|
10
|
+
* Trust boundary: this returns the adapter's raw {@link UploadResult}. The
|
|
11
|
+
* pipeline (M5) runs it through `validateUploadResult()` exactly as it does for
|
|
12
|
+
* single-shot uploads, so the runner stays a pure orchestrator.
|
|
13
|
+
*
|
|
14
|
+
* @experimental Public surface may change before v1.0.
|
|
15
|
+
*/
|
|
16
|
+
import type { ResumableUploadAdapter, UploadSessionStore } from "../types/resumable.js";
|
|
17
|
+
import type { UploadResult } from "../types/types.js";
|
|
18
|
+
import { type RetryOptions } from "./retry.js";
|
|
19
|
+
/** Progress snapshot emitted as parts complete. */
|
|
20
|
+
export interface ResumableUploadProgress {
|
|
21
|
+
/** Bytes confirmed stored so far (sum of completed part sizes). */
|
|
22
|
+
readonly uploadedBytes: number;
|
|
23
|
+
/** Total bytes in the source file. */
|
|
24
|
+
readonly totalBytes: number;
|
|
25
|
+
/** Number of parts confirmed stored. */
|
|
26
|
+
readonly uploadedParts: number;
|
|
27
|
+
/** Total number of parts in the plan. */
|
|
28
|
+
readonly totalParts: number;
|
|
29
|
+
}
|
|
30
|
+
/** Options for {@link runResumableUpload}. */
|
|
31
|
+
export interface RunResumableUploadOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Bytes per part for a *fresh* upload. Ignored when resuming — the locked
|
|
34
|
+
* size from the persisted/echoed session is used instead. Defaults to 8 MiB.
|
|
35
|
+
*/
|
|
36
|
+
readonly partSize?: number;
|
|
37
|
+
/** Aborts the upload (and any in-flight part + retry sleeps). */
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
/** Called after `begin` (with resumed progress) and after each part completes. */
|
|
40
|
+
readonly onProgress?: (progress: ResumableUploadProgress) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Where progress is persisted for resume. Defaults to the built-in
|
|
43
|
+
* `createUploadSessionStore()` (localStorage, in-memory fallback).
|
|
44
|
+
*/
|
|
45
|
+
readonly sessionStore?: UploadSessionStore;
|
|
46
|
+
/** Forwarded to `withRetry()` for each part upload (retry-by-part). */
|
|
47
|
+
readonly retry?: RetryOptions;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Upload `file` through `adapter` as a resumable multipart upload. Returns the
|
|
51
|
+
* adapter's `UploadResult` (unvalidated — the caller validates). Rejects on
|
|
52
|
+
* abort (after best-effort backend teardown) or when a part's retries are
|
|
53
|
+
* exhausted (leaving the session persisted so a later call can resume).
|
|
54
|
+
*/
|
|
55
|
+
export declare function runResumableUpload(adapter: ResumableUploadAdapter, file: File, options?: RunResumableUploadOptions): Promise<UploadResult>;
|
|
56
|
+
//# sourceMappingURL=run-resumable-upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-resumable-upload.d.ts","sourceRoot":"","sources":["../../src/utils/run-resumable-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAGX,sBAAsB,EAGtB,kBAAkB,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,KAAK,YAAY,EAAa,MAAM,YAAY,CAAC;AAM1D,mDAAmD;AACnD,MAAM,WAAW,uBAAuB;IACvC,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED,8CAA8C;AAC9C,MAAM,WAAW,yBAAyB;IACzC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAClE;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAC3C,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,yBAA8B,GACrC,OAAO,CAAC,YAAY,CAAC,CAwGvB"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AssetValidationError } from "./errors.js";
|
|
2
|
+
import { withRetry } from "./retry.js";
|
|
3
|
+
import { createUploadSessionStore } from "./upload-session-store.js";
|
|
4
|
+
const DEFAULT_PART_SIZE = 8388608;
|
|
5
|
+
async function runResumableUpload(adapter, file, options = {}) {
|
|
6
|
+
const sessionStore = options.sessionStore ?? createUploadSessionStore();
|
|
7
|
+
const { signal } = options;
|
|
8
|
+
const callOptions = signal ? {
|
|
9
|
+
signal
|
|
10
|
+
} : void 0;
|
|
11
|
+
throwIfAborted(signal);
|
|
12
|
+
const persisted = await sessionStore.load(file) ?? void 0;
|
|
13
|
+
const session = await adapter.begin(file, persisted, callOptions);
|
|
14
|
+
const resumedParts = session.parts ?? [];
|
|
15
|
+
let effectivePartSize;
|
|
16
|
+
if (void 0 !== persisted && resumedParts.length > 0) {
|
|
17
|
+
const locked = persisted.partSize;
|
|
18
|
+
if (void 0 !== session.partSize && session.partSize !== locked) throw new AssetValidationError("PART_SIZE_MISMATCH", `runResumableUpload: resumed session changed part size (${locked} → ${session.partSize}); refusing to corrupt already-uploaded parts.`);
|
|
19
|
+
effectivePartSize = locked;
|
|
20
|
+
} else effectivePartSize = session.partSize ?? options.partSize ?? DEFAULT_PART_SIZE;
|
|
21
|
+
if (!Number.isSafeInteger(effectivePartSize) || effectivePartSize <= 0) throw new AssetValidationError("INVALID_PART_SIZE", `runResumableUpload: part size must be a positive integer (got ${effectivePartSize}).`);
|
|
22
|
+
const plan = planParts(file, effectivePartSize);
|
|
23
|
+
const sizeByPart = new Map(plan.map((p)=>[
|
|
24
|
+
p.partNumber,
|
|
25
|
+
p.end - p.start
|
|
26
|
+
]));
|
|
27
|
+
const completed = new Map();
|
|
28
|
+
for (const tag of resumedParts)if (sizeByPart.has(tag.partNumber)) completed.set(tag.partNumber, tag);
|
|
29
|
+
const emitProgress = ()=>{
|
|
30
|
+
options.onProgress?.({
|
|
31
|
+
uploadedBytes: sumCompletedBytes(completed, sizeByPart),
|
|
32
|
+
totalBytes: file.size,
|
|
33
|
+
uploadedParts: completed.size,
|
|
34
|
+
totalParts: plan.length
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
emitProgress();
|
|
38
|
+
try {
|
|
39
|
+
for (const part of plan){
|
|
40
|
+
throwIfAborted(signal);
|
|
41
|
+
if (completed.has(part.partNumber)) continue;
|
|
42
|
+
const tag = await withRetry(()=>adapter.uploadPart(session, part, callOptions), {
|
|
43
|
+
...options.retry ?? {},
|
|
44
|
+
...signal ? {
|
|
45
|
+
signal
|
|
46
|
+
} : {}
|
|
47
|
+
});
|
|
48
|
+
completed.set(part.partNumber, tag);
|
|
49
|
+
await sessionStore.save(file, toPersisted(session, effectivePartSize, completed));
|
|
50
|
+
emitProgress();
|
|
51
|
+
}
|
|
52
|
+
const orderedTags = plan.map((part)=>{
|
|
53
|
+
const tag = completed.get(part.partNumber);
|
|
54
|
+
if (void 0 === tag) throw new Error(`runResumableUpload: missing tag for part ${part.partNumber}.`);
|
|
55
|
+
return tag;
|
|
56
|
+
});
|
|
57
|
+
const result = await adapter.complete(session, orderedTags, callOptions);
|
|
58
|
+
await sessionStore.clear(file);
|
|
59
|
+
return result;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (isAbort(error, signal)) {
|
|
62
|
+
await safeAbort(adapter, session);
|
|
63
|
+
await sessionStore.clear(file);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function planParts(file, partSize) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
const size = file.size;
|
|
71
|
+
let partNumber = 1;
|
|
72
|
+
for(let start = 0; start < size; start += partSize){
|
|
73
|
+
const end = Math.min(start + partSize, size);
|
|
74
|
+
parts.push({
|
|
75
|
+
partNumber,
|
|
76
|
+
start,
|
|
77
|
+
end,
|
|
78
|
+
blob: file.slice(start, end)
|
|
79
|
+
});
|
|
80
|
+
partNumber += 1;
|
|
81
|
+
}
|
|
82
|
+
return parts;
|
|
83
|
+
}
|
|
84
|
+
function sumCompletedBytes(completed, sizeByPart) {
|
|
85
|
+
let total = 0;
|
|
86
|
+
for (const partNumber of completed.keys())total += sizeByPart.get(partNumber) ?? 0;
|
|
87
|
+
return total;
|
|
88
|
+
}
|
|
89
|
+
function toPersisted(session, partSize, completed) {
|
|
90
|
+
const parts = [
|
|
91
|
+
...completed.values()
|
|
92
|
+
].sort((a, b)=>a.partNumber - b.partNumber);
|
|
93
|
+
return {
|
|
94
|
+
uploadId: session.uploadId,
|
|
95
|
+
partSize,
|
|
96
|
+
parts,
|
|
97
|
+
...session.meta ? {
|
|
98
|
+
meta: session.meta
|
|
99
|
+
} : {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function safeAbort(adapter, session) {
|
|
103
|
+
try {
|
|
104
|
+
await adapter.abort(session);
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
function isAbort(error, signal) {
|
|
108
|
+
if (signal?.aborted) return true;
|
|
109
|
+
return null !== error && "object" == typeof error && "AbortError" === error.name;
|
|
110
|
+
}
|
|
111
|
+
function throwIfAborted(signal) {
|
|
112
|
+
if (signal?.aborted) throw toAbortError(signal);
|
|
113
|
+
}
|
|
114
|
+
function toAbortError(signal) {
|
|
115
|
+
const reason = signal.reason;
|
|
116
|
+
if (reason instanceof Error) return reason;
|
|
117
|
+
if ("u" > typeof DOMException) return new DOMException("Aborted", "AbortError");
|
|
118
|
+
const error = new Error("Aborted");
|
|
119
|
+
error.name = "AbortError";
|
|
120
|
+
return error;
|
|
121
|
+
}
|
|
122
|
+
export { runResumableUpload };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
|
+
var define = (defs, kind)=>{
|
|
6
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
[kind]: defs[key]
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
define(getters, "get");
|
|
12
|
+
define(values, "value");
|
|
13
|
+
};
|
|
14
|
+
})();
|
|
15
|
+
(()=>{
|
|
16
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
17
|
+
})();
|
|
18
|
+
(()=>{
|
|
19
|
+
__webpack_require__.r = (exports1)=>{
|
|
20
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
21
|
+
value: 'Module'
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
24
|
+
value: true
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
})();
|
|
28
|
+
var __webpack_exports__ = {};
|
|
29
|
+
__webpack_require__.r(__webpack_exports__);
|
|
30
|
+
const ascii = (text)=>Array.from(text, (ch)=>ch.charCodeAt(0));
|
|
31
|
+
const SIGNATURES = [
|
|
32
|
+
{
|
|
33
|
+
mime: "image/png",
|
|
34
|
+
parts: [
|
|
35
|
+
{
|
|
36
|
+
offset: 0,
|
|
37
|
+
bytes: [
|
|
38
|
+
0x89,
|
|
39
|
+
0x50,
|
|
40
|
+
0x4e,
|
|
41
|
+
0x47,
|
|
42
|
+
0x0d,
|
|
43
|
+
0x0a,
|
|
44
|
+
0x1a,
|
|
45
|
+
0x0a
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
mime: "image/jpeg",
|
|
52
|
+
parts: [
|
|
53
|
+
{
|
|
54
|
+
offset: 0,
|
|
55
|
+
bytes: [
|
|
56
|
+
0xff,
|
|
57
|
+
0xd8,
|
|
58
|
+
0xff
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
mime: "image/gif",
|
|
65
|
+
parts: [
|
|
66
|
+
{
|
|
67
|
+
offset: 0,
|
|
68
|
+
bytes: ascii("GIF8")
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
mime: "image/bmp",
|
|
74
|
+
parts: [
|
|
75
|
+
{
|
|
76
|
+
offset: 0,
|
|
77
|
+
bytes: ascii("BM")
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
mime: "image/webp",
|
|
83
|
+
parts: [
|
|
84
|
+
{
|
|
85
|
+
offset: 0,
|
|
86
|
+
bytes: ascii("RIFF")
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
offset: 8,
|
|
90
|
+
bytes: ascii("WEBP")
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
mime: "audio/wav",
|
|
96
|
+
parts: [
|
|
97
|
+
{
|
|
98
|
+
offset: 0,
|
|
99
|
+
bytes: ascii("RIFF")
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
offset: 8,
|
|
103
|
+
bytes: ascii("WAVE")
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
mime: "video/webm",
|
|
109
|
+
parts: [
|
|
110
|
+
{
|
|
111
|
+
offset: 0,
|
|
112
|
+
bytes: [
|
|
113
|
+
0x1a,
|
|
114
|
+
0x45,
|
|
115
|
+
0xdf,
|
|
116
|
+
0xa3
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
mime: "audio/ogg",
|
|
123
|
+
parts: [
|
|
124
|
+
{
|
|
125
|
+
offset: 0,
|
|
126
|
+
bytes: ascii("OggS")
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
mime: "audio/mpeg",
|
|
132
|
+
parts: [
|
|
133
|
+
{
|
|
134
|
+
offset: 0,
|
|
135
|
+
bytes: ascii("ID3")
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
mime: "application/pdf",
|
|
141
|
+
parts: [
|
|
142
|
+
{
|
|
143
|
+
offset: 0,
|
|
144
|
+
bytes: ascii("%PDF")
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
const FTYP_BRAND_MIME = {
|
|
150
|
+
avif: "image/avif",
|
|
151
|
+
avis: "image/avif",
|
|
152
|
+
heic: "image/heic",
|
|
153
|
+
heix: "image/heic",
|
|
154
|
+
heim: "image/heic",
|
|
155
|
+
heis: "image/heic",
|
|
156
|
+
mif1: "image/heic",
|
|
157
|
+
isom: "video/mp4",
|
|
158
|
+
iso2: "video/mp4",
|
|
159
|
+
mp41: "video/mp4",
|
|
160
|
+
mp42: "video/mp4",
|
|
161
|
+
avc1: "video/mp4",
|
|
162
|
+
dash: "video/mp4",
|
|
163
|
+
"qt ": "video/quicktime"
|
|
164
|
+
};
|
|
165
|
+
const FTYP = ascii("ftyp");
|
|
166
|
+
const SNIFF_BYTE_COUNT = 16;
|
|
167
|
+
function detectMimeFromBytes(bytes) {
|
|
168
|
+
if (matchesAt(bytes, {
|
|
169
|
+
offset: 4,
|
|
170
|
+
bytes: FTYP
|
|
171
|
+
})) {
|
|
172
|
+
const brand = readAscii(bytes, 8, 4);
|
|
173
|
+
return void 0 !== brand ? FTYP_BRAND_MIME[brand] : void 0;
|
|
174
|
+
}
|
|
175
|
+
for (const signature of SIGNATURES)if (signature.parts.every((part)=>matchesAt(bytes, part))) return signature.mime;
|
|
176
|
+
}
|
|
177
|
+
function readAscii(bytes, offset, length) {
|
|
178
|
+
if (offset + length > bytes.length) return;
|
|
179
|
+
let out = "";
|
|
180
|
+
for(let i = 0; i < length; i += 1)out += String.fromCharCode(bytes[offset + i]);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
function matchesAt(bytes, part) {
|
|
184
|
+
if (part.offset + part.bytes.length > bytes.length) return false;
|
|
185
|
+
for(let i = 0; i < part.bytes.length; i += 1)if (bytes[part.offset + i] !== part.bytes[i]) return false;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
async function sniffFileMime(file) {
|
|
189
|
+
if ("function" != typeof file.slice) return;
|
|
190
|
+
const buffer = await file.slice(0, SNIFF_BYTE_COUNT).arrayBuffer();
|
|
191
|
+
return detectMimeFromBytes(new Uint8Array(buffer));
|
|
192
|
+
}
|
|
193
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
194
|
+
detectMimeFromBytes: ()=>detectMimeFromBytes,
|
|
195
|
+
sniffFileMime: ()=>sniffFileMime
|
|
196
|
+
}, {
|
|
197
|
+
SNIFF_BYTE_COUNT: SNIFF_BYTE_COUNT
|
|
198
|
+
});
|
|
199
|
+
exports.SNIFF_BYTE_COUNT = __webpack_exports__.SNIFF_BYTE_COUNT;
|
|
200
|
+
exports.detectMimeFromBytes = __webpack_exports__.detectMimeFromBytes;
|
|
201
|
+
exports.sniffFileMime = __webpack_exports__.sniffFileMime;
|
|
202
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
203
|
+
"SNIFF_BYTE_COUNT",
|
|
204
|
+
"detectMimeFromBytes",
|
|
205
|
+
"sniffFileMime"
|
|
206
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
207
|
+
Object.defineProperty(exports, '__esModule', {
|
|
208
|
+
value: true
|
|
209
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Magic-byte file-type sniffer (PRD 0004 — review #5 residual).
|
|
3
|
+
*
|
|
4
|
+
* Browser `File.type` is host-set and often empty or spoofable, so the MIME /
|
|
5
|
+
* extension allowlists in `validateSelectedFile` can be lied to. This detects a
|
|
6
|
+
* file's REAL type from its leading bytes (a small signature table covering the
|
|
7
|
+
* common image / video / audio / document formats) so the upload pipeline can
|
|
8
|
+
* reject content that contradicts its declared type.
|
|
9
|
+
*
|
|
10
|
+
* Lazy-loaded (imported only when `sniffContent` is enabled) so the signature
|
|
11
|
+
* table never enters the eager headless entry.
|
|
12
|
+
*
|
|
13
|
+
* @experimental Public surface may change before v1.0.
|
|
14
|
+
*/
|
|
15
|
+
/** Number of leading bytes the table needs (max offset + part length). */
|
|
16
|
+
export declare const SNIFF_BYTE_COUNT = 16;
|
|
17
|
+
/**
|
|
18
|
+
* Detect a MIME type from a file's leading bytes, or `undefined` when no known
|
|
19
|
+
* signature matches (many valid types — SVG, plain text, etc. — have no fixed
|
|
20
|
+
* magic bytes, so the caller must treat `undefined` as "unknown", not "bad").
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectMimeFromBytes(bytes: Uint8Array): string | undefined;
|
|
23
|
+
/** Read a file's leading bytes and detect its type (see {@link detectMimeFromBytes}). */
|
|
24
|
+
export declare function sniffFileMime(file: File): Promise<string | undefined>;
|
|
25
|
+
//# sourceMappingURL=sniff-file-type.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sniff-file-type.d.cts","sourceRoot":"","sources":["../../src/utils/sniff-file-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8EH,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAYzE;AAuBD,yFAAyF;AACzF,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Magic-byte file-type sniffer (PRD 0004 — review #5 residual).
|
|
3
|
+
*
|
|
4
|
+
* Browser `File.type` is host-set and often empty or spoofable, so the MIME /
|
|
5
|
+
* extension allowlists in `validateSelectedFile` can be lied to. This detects a
|
|
6
|
+
* file's REAL type from its leading bytes (a small signature table covering the
|
|
7
|
+
* common image / video / audio / document formats) so the upload pipeline can
|
|
8
|
+
* reject content that contradicts its declared type.
|
|
9
|
+
*
|
|
10
|
+
* Lazy-loaded (imported only when `sniffContent` is enabled) so the signature
|
|
11
|
+
* table never enters the eager headless entry.
|
|
12
|
+
*
|
|
13
|
+
* @experimental Public surface may change before v1.0.
|
|
14
|
+
*/
|
|
15
|
+
/** Number of leading bytes the table needs (max offset + part length). */
|
|
16
|
+
export declare const SNIFF_BYTE_COUNT = 16;
|
|
17
|
+
/**
|
|
18
|
+
* Detect a MIME type from a file's leading bytes, or `undefined` when no known
|
|
19
|
+
* signature matches (many valid types — SVG, plain text, etc. — have no fixed
|
|
20
|
+
* magic bytes, so the caller must treat `undefined` as "unknown", not "bad").
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectMimeFromBytes(bytes: Uint8Array): string | undefined;
|
|
23
|
+
/** Read a file's leading bytes and detect its type (see {@link detectMimeFromBytes}). */
|
|
24
|
+
export declare function sniffFileMime(file: File): Promise<string | undefined>;
|
|
25
|
+
//# sourceMappingURL=sniff-file-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sniff-file-type.d.ts","sourceRoot":"","sources":["../../src/utils/sniff-file-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8EH,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAYzE;AAuBD,yFAAyF;AACzF,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E"}
|