@cardinali-studio/adminkit 2.2.25 → 2.2.33
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/dist/app/app.css +3 -3
- package/dist/app/delivery.d.ts +11 -0
- package/dist/app/delivery.js +46 -0
- package/dist/app/index.d.ts +4 -1
- package/dist/app/index.js +3 -2
- package/dist/app/lib/deliveries.d.ts +8 -0
- package/dist/app/lib/delivery-network.d.ts +25 -0
- package/dist/app/lib/delivery-network.js +159 -0
- package/dist/app/lib/delivery-upload.d.ts +29 -11
- package/dist/app/lib/delivery-upload.js +408 -131
- package/dist/app/lib/upload-queue.d.ts +21 -3
- package/dist/app/lib/upload-queue.js +76 -19
- package/dist/app/routes/Deliveries.js +108 -29
- package/dist/app/routes/UploadDock.js +6 -2
- package/dist/direct-publication.d.ts +20 -0
- package/dist/direct-publication.js +75 -0
- package/dist/functions/_shared/b2.js +22 -3
- package/dist/functions/_shared/delivery-crypto.js +5 -0
- package/dist/functions/_shared/delivery-limits.d.ts +9 -0
- package/dist/functions/_shared/delivery-limits.js +29 -0
- package/dist/functions/_shared/delivery-manifest-v2.sql +49 -0
- package/dist/functions/_shared/delivery-manifest.js +392 -0
- package/dist/functions/_shared/delivery-purge.js +17 -1
- package/dist/functions/api/deliveries.js +177 -49
- package/dist/functions/api/delivery/image.js +11 -6
- package/dist/functions/api/delivery/multipart.js +196 -64
- package/dist/functions/delivery-public/assets/client.js +246 -0
- package/dist/functions/delivery-public/assets/delivery.css +297 -0
- package/dist/functions/delivery-public/assets/gallery.js +243 -0
- package/dist/functions/delivery-public/assets/stage.js +107 -0
- package/dist/functions/delivery-public/index.js +637 -0
- package/dist/functions/delivery-public/migrations/0002_deliveries.sql +128 -0
- package/dist/functions/delivery-public/migrations/0003_delivery_quick_hero.sql +34 -0
- package/dist/functions/delivery-public/migrations/0004_delivery_file_expiry.sql +9 -0
- package/dist/functions/delivery-public/migrations/0006_delivery_manifest_v2.sql +49 -0
- package/dist/functions/delivery-public/migrations/0007_delivery_link_aliases.sql +8 -0
- package/dist/functions/delivery-public/migrations/0008_delivery_web_format.sql +2 -0
- package/dist/functions/delivery-public/migrations/0009_delivery_download_labels.sql +2 -0
- package/dist/functions/index.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +10 -3
|
@@ -6,26 +6,175 @@
|
|
|
6
6
|
* - il manifest conserva gli offset dei membri STORE, così la Function pubblica può
|
|
7
7
|
* servire una singola foto con un GET Range senza duplicarla come oggetto.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import { deliveryJson, deliveryPut, deliveryGate, checkCancelled, UploadCancelled } from "./delivery-network.js";
|
|
10
|
+
import { DELIVERY_PART_BYTES, DELIVERY_MAX_ARCHIVE_BYTES, validateDeliverySourceBytes, DELIVERY_V1_MANIFEST_BYTES, DELIVERY_SKIP_REASON_CHARS } from "../../functions/_shared/delivery-limits.js";
|
|
11
|
+
export { UploadCancelled } from "./delivery-network.js";
|
|
12
|
+
const PART = DELIVERY_PART_BYTES;
|
|
10
13
|
const READ = 4 * 1024 * 1024;
|
|
11
14
|
const UPLOAD_CONCURRENCY = 4;
|
|
12
15
|
const IMAGE = /\.(jpe?g|png|webp)$/i;
|
|
13
16
|
const enc = new TextEncoder();
|
|
14
17
|
const droppedPaths = new WeakMap();
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
/* Il cancello di pausa e annullamento.
|
|
19
|
+
*
|
|
20
|
+
* Chi carica lo passa da fuori (lib/upload-queue.ts) e questo file si limita a bussare: a
|
|
21
|
+
* ogni file e a ogni PARTE. Le parti contano più dei file — un solo video da 20 GB è un
|
|
22
|
+
* file solo e mille parti, e senza il controllo lì la pausa non arriverebbe mai.
|
|
23
|
+
* `gate()` si risolve subito quando non c'è niente da aspettare: sul percorso normale costa
|
|
24
|
+
* una promise già risolta per parte, cioè niente. */
|
|
25
|
+
export async function uploadDeliveryPackage(deliveryId, packageId, selected, options, onProgress, control = { gate: async () => { } }) {
|
|
26
|
+
preflightDeliverySelection(selected, options);
|
|
27
|
+
if (options.manifestVersion !== 2)
|
|
28
|
+
return performDeliveryPackage(deliveryId, packageId, selected, options, onProgress, control);
|
|
29
|
+
if (typeof navigator === "undefined" || !navigator.locks)
|
|
30
|
+
throw new Error("Per la ripresa persistente serve un browser aggiornato con supporto ai blocchi di caricamento.");
|
|
31
|
+
return navigator.locks.request(`adminkit-delivery:${packageId}`, { ifAvailable: true }, async (lock) => {
|
|
32
|
+
if (!lock)
|
|
33
|
+
throw new Error("Il caricamento è già in corso in un’altra scheda.");
|
|
34
|
+
const key = `adminkit-delivery-writer:${packageId}`;
|
|
35
|
+
let token;
|
|
36
|
+
try {
|
|
37
|
+
token = localStorage.getItem(key) || crypto.randomUUID();
|
|
38
|
+
localStorage.setItem(key, token);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error("Il browser non può conservare l’identità del caricamento. Consenti l’archiviazione locale e riprova.");
|
|
42
|
+
}
|
|
43
|
+
control.writerToken = token;
|
|
44
|
+
await deliveryJson("/admin/api/deliveries", { action: "manifestClaim", id: deliveryId, packageId }, control);
|
|
45
|
+
// Mantiene la proprietà anche durante una pausa; un errore di rete è gestito
|
|
46
|
+
// dalla prossima mutazione, senza tentativi sovrapposti o errori non osservati.
|
|
47
|
+
let renewing = false;
|
|
48
|
+
const timer = setInterval(() => {
|
|
49
|
+
if (renewing || control.signal?.aborted)
|
|
50
|
+
return;
|
|
51
|
+
renewing = true;
|
|
52
|
+
void fetch("/admin/api/deliveries", { method: "POST", credentials: "same-origin",
|
|
53
|
+
headers: { "Content-Type": "application/json" }, signal: control.signal,
|
|
54
|
+
body: JSON.stringify({ action: "manifestClaim", id: deliveryId, packageId, writerToken: token }),
|
|
55
|
+
}).catch(() => { }).finally(() => { renewing = false; });
|
|
56
|
+
}, 5 * 60_000);
|
|
57
|
+
try {
|
|
58
|
+
return await performDeliveryPackage(deliveryId, packageId, selected, options, onProgress, control);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearInterval(timer);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function performDeliveryPackage(deliveryId, packageId, selected, options, onProgress, control) {
|
|
66
|
+
preflightDeliverySelection(selected, options);
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const abort = () => controller.abort();
|
|
69
|
+
control?.signal?.addEventListener("abort", abort, { once: true });
|
|
70
|
+
if (control?.signal?.aborted)
|
|
71
|
+
abort();
|
|
72
|
+
const local = {
|
|
73
|
+
writerToken: control?.writerToken,
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
networkWaiting: (waiting) => control?.networkWaiting?.(waiting),
|
|
76
|
+
gate: async () => { await deliveryGate({ ...control, signal: controller.signal, gate: () => control?.gate() || Promise.resolve() }); },
|
|
77
|
+
};
|
|
78
|
+
const sinks = [];
|
|
79
|
+
try {
|
|
80
|
+
return await buildDeliveryPackage(deliveryId, packageId, selected, options, onProgress, local, sinks);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
controller.abort();
|
|
84
|
+
await Promise.allSettled(sinks.map((sink) => sink.settle()));
|
|
85
|
+
control?.signal?.removeEventListener("abort", abort);
|
|
86
|
+
}
|
|
17
87
|
}
|
|
18
|
-
|
|
88
|
+
async function buildDeliveryPackage(deliveryId, packageId, selected, options, onProgress, control, sinks) {
|
|
19
89
|
const startedAt = Date.now();
|
|
20
|
-
const files = [...selected].sort((a, b) =>
|
|
90
|
+
const files = [...selected].sort((a, b) => options.manifestVersion === 2
|
|
91
|
+
? (deliveryRelativeName(a) < deliveryRelativeName(b) ? -1 : deliveryRelativeName(a) > deliveryRelativeName(b) ? 1 : 0)
|
|
92
|
+
: deliveryRelativeName(a).localeCompare(deliveryRelativeName(b), undefined, { numeric: true, sensitivity: "base" }));
|
|
21
93
|
if (!files.length)
|
|
22
94
|
throw new Error("Nessun file selezionato");
|
|
23
95
|
const sourceBytes = files.reduce((n, f) => n + f.size, 0);
|
|
24
96
|
const entries = [];
|
|
25
|
-
let
|
|
97
|
+
let checkpoint = null;
|
|
98
|
+
if (options.manifestVersion === 2) {
|
|
99
|
+
const status = await deliveryJson("/admin/api/deliveries", {
|
|
100
|
+
action: "manifestStatus", id: deliveryId, packageId,
|
|
101
|
+
}, control);
|
|
102
|
+
if (status.state === "sealed" || status.state === "ready")
|
|
103
|
+
return {
|
|
104
|
+
manifest: { version: 2, createdAt: "", entries: [], skipped: [] }, fileCount: files.length, sourceBytes, storedBytes: 0,
|
|
105
|
+
};
|
|
106
|
+
checkpoint = status.checkpoint;
|
|
107
|
+
if (checkpoint) {
|
|
108
|
+
let after = -1;
|
|
109
|
+
while (entries.length < checkpoint.fileIndex) {
|
|
110
|
+
const page = await deliveryJson("/admin/api/deliveries", {
|
|
111
|
+
action: "manifestDraftPage", id: deliveryId, packageId, after,
|
|
112
|
+
}, control);
|
|
113
|
+
if (!page.entries.length)
|
|
114
|
+
throw new Error("Il checkpoint non contiene tutti i file precedenti.");
|
|
115
|
+
for (const row of page.entries) {
|
|
116
|
+
if (row.index >= checkpoint.fileIndex)
|
|
117
|
+
break;
|
|
118
|
+
if (row.index !== entries.length || row.entry.name !== safePath(deliveryRelativeName(files[row.index])) || row.entry.original.length !== files[row.index].size) {
|
|
119
|
+
throw new Error("La selezione non corrisponde al checkpoint.");
|
|
120
|
+
}
|
|
121
|
+
// Le varianti verranno riprese nella loro fase; i batch originali restano immutabili.
|
|
122
|
+
entries.push({ name: row.entry.name, type: row.entry.type, original: row.entry.original });
|
|
123
|
+
after = row.index;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let processed = checkpoint?.fileIndex || 0;
|
|
129
|
+
const entryByName = new Map();
|
|
130
|
+
let metadataBatch = [];
|
|
131
|
+
let metadataSize = 0;
|
|
132
|
+
let phase = "originals";
|
|
133
|
+
let sequence = checkpoint?.sequence || 0;
|
|
134
|
+
for (let i = 0; i < entries.length; i++)
|
|
135
|
+
entryByName.set(entries[i].name, i);
|
|
136
|
+
async function flushMetadata() {
|
|
137
|
+
if (!metadataBatch.length || options.manifestVersion !== 2)
|
|
138
|
+
return;
|
|
139
|
+
await deliveryJson("/admin/api/deliveries", { action: "manifestBatch", id: deliveryId, packageId,
|
|
140
|
+
phase, batchId: `${phase}-${sequence++}`, entries: metadataBatch }, control);
|
|
141
|
+
metadataBatch = [];
|
|
142
|
+
metadataSize = 0;
|
|
143
|
+
}
|
|
144
|
+
async function saveMetadata(index, entry) {
|
|
145
|
+
if (options.manifestVersion !== 2)
|
|
146
|
+
return;
|
|
147
|
+
const row = { index, entry };
|
|
148
|
+
const size = enc.encode(JSON.stringify(row)).length + 1;
|
|
149
|
+
if (metadataBatch.length >= 200 || metadataSize + size > 250 * 1024)
|
|
150
|
+
await flushMetadata();
|
|
151
|
+
metadataBatch.push(row);
|
|
152
|
+
metadataSize += size;
|
|
153
|
+
}
|
|
26
154
|
let currentOriginal = "";
|
|
27
|
-
const originalsSink = new B2Sink(deliveryId, packageId, "originals", reportOriginals, control);
|
|
28
|
-
|
|
155
|
+
const originalsSink = new B2Sink(deliveryId, packageId, "originals", reportOriginals, control, options.manifestVersion === 2);
|
|
156
|
+
sinks.push(originalsSink);
|
|
157
|
+
const originals = new ZipStream(originalsSink, options.manifestVersion === 2);
|
|
158
|
+
if (checkpoint) {
|
|
159
|
+
if (checkpoint.complete) {
|
|
160
|
+
originalsSink.totalBytes = checkpoint.offset;
|
|
161
|
+
originalsSink.uploadedBytes = checkpoint.offset;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await originalsSink.restore(checkpoint.offset, checkpoint.partsHash);
|
|
165
|
+
originals.restore(entries, files, checkpoint.offset);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let lastCheckpoint = checkpoint?.offset || 0;
|
|
169
|
+
async function saveCheckpoint(current, offset) {
|
|
170
|
+
if (options.manifestVersion !== 2 || offset - lastCheckpoint < PART * 4)
|
|
171
|
+
return;
|
|
172
|
+
await originalsSink.checkpointBoundary();
|
|
173
|
+
await flushMetadata();
|
|
174
|
+
await deliveryJson("/admin/api/deliveries", { action: "manifestCheckpoint", id: deliveryId, packageId,
|
|
175
|
+
checkpoint: { version: 1, fileIndex: processed, offset, sequence, current } }, control);
|
|
176
|
+
lastCheckpoint = offset;
|
|
177
|
+
}
|
|
29
178
|
function reportOriginals() {
|
|
30
179
|
onProgress({
|
|
31
180
|
phase: "originals", file: currentOriginal, doneFiles: processed, totalFiles: files.length,
|
|
@@ -34,17 +183,27 @@ export async function uploadDeliveryPackage(deliveryId, packageId, selected, opt
|
|
|
34
183
|
transferStartedAt: originalsSink.transferStartedAt || undefined,
|
|
35
184
|
});
|
|
36
185
|
}
|
|
37
|
-
for (const file of files) {
|
|
186
|
+
for (const file of files.slice(processed)) {
|
|
38
187
|
if (control)
|
|
39
188
|
await control.gate();
|
|
40
189
|
const name = safePath(deliveryRelativeName(file));
|
|
41
190
|
currentOriginal = name;
|
|
42
|
-
const member = await originals.add(name, file, reportOriginals, new Date(file.lastModified));
|
|
191
|
+
const member = await originals.add(name, file, reportOriginals, new Date(file.lastModified), options.manifestVersion === 2 ? saveCheckpoint : undefined, checkpoint && processed === checkpoint.fileIndex ? checkpoint.current : undefined);
|
|
192
|
+
entryByName.set(name, entries.length);
|
|
43
193
|
entries.push({ name, type: file.type || mimeFromName(name), original: member });
|
|
194
|
+
await saveMetadata(entries.length - 1, entries[entries.length - 1]);
|
|
44
195
|
processed++;
|
|
45
196
|
reportOriginals();
|
|
46
197
|
}
|
|
47
|
-
|
|
198
|
+
if (!checkpoint?.complete)
|
|
199
|
+
await originals.close();
|
|
200
|
+
await flushMetadata();
|
|
201
|
+
if (options.manifestVersion === 2 && !checkpoint?.complete) {
|
|
202
|
+
await deliveryJson("/admin/api/deliveries", { action: "manifestCheckpoint", id: deliveryId, packageId,
|
|
203
|
+
checkpoint: { version: 1, fileIndex: files.length, offset: originalsSink.totalBytes, sequence, complete: true } }, control);
|
|
204
|
+
}
|
|
205
|
+
phase = "variants";
|
|
206
|
+
sequence = 0;
|
|
48
207
|
const wantWebArchive = options.webEnabled || options.galleryEnabled;
|
|
49
208
|
let webSink = null;
|
|
50
209
|
let thumbsSink = null;
|
|
@@ -52,12 +211,14 @@ export async function uploadDeliveryPackage(deliveryId, packageId, selected, opt
|
|
|
52
211
|
let thumbsZip = null;
|
|
53
212
|
const skipped = [];
|
|
54
213
|
if (wantWebArchive) {
|
|
55
|
-
webSink = new B2Sink(deliveryId, packageId, "web", undefined, control);
|
|
56
|
-
|
|
214
|
+
webSink = new B2Sink(deliveryId, packageId, "web", undefined, control, options.manifestVersion === 2);
|
|
215
|
+
sinks.push(webSink);
|
|
216
|
+
webZip = new ZipStream(webSink, options.manifestVersion === 2);
|
|
57
217
|
}
|
|
58
218
|
if (options.galleryEnabled) {
|
|
59
|
-
thumbsSink = new B2Sink(deliveryId, packageId, "thumbs", undefined, control);
|
|
60
|
-
|
|
219
|
+
thumbsSink = new B2Sink(deliveryId, packageId, "thumbs", undefined, control, options.manifestVersion === 2);
|
|
220
|
+
sinks.push(thumbsSink);
|
|
221
|
+
thumbsZip = new ZipStream(thumbsSink, options.manifestVersion === 2);
|
|
61
222
|
}
|
|
62
223
|
if (webZip) {
|
|
63
224
|
let imageDone = 0;
|
|
@@ -66,24 +227,36 @@ export async function uploadDeliveryPackage(deliveryId, packageId, selected, opt
|
|
|
66
227
|
if (control)
|
|
67
228
|
await control.gate();
|
|
68
229
|
const sourceName = safePath(deliveryRelativeName(file));
|
|
230
|
+
let variants;
|
|
69
231
|
try {
|
|
70
|
-
|
|
71
|
-
const outputName = sourceName.replace(/\.[^.\/]+$/, "") + ".webp";
|
|
72
|
-
const modified = new Date(file.lastModified);
|
|
73
|
-
const web = await webZip.add(outputName, variants.display, undefined, modified);
|
|
74
|
-
const entry = entries.find((it) => it.name === sourceName);
|
|
75
|
-
if (entry) {
|
|
76
|
-
entry.web = web;
|
|
77
|
-
entry.width = variants.width;
|
|
78
|
-
entry.height = variants.height;
|
|
79
|
-
if (thumbsZip && variants.thumb) {
|
|
80
|
-
entry.thumb = await thumbsZip.add(outputName, variants.thumb, undefined, modified);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
232
|
+
variants = await makeVariants(file, !!thumbsZip, options.webEnabled ? options.webFormat : "webp");
|
|
83
233
|
}
|
|
84
234
|
catch (e) {
|
|
85
|
-
|
|
235
|
+
checkCancelled(control);
|
|
236
|
+
if (e instanceof UploadCancelled)
|
|
237
|
+
throw e;
|
|
238
|
+
skipped.push({ name: sourceName, reason: (e instanceof Error ? e.message : String(e)).slice(0, DELIVERY_SKIP_REASON_CHARS) });
|
|
239
|
+
const index = entryByName.get(sourceName);
|
|
240
|
+
await saveMetadata(index, { ...entries[index], skipped: skipped[skipped.length - 1].reason });
|
|
241
|
+
imageDone++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
await control?.gate();
|
|
245
|
+
const outputBase = options.manifestVersion === 2 ? sourceName : sourceName.replace(/\.[^.\/]+$/, "");
|
|
246
|
+
const outputName = outputBase + (options.webEnabled && options.webFormat === "jpg" ? ".jpg" : ".webp");
|
|
247
|
+
const modified = new Date(file.lastModified);
|
|
248
|
+
const web = await webZip.add(outputName, variants.display, undefined, modified);
|
|
249
|
+
const index = entryByName.get(sourceName);
|
|
250
|
+
const entry = entries[index];
|
|
251
|
+
if (entry) {
|
|
252
|
+
entry.web = web;
|
|
253
|
+
entry.width = variants.width;
|
|
254
|
+
entry.height = variants.height;
|
|
255
|
+
if (thumbsZip && variants.thumb) {
|
|
256
|
+
entry.thumb = await thumbsZip.add(outputBase + ".webp", variants.thumb, undefined, modified);
|
|
257
|
+
}
|
|
86
258
|
}
|
|
259
|
+
await saveMetadata(index, entry);
|
|
87
260
|
imageDone++;
|
|
88
261
|
onProgress({
|
|
89
262
|
phase: "derivatives", file: sourceName, doneFiles: imageDone,
|
|
@@ -101,6 +274,7 @@ export async function uploadDeliveryPackage(deliveryId, packageId, selected, opt
|
|
|
101
274
|
if (thumbsZip)
|
|
102
275
|
await thumbsZip.close();
|
|
103
276
|
}
|
|
277
|
+
await flushMetadata();
|
|
104
278
|
onProgress({
|
|
105
279
|
phase: "finalizing", file: "", doneFiles: files.length, totalFiles: files.length,
|
|
106
280
|
sourceBytes, uploadedBytes: originalsSink.uploadedBytes + (webSink?.uploadedBytes || 0) +
|
|
@@ -111,13 +285,14 @@ export async function uploadDeliveryPackage(deliveryId, packageId, selected, opt
|
|
|
111
285
|
transferStartedAt: firstStartedAt(originalsSink, webSink, thumbsSink),
|
|
112
286
|
});
|
|
113
287
|
return {
|
|
114
|
-
manifest: { version: 1, createdAt: new Date().toISOString(), entries, skipped },
|
|
288
|
+
manifest: { version: options.manifestVersion || 1, createdAt: new Date().toISOString(), entries: options.manifestVersion === 2 ? [] : entries, skipped },
|
|
115
289
|
fileCount: files.length,
|
|
116
290
|
sourceBytes,
|
|
117
291
|
storedBytes: originalsSink.totalBytes + (webSink?.totalBytes || 0) + (thumbsSink?.totalBytes || 0),
|
|
118
292
|
};
|
|
119
293
|
}
|
|
120
294
|
class B2Sink {
|
|
295
|
+
forceMultipart;
|
|
121
296
|
deliveryId;
|
|
122
297
|
packageId;
|
|
123
298
|
variant;
|
|
@@ -134,7 +309,8 @@ class B2Sink {
|
|
|
134
309
|
failure = null;
|
|
135
310
|
onTransfer;
|
|
136
311
|
control;
|
|
137
|
-
constructor(deliveryId, packageId, variant, onTransfer, control) {
|
|
312
|
+
constructor(deliveryId, packageId, variant, onTransfer, control, forceMultipart = false) {
|
|
313
|
+
this.forceMultipart = forceMultipart;
|
|
138
314
|
this.deliveryId = deliveryId;
|
|
139
315
|
this.packageId = packageId;
|
|
140
316
|
this.variant = variant;
|
|
@@ -144,6 +320,9 @@ class B2Sink {
|
|
|
144
320
|
async write(input) {
|
|
145
321
|
if (!input.byteLength)
|
|
146
322
|
return;
|
|
323
|
+
if (this.totalBytes + input.byteLength > DELIVERY_MAX_ARCHIVE_BYTES) {
|
|
324
|
+
throw new Error("L’archivio supera il limite tecnico: dividi i file in più invii.");
|
|
325
|
+
}
|
|
147
326
|
this.chunks.push(input);
|
|
148
327
|
this.buffered += input.byteLength;
|
|
149
328
|
this.totalBytes += input.byteLength;
|
|
@@ -151,47 +330,53 @@ class B2Sink {
|
|
|
151
330
|
await this.queuePart(this.take(PART));
|
|
152
331
|
}
|
|
153
332
|
async close() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
await this.drain();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
333
|
+
await this.control?.gate();
|
|
334
|
+
if (this.forceMultipart && !this.session)
|
|
335
|
+
await this.ensureSession();
|
|
160
336
|
if (!this.session) {
|
|
161
337
|
const body = this.take(this.buffered);
|
|
162
|
-
const signed = await postJson({
|
|
163
|
-
action: "putPresign", id: this.deliveryId, packageId: this.packageId, variant: this.variant,
|
|
164
|
-
});
|
|
165
338
|
this.startTransfer();
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
339
|
+
const etag = await deliveryPut(async () => {
|
|
340
|
+
const signed = await postJson({
|
|
341
|
+
action: "putPresign", id: this.deliveryId, packageId: this.packageId, variant: this.variant,
|
|
342
|
+
}, this.control);
|
|
343
|
+
return signed.url;
|
|
344
|
+
}, body.buffer, "application/zip", this.control);
|
|
345
|
+
await this.control?.gate();
|
|
171
346
|
await postJson({
|
|
172
347
|
action: "putComplete", id: this.deliveryId, packageId: this.packageId,
|
|
173
|
-
variant: this.variant, bytes: body.byteLength,
|
|
174
|
-
});
|
|
348
|
+
variant: this.variant, bytes: body.byteLength, etag,
|
|
349
|
+
}, this.control);
|
|
175
350
|
this.markUploaded(body.byteLength, true);
|
|
176
351
|
return;
|
|
177
352
|
}
|
|
178
353
|
if (this.buffered)
|
|
179
354
|
await this.queuePart(this.take(this.buffered));
|
|
180
355
|
await this.drain();
|
|
181
|
-
await
|
|
356
|
+
await this.control?.gate();
|
|
357
|
+
if (this.session.state === "completed") {
|
|
358
|
+
if (this.session.bytes !== this.totalBytes || this.priorParts.size !== this.partNumber) {
|
|
359
|
+
throw new Error("I file non corrispondono all’archivio già caricato. Crea un nuovo caricamento.");
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
await postJson({ action: "complete", uploadId: this.session.id,
|
|
364
|
+
bytes: this.totalBytes, partCount: this.partNumber }, this.control);
|
|
182
365
|
}
|
|
183
366
|
async ensureSession() {
|
|
184
367
|
if (this.session)
|
|
185
368
|
return this.session;
|
|
186
369
|
const out = await postJson({
|
|
187
370
|
action: "create", id: this.deliveryId, packageId: this.packageId, variant: this.variant,
|
|
188
|
-
});
|
|
371
|
+
}, this.control);
|
|
189
372
|
this.session = out.upload;
|
|
190
|
-
if (out.upload.state === "completed")
|
|
191
|
-
return out.upload;
|
|
192
373
|
const reconciled = await postJson({
|
|
193
374
|
action: "reconcile", uploadId: out.upload.id,
|
|
194
|
-
});
|
|
375
|
+
}, this.control);
|
|
376
|
+
if (reconciled.completed) {
|
|
377
|
+
this.session.state = "completed";
|
|
378
|
+
this.session.bytes = reconciled.bytes;
|
|
379
|
+
}
|
|
195
380
|
this.priorParts = new Map(reconciled.parts.map((part) => [part.partNumber, part]));
|
|
196
381
|
return out.upload;
|
|
197
382
|
}
|
|
@@ -217,69 +402,59 @@ class B2Sink {
|
|
|
217
402
|
}
|
|
218
403
|
}
|
|
219
404
|
async uploadPart(body, partNumber, session) {
|
|
220
|
-
if (session.state === "completed") {
|
|
221
|
-
this.markUploaded(body.byteLength, false);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
405
|
const digest = await digestHex(body);
|
|
225
406
|
const prior = this.priorParts.get(partNumber);
|
|
226
407
|
if (prior?.size === body.byteLength && prior.digest === digest) {
|
|
227
408
|
this.markUploaded(body.byteLength, false);
|
|
228
409
|
return;
|
|
229
410
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
233
|
-
let res = null;
|
|
234
|
-
let failure = "errore di rete o CORS";
|
|
235
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
236
|
-
this.startTransfer();
|
|
237
|
-
res = await fetch(signed.url, {
|
|
238
|
-
method: "PUT", body: body.buffer, headers: { "Content-Type": "application/octet-stream" },
|
|
239
|
-
}).catch(() => null);
|
|
240
|
-
if (res?.ok)
|
|
241
|
-
break;
|
|
242
|
-
if (res) {
|
|
243
|
-
const detail = (await res.text().catch(() => "")).replace(/\s+/g, " ").slice(0, 180);
|
|
244
|
-
failure = `HTTP ${res.status}${detail ? ` · ${detail}` : ""}`;
|
|
245
|
-
}
|
|
246
|
-
await delay(500 * 2 ** attempt);
|
|
247
|
-
}
|
|
248
|
-
if (!res?.ok) {
|
|
249
|
-
throw new Error(`Un pezzo del caricamento non è passato (${failure}). Riprova.`);
|
|
250
|
-
}
|
|
251
|
-
const etag = res.headers.get("etag");
|
|
252
|
-
if (!etag) {
|
|
253
|
-
throw new Error("L’archivio non espone l’header ETag: manca la configurazione CORS del bucket (vedi DELIVERY.md)");
|
|
254
|
-
}
|
|
255
|
-
let recorded = false;
|
|
256
|
-
for (let attempt = 0; attempt < 3 && !recorded; attempt++) {
|
|
257
|
-
try {
|
|
258
|
-
await postJson({
|
|
259
|
-
action: "record", uploadId: session.id, partNumber, etag,
|
|
260
|
-
digest, size: body.byteLength,
|
|
261
|
-
});
|
|
262
|
-
recorded = true;
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
if (attempt === 2)
|
|
266
|
-
throw new Error("Un pezzo del caricamento non è stato registrato. Riprova.");
|
|
267
|
-
await delay(400 * 2 ** attempt);
|
|
268
|
-
}
|
|
411
|
+
if (session.state === "completed") {
|
|
412
|
+
throw new Error("Non posso verificare gli stessi file nell’archivio completato. Crea un nuovo caricamento.");
|
|
269
413
|
}
|
|
414
|
+
this.startTransfer();
|
|
415
|
+
const etag = await deliveryPut(async () => {
|
|
416
|
+
const signed = await postJson({
|
|
417
|
+
action: "presign", uploadId: session.id, partNumber,
|
|
418
|
+
}, this.control);
|
|
419
|
+
return signed.url;
|
|
420
|
+
}, body.buffer, "application/octet-stream", this.control);
|
|
421
|
+
if (!etag)
|
|
422
|
+
throw new Error("L’archivio non espone ETag: verifica la configurazione CORS del bucket.");
|
|
423
|
+
await postJson({ action: "record", uploadId: session.id, partNumber, etag,
|
|
424
|
+
digest, size: body.byteLength }, this.control);
|
|
270
425
|
this.markUploaded(body.byteLength, true);
|
|
271
426
|
}
|
|
427
|
+
async restore(offset, partsHash) {
|
|
428
|
+
await this.ensureSession();
|
|
429
|
+
const count = offset / PART;
|
|
430
|
+
const parts = [...this.priorParts.values()].filter(p => p.partNumber <= count).sort((a, b) => a.partNumber - b.partNumber);
|
|
431
|
+
const hash = await digestHex(enc.encode(JSON.stringify(parts.map(p => [p.partNumber, p.size, p.digest]))));
|
|
432
|
+
if (parts.length !== count || hash !== partsHash)
|
|
433
|
+
throw new Error("Le parti remote non corrispondono al checkpoint. La ripresa è stata fermata per proteggere l’integrità.");
|
|
434
|
+
this.partNumber = count;
|
|
435
|
+
this.totalBytes = offset;
|
|
436
|
+
this.uploadedBytes = offset;
|
|
437
|
+
}
|
|
438
|
+
async gate() { await this.control?.gate(); }
|
|
439
|
+
async checkpointBoundary() {
|
|
440
|
+
await this.drain();
|
|
441
|
+
if (this.buffered)
|
|
442
|
+
throw new Error("Checkpoint fuori dal confine di una parte.");
|
|
443
|
+
}
|
|
444
|
+
async settle() { await Promise.allSettled([...this.inFlight]); }
|
|
272
445
|
async drain() {
|
|
273
|
-
|
|
446
|
+
while (this.inFlight.size) {
|
|
447
|
+
await Promise.race(this.inFlight);
|
|
448
|
+
await this.failIfNeeded();
|
|
449
|
+
}
|
|
274
450
|
await this.failIfNeeded();
|
|
275
451
|
}
|
|
276
452
|
async failIfNeeded() {
|
|
277
453
|
if (this.failure == null)
|
|
278
454
|
return;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.failure
|
|
282
|
-
throw error;
|
|
455
|
+
// Il finally del motore abortisce e attende le altre richieste. Attenderle qui
|
|
456
|
+
// bloccherebbe un errore permanente dietro ai retry di un'altra parte.
|
|
457
|
+
throw this.failure;
|
|
283
458
|
}
|
|
284
459
|
startTransfer() {
|
|
285
460
|
if (!this.transferStartedAt)
|
|
@@ -315,14 +490,28 @@ function firstStartedAt(...sinks) {
|
|
|
315
490
|
return values.length ? Math.min(...values) : undefined;
|
|
316
491
|
}
|
|
317
492
|
export class ZipStream {
|
|
493
|
+
utc;
|
|
318
494
|
sink;
|
|
319
495
|
offset = 0;
|
|
320
496
|
entries = [];
|
|
321
|
-
constructor(sink
|
|
322
|
-
|
|
497
|
+
constructor(sink, utc = false) {
|
|
498
|
+
this.utc = utc;
|
|
499
|
+
this.sink = sink;
|
|
500
|
+
}
|
|
501
|
+
restore(entries, files, offset) {
|
|
502
|
+
this.offset = offset;
|
|
503
|
+
this.entries = entries.map((entry, i) => {
|
|
504
|
+
const name = enc.encode(entry.name);
|
|
505
|
+
const size = entry.original.length;
|
|
506
|
+
const [time, date] = dosDate(new Date(files[i].lastModified), this.utc);
|
|
507
|
+
return { name, size, crc: entry.original.crc32, time, date,
|
|
508
|
+
localOffset: entry.original.offset - 30 - name.length - (size >= 0xffffffff ? 20 : 0) };
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async add(nameValue, blob, progress, modified = new Date(), checkpoint, resume) {
|
|
323
512
|
const name = enc.encode(safePath(nameValue));
|
|
324
513
|
const size = blob.size;
|
|
325
|
-
const localOffset = this.offset;
|
|
514
|
+
const localOffset = resume?.localOffset ?? this.offset;
|
|
326
515
|
const zip64 = size >= 0xffffffff;
|
|
327
516
|
const extra = zip64 ? zip64Extra([size, size]) : new Uint8Array();
|
|
328
517
|
const local = bytes(30 + name.length + extra.length);
|
|
@@ -331,7 +520,7 @@ export class ZipStream {
|
|
|
331
520
|
v.setUint16(4, zip64 ? 45 : 20, true);
|
|
332
521
|
v.setUint16(6, 0x0808, true); // descriptor + nomi UTF-8
|
|
333
522
|
v.setUint16(8, 0, true); // STORE
|
|
334
|
-
const [time, date] = dosDate(modified);
|
|
523
|
+
const [time, date] = dosDate(modified, this.utc);
|
|
335
524
|
v.setUint16(10, time, true);
|
|
336
525
|
v.setUint16(12, date, true);
|
|
337
526
|
v.setUint32(18, zip64 ? 0xffffffff : 0, true);
|
|
@@ -340,15 +529,42 @@ export class ZipStream {
|
|
|
340
529
|
v.setUint16(28, extra.length, true);
|
|
341
530
|
local.set(name, 30);
|
|
342
531
|
local.set(extra, 30 + name.length);
|
|
343
|
-
|
|
344
|
-
|
|
532
|
+
if (!resume)
|
|
533
|
+
await this.push(local);
|
|
534
|
+
const dataOffset = resume?.dataOffset ?? this.offset;
|
|
345
535
|
let crc = 0xffffffff;
|
|
346
536
|
let read = 0;
|
|
537
|
+
let chain = "";
|
|
538
|
+
if (resume && (resume.name !== nameValue || resume.size !== size || resume.time !== time || resume.date !== date ||
|
|
539
|
+
resume.read > size || resume.read < 0 || dataOffset + resume.read !== this.offset))
|
|
540
|
+
throw new Error("File diverso da quello del checkpoint.");
|
|
541
|
+
// Verifica il prefisso del solo file interrotto; gli originali già conclusi sono
|
|
542
|
+
// la copia remota autorevole. Non mescolare un nuovo suffisso a un vecchio prefisso.
|
|
543
|
+
while (resume && read < resume.read) {
|
|
544
|
+
const length = Math.min(READ, PART - ((dataOffset + read) % PART), resume.read - read);
|
|
545
|
+
const chunk = new Uint8Array(await blob.slice(read, read + length).arrayBuffer());
|
|
546
|
+
if (chunk.length !== length)
|
|
547
|
+
throw new Error("Lettura sorgente incompleta durante la verifica.");
|
|
548
|
+
crc = crc32Update(crc, chunk);
|
|
549
|
+
chain = await digestHex(enc.encode(chain + ":" + await digestHex(chunk)));
|
|
550
|
+
read += length;
|
|
551
|
+
// La verifica può essere lunga: il gate deve continuare a rispondere ad annullamento.
|
|
552
|
+
await this.sinkGate();
|
|
553
|
+
}
|
|
554
|
+
if (resume && (chain !== resume.chain || crc !== resume.crc))
|
|
555
|
+
throw new Error("Il file interrotto è cambiato: impossibile riprenderlo senza alterare i dati.");
|
|
347
556
|
while (read < size) {
|
|
348
|
-
const
|
|
557
|
+
const length = Math.min(READ, size - read, checkpoint ? PART - (this.offset % PART) : READ);
|
|
558
|
+
const chunk = new Uint8Array(await blob.slice(read, read + length).arrayBuffer());
|
|
559
|
+
if (chunk.length !== length)
|
|
560
|
+
throw new Error("Lettura sorgente incompleta.");
|
|
349
561
|
crc = crc32Update(crc, chunk);
|
|
562
|
+
if (checkpoint)
|
|
563
|
+
chain = await digestHex(enc.encode(chain + ":" + await digestHex(chunk)));
|
|
350
564
|
read += chunk.byteLength;
|
|
351
565
|
await this.push(chunk);
|
|
566
|
+
if (checkpoint && this.offset % PART === 0)
|
|
567
|
+
await checkpoint({ name: nameValue, size, read, crc, chain, localOffset, dataOffset, time, date }, this.offset);
|
|
352
568
|
progress?.(read);
|
|
353
569
|
}
|
|
354
570
|
crc = (crc ^ 0xffffffff) >>> 0;
|
|
@@ -424,20 +640,21 @@ export class ZipStream {
|
|
|
424
640
|
await this.push(end);
|
|
425
641
|
await this.sink.close();
|
|
426
642
|
}
|
|
643
|
+
async sinkGate() { await this.sink.gate?.(); }
|
|
427
644
|
async push(data) {
|
|
428
645
|
await this.sink.write(data);
|
|
429
646
|
this.offset += data.byteLength;
|
|
430
647
|
}
|
|
431
648
|
}
|
|
432
|
-
async function makeVariants(file, wantThumb) {
|
|
649
|
+
async function makeVariants(file, wantThumb, format = "webp") {
|
|
433
650
|
const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
|
|
434
651
|
try {
|
|
435
652
|
const displaySize = fit(bitmap.width, bitmap.height, 2048);
|
|
436
|
-
const display = await
|
|
653
|
+
const display = await drawImageBlob(bitmap, displaySize.width, displaySize.height, format === "jpg" ? .82 : .8, format === "jpg" ? "image/jpeg" : "image/webp");
|
|
437
654
|
let thumb;
|
|
438
655
|
if (wantThumb) {
|
|
439
656
|
const thumbSize = fit(bitmap.width, bitmap.height, 640);
|
|
440
|
-
thumb = await
|
|
657
|
+
thumb = await drawImageBlob(bitmap, thumbSize.width, thumbSize.height, .72);
|
|
441
658
|
}
|
|
442
659
|
return { display, thumb, width: displaySize.width, height: displaySize.height };
|
|
443
660
|
}
|
|
@@ -445,14 +662,21 @@ async function makeVariants(file, wantThumb) {
|
|
|
445
662
|
bitmap.close();
|
|
446
663
|
}
|
|
447
664
|
}
|
|
448
|
-
async function
|
|
665
|
+
async function drawImageBlob(bitmap, width, height, quality, type = "image/webp") {
|
|
449
666
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
450
667
|
const canvas = new OffscreenCanvas(width, height);
|
|
451
668
|
const ctx = canvas.getContext("2d", { alpha: false });
|
|
452
669
|
if (!ctx)
|
|
453
670
|
throw new Error("Canvas 2D non disponibile");
|
|
671
|
+
if (type === "image/jpeg") {
|
|
672
|
+
ctx.fillStyle = "#ffffff";
|
|
673
|
+
ctx.fillRect(0, 0, width, height);
|
|
674
|
+
}
|
|
454
675
|
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
455
|
-
|
|
676
|
+
const blob = await canvas.convertToBlob({ type, quality });
|
|
677
|
+
if (blob.type !== type)
|
|
678
|
+
throw new Error("Il browser non supporta il formato richiesto");
|
|
679
|
+
return blob;
|
|
456
680
|
}
|
|
457
681
|
const canvas = document.createElement("canvas");
|
|
458
682
|
canvas.width = width;
|
|
@@ -460,21 +684,19 @@ async function drawWebp(bitmap, width, height, quality) {
|
|
|
460
684
|
const ctx = canvas.getContext("2d", { alpha: false });
|
|
461
685
|
if (!ctx)
|
|
462
686
|
throw new Error("Canvas 2D non disponibile");
|
|
687
|
+
if (type === "image/jpeg") {
|
|
688
|
+
ctx.fillStyle = "#ffffff";
|
|
689
|
+
ctx.fillRect(0, 0, width, height);
|
|
690
|
+
}
|
|
463
691
|
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
464
|
-
return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("Conversione
|
|
692
|
+
return new Promise((resolve, reject) => canvas.toBlob((blob) => blob?.type === type ? resolve(blob) : reject(new Error("Conversione nel formato richiesto non riuscita")), type, quality));
|
|
465
693
|
}
|
|
466
694
|
function fit(width, height, edge) {
|
|
467
695
|
const scale = Math.min(1, edge / Math.max(width, height));
|
|
468
696
|
return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)) };
|
|
469
697
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
|
|
473
|
-
});
|
|
474
|
-
const answer = await res.json().catch(() => ({}));
|
|
475
|
-
if (!res.ok || answer.ok === false)
|
|
476
|
-
throw new Error(answer.error || `HTTP ${res.status}`);
|
|
477
|
-
return answer;
|
|
698
|
+
function postJson(body, control) {
|
|
699
|
+
return deliveryJson("/admin/api/delivery/multipart", body, control);
|
|
478
700
|
}
|
|
479
701
|
export function setDeliveryRelativePath(file, path) {
|
|
480
702
|
droppedPaths.set(file, safePath(path));
|
|
@@ -482,6 +704,60 @@ export function setDeliveryRelativePath(file, path) {
|
|
|
482
704
|
export function deliveryRelativeName(file) {
|
|
483
705
|
return droppedPaths.get(file) || file.webkitRelativePath || file.name;
|
|
484
706
|
}
|
|
707
|
+
/** Solo metadati: nessuna lettura dei file o richiesta di rete prima del rifiuto. */
|
|
708
|
+
export function preflightDeliverySelection(files, options = { galleryEnabled: true, webEnabled: true }) {
|
|
709
|
+
if (!files.length)
|
|
710
|
+
throw new Error("Nessun file selezionato");
|
|
711
|
+
if (options.manifestVersion === 2 && files.length > 100_000)
|
|
712
|
+
throw new Error("Troppi file: il limite è 100.000 per invio.");
|
|
713
|
+
let sourceBytes = 0;
|
|
714
|
+
// Stima conservativa ZIP64: extra locali/centrali e descriptor per ogni membro.
|
|
715
|
+
let archiveBytes = 98;
|
|
716
|
+
let manifestBytes = 128; // envelope, data ISO, delimitatori
|
|
717
|
+
const names = new Set();
|
|
718
|
+
const variantNames = new Set();
|
|
719
|
+
const member = { offset: DELIVERY_MAX_ARCHIVE_BYTES, length: DELIVERY_MAX_ARCHIVE_BYTES, crc32: 0xffffffff };
|
|
720
|
+
for (const file of files) {
|
|
721
|
+
validateDeliverySourceBytes(file.size);
|
|
722
|
+
sourceBytes = validateDeliverySourceBytes(sourceBytes + file.size);
|
|
723
|
+
const name = safePath(deliveryRelativeName(file));
|
|
724
|
+
if (names.has(name))
|
|
725
|
+
throw new Error("Due file hanno lo stesso percorso nell’archivio. Seleziona cartelle con percorsi distinti.");
|
|
726
|
+
names.add(name);
|
|
727
|
+
const nameBytes = enc.encode(name).length;
|
|
728
|
+
if (options.manifestVersion === 2 && (nameBytes > 8192 || /[\x00-\x1f\x7f]/.test(name)))
|
|
729
|
+
throw new Error("Un percorso supera i limiti del nuovo formato (8 KiB, senza caratteri di controllo).");
|
|
730
|
+
if (enc.encode(file.type || "").length > 256)
|
|
731
|
+
throw new Error("Il tipo di un file è troppo lungo.");
|
|
732
|
+
if ((options.galleryEnabled || options.webEnabled) && IMAGE.test(file.name)) {
|
|
733
|
+
const variantName = (options.manifestVersion === 2 ? name : name.replace(/\.[^.\/]+$/, "")) + (options.webEnabled && options.webFormat === "jpg" ? ".jpg" : ".webp");
|
|
734
|
+
if (variantNames.has(variantName))
|
|
735
|
+
throw new Error("Due immagini produrrebbero lo stesso nome di anteprima. Seleziona cartelle con percorsi distinti.");
|
|
736
|
+
variantNames.add(variantName);
|
|
737
|
+
}
|
|
738
|
+
const entry = { name, type: file.type || mimeFromName(name), original: member };
|
|
739
|
+
if ((options.galleryEnabled || options.webEnabled) && IMAGE.test(file.name)) {
|
|
740
|
+
entry.web = member;
|
|
741
|
+
if (options.galleryEnabled)
|
|
742
|
+
entry.thumb = member;
|
|
743
|
+
entry.width = Number.MAX_SAFE_INTEGER;
|
|
744
|
+
entry.height = Number.MAX_SAFE_INTEGER;
|
|
745
|
+
// Anche l'errore di conversione ha un budget; conteggiare entrambi è prudente.
|
|
746
|
+
manifestBytes += enc.encode(JSON.stringify({ name, reason: "\u0000".repeat(DELIVERY_SKIP_REASON_CHARS) })).length + 1;
|
|
747
|
+
}
|
|
748
|
+
manifestBytes += enc.encode(JSON.stringify(entry)).length + 1;
|
|
749
|
+
if (manifestBytes > (options.manifestVersion === 2 ? 64 * 1024 * 1024 : DELIVERY_V1_MANIFEST_BYTES)) {
|
|
750
|
+
throw new Error("Troppi metadati per questo formato di invio: dividi la selezione in più invii prima di caricare i file.");
|
|
751
|
+
}
|
|
752
|
+
if (nameBytes > 65_535)
|
|
753
|
+
throw new Error("Un percorso è troppo lungo per l’archivio ZIP: accorcialo prima di inviare.");
|
|
754
|
+
archiveBytes += file.size + 148 + 2 * nameBytes;
|
|
755
|
+
if (archiveBytes > DELIVERY_MAX_ARCHIVE_BYTES) {
|
|
756
|
+
throw new Error("I file e i loro percorsi superano il limite dell’archivio: dividi l’invio.");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return { sourceBytes, archiveBytes, manifestBytes };
|
|
760
|
+
}
|
|
485
761
|
export function deliverySelectionName(files) {
|
|
486
762
|
const first = files[0];
|
|
487
763
|
if (!first)
|
|
@@ -512,7 +788,7 @@ async function makeHeroVariant(file) {
|
|
|
512
788
|
try {
|
|
513
789
|
const size = fit(bitmap.width, bitmap.height, 2560);
|
|
514
790
|
return {
|
|
515
|
-
blob: await
|
|
791
|
+
blob: await drawImageBlob(bitmap, size.width, size.height, .82),
|
|
516
792
|
width: size.width, height: size.height,
|
|
517
793
|
};
|
|
518
794
|
}
|
|
@@ -520,9 +796,9 @@ async function makeHeroVariant(file) {
|
|
|
520
796
|
bitmap.close();
|
|
521
797
|
}
|
|
522
798
|
}
|
|
523
|
-
export async function deliverySourceFingerprint(files) {
|
|
799
|
+
export async function deliverySourceFingerprint(files, version = 1) {
|
|
524
800
|
const lines = [...files]
|
|
525
|
-
.sort((a, b) => deliveryRelativeName(a).localeCompare(deliveryRelativeName(b), undefined, {
|
|
801
|
+
.sort((a, b) => version === 2 ? (deliveryRelativeName(a) < deliveryRelativeName(b) ? -1 : deliveryRelativeName(a) > deliveryRelativeName(b) ? 1 : 0) : deliveryRelativeName(a).localeCompare(deliveryRelativeName(b), undefined, {
|
|
526
802
|
numeric: true, sensitivity: "base",
|
|
527
803
|
}))
|
|
528
804
|
.map((file) => `${safePath(deliveryRelativeName(file))}\0${file.size}\0${file.lastModified}`)
|
|
@@ -554,12 +830,14 @@ function zip64Extra(values) {
|
|
|
554
830
|
values.forEach((value, i) => setU64(v, 4 + i * 8, value));
|
|
555
831
|
return out;
|
|
556
832
|
}
|
|
557
|
-
function dosDate(date) {
|
|
558
|
-
const year = Math.max(1980, date.getFullYear());
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
833
|
+
function dosDate(date, utc = false) {
|
|
834
|
+
const year = Math.min(2107, Math.max(1980, utc ? date.getUTCFullYear() : date.getFullYear()));
|
|
835
|
+
const hour = utc ? date.getUTCHours() : date.getHours();
|
|
836
|
+
const minute = utc ? date.getUTCMinutes() : date.getMinutes();
|
|
837
|
+
const second = utc ? date.getUTCSeconds() : date.getSeconds();
|
|
838
|
+
const month = utc ? date.getUTCMonth() : date.getMonth();
|
|
839
|
+
const day = utc ? date.getUTCDate() : date.getDate();
|
|
840
|
+
return [(hour << 11) | (minute << 5) | Math.floor(second / 2), ((year - 1980) << 9) | ((month + 1) << 5) | day];
|
|
563
841
|
}
|
|
564
842
|
const CRC_TABLE = (() => {
|
|
565
843
|
const table = new Uint32Array(256);
|
|
@@ -577,7 +855,6 @@ function crc32Update(crc, data) {
|
|
|
577
855
|
c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
|
|
578
856
|
return c >>> 0;
|
|
579
857
|
}
|
|
580
|
-
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
581
858
|
async function digestHex(data) {
|
|
582
859
|
const digest = await crypto.subtle.digest("SHA-256", data.buffer);
|
|
583
860
|
return [...new Uint8Array(digest)]
|