@cardinali-studio/adminkit 2.2.37 → 2.2.38
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
CHANGED
|
@@ -196,6 +196,14 @@ It is opinionated about the useful narrow case: Hugo content, React 19, Zod 4, C
|
|
|
196
196
|
|
|
197
197
|
This narrowness is intentional. A smaller system with explicit failure modes is easier to trust around content that people have edited by hand.
|
|
198
198
|
|
|
199
|
+
## Delivery ZIP compatibility (2.2.38)
|
|
200
|
+
|
|
201
|
+
New delivery archives use STORE members with their CRC and sizes in each local header, without data descriptors. This prevents streaming extractors from mistaking descriptor-shaped bytes inside a photograph for the end of the file. CRC preparation reads bounded 4 MiB slices and remains pausable and cancellable; source changes fail closed. ZIP64 sizes and offsets are promoted independently.
|
|
202
|
+
|
|
203
|
+
Persistent checkpoints record `store-header-v1`. Checkpoints from earlier releases continue with `store-descriptor-v1`, preserving confirmed multipart bytes and derivative retries. The update affects newly generated archives; it does not rewrite existing stored ZIPs. Consumers must update the dependency, rebuild and redeploy.
|
|
204
|
+
|
|
205
|
+
The regression includes 208 synthetic members with the collision at member 53, streaming and indexed readers, ZIP64 offsets, and byte-identical resume against fixtures from 2.2.37.
|
|
206
|
+
|
|
199
207
|
## Development
|
|
200
208
|
|
|
201
209
|
```sh
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type UploadControl } from "./delivery-network.js";
|
|
2
2
|
export { UploadCancelled, type UploadControl } from "./delivery-network.js";
|
|
3
|
+
declare const ZIP_FORMAT = "store-header-v1";
|
|
4
|
+
declare const LEGACY_ZIP_FORMAT = "store-descriptor-v1";
|
|
5
|
+
type ZipFormat = typeof ZIP_FORMAT | typeof LEGACY_ZIP_FORMAT;
|
|
3
6
|
export interface DeliveryUploadProgress {
|
|
4
7
|
phase: "originals" | "derivatives" | "finalizing";
|
|
5
8
|
file: string;
|
|
@@ -57,6 +60,8 @@ interface MemberCheckpoint {
|
|
|
57
60
|
dataOffset: number;
|
|
58
61
|
time: number;
|
|
59
62
|
date: number;
|
|
63
|
+
zipFormat?: ZipFormat;
|
|
64
|
+
headerCrc?: number;
|
|
60
65
|
}
|
|
61
66
|
export declare function uploadDeliveryPackage(deliveryId: string, packageId: string, selected: File[], options: UploadOptions, onProgress: (progress: DeliveryUploadProgress) => void, control?: UploadControl): Promise<UploadResult>;
|
|
62
67
|
interface ZipSink {
|
|
@@ -69,9 +74,10 @@ export declare class ZipStream {
|
|
|
69
74
|
private sink;
|
|
70
75
|
private offset;
|
|
71
76
|
private entries;
|
|
72
|
-
|
|
77
|
+
private format;
|
|
78
|
+
constructor(sink: ZipSink, utc?: boolean, format?: ZipFormat);
|
|
73
79
|
restore(entries: DeliveryManifestEntry[], files: File[], offset: number): void;
|
|
74
|
-
add(nameValue: string, blob: Blob, progress?: (read: number) => void, modified?: Date, checkpoint?: (current: MemberCheckpoint, offset: number) => Promise<void>, resume?: MemberCheckpoint): Promise<ZipMember>;
|
|
80
|
+
add(nameValue: string, blob: Blob, progress?: (read: number, preparing?: boolean) => void, modified?: Date, checkpoint?: (current: MemberCheckpoint, offset: number) => Promise<void>, resume?: MemberCheckpoint): Promise<ZipMember>;
|
|
75
81
|
close(): Promise<void>;
|
|
76
82
|
private sinkGate;
|
|
77
83
|
private push;
|
|
@@ -14,6 +14,8 @@ const READ = 4 * 1024 * 1024;
|
|
|
14
14
|
const UPLOAD_CONCURRENCY = 4;
|
|
15
15
|
const IMAGE = /\.(jpe?g|png|webp)$/i;
|
|
16
16
|
const enc = new TextEncoder();
|
|
17
|
+
const ZIP_FORMAT = "store-header-v1";
|
|
18
|
+
const LEGACY_ZIP_FORMAT = "store-descriptor-v1";
|
|
17
19
|
const droppedPaths = new WeakMap();
|
|
18
20
|
/* Il cancello di pausa e annullamento.
|
|
19
21
|
*
|
|
@@ -154,7 +156,9 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
|
|
|
154
156
|
let currentOriginal = "";
|
|
155
157
|
const originalsSink = new B2Sink(deliveryId, packageId, "originals", reportOriginals, control, options.manifestVersion === 2);
|
|
156
158
|
sinks.push(originalsSink);
|
|
157
|
-
|
|
159
|
+
// Confirmed multipart prefixes and derivative retries keep their original layout.
|
|
160
|
+
const zipFormat = checkpoint ? (checkpoint.current?.zipFormat || LEGACY_ZIP_FORMAT) : ZIP_FORMAT;
|
|
161
|
+
const originals = new ZipStream(originalsSink, options.manifestVersion === 2, zipFormat);
|
|
158
162
|
if (checkpoint) {
|
|
159
163
|
if (checkpoint.complete) {
|
|
160
164
|
originalsSink.totalBytes = checkpoint.offset;
|
|
@@ -175,12 +179,13 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
|
|
|
175
179
|
checkpoint: { version: 1, fileIndex: processed, offset, sequence, current } }, control);
|
|
176
180
|
lastCheckpoint = offset;
|
|
177
181
|
}
|
|
178
|
-
function reportOriginals() {
|
|
182
|
+
function reportOriginals(_read, preparing = false) {
|
|
179
183
|
onProgress({
|
|
180
184
|
phase: "originals", file: currentOriginal, doneFiles: processed, totalFiles: files.length,
|
|
181
185
|
sourceBytes, uploadedBytes: originalsSink.uploadedBytes,
|
|
182
186
|
transferredBytes: originalsSink.transferredBytes, startedAt,
|
|
183
187
|
transferStartedAt: originalsSink.transferStartedAt || undefined,
|
|
188
|
+
note: preparing ? "Verifico il file prima del caricamento…" : undefined,
|
|
184
189
|
});
|
|
185
190
|
}
|
|
186
191
|
for (const file of files.slice(processed)) {
|
|
@@ -188,7 +193,7 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
|
|
|
188
193
|
await control.gate();
|
|
189
194
|
const name = safePath(deliveryRelativeName(file));
|
|
190
195
|
currentOriginal = name;
|
|
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);
|
|
196
|
+
const member = await originals.add(name, file, reportOriginals, new Date(file.lastModified), options.manifestVersion === 2 ? saveCheckpoint : undefined, checkpoint && !checkpoint.complete && processed === checkpoint.fileIndex ? checkpoint.current : undefined);
|
|
192
197
|
entryByName.set(name, entries.length);
|
|
193
198
|
entries.push({ name, type: file.type || mimeFromName(name), original: member });
|
|
194
199
|
await saveMetadata(entries.length - 1, entries[entries.length - 1]);
|
|
@@ -200,7 +205,7 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
|
|
|
200
205
|
await flushMetadata();
|
|
201
206
|
if (options.manifestVersion === 2 && !checkpoint?.complete) {
|
|
202
207
|
await deliveryJson("/admin/api/deliveries", { action: "manifestCheckpoint", id: deliveryId, packageId,
|
|
203
|
-
checkpoint: { version: 1, fileIndex: files.length, offset: originalsSink.totalBytes, sequence, complete: true } }, control);
|
|
208
|
+
checkpoint: { version: 1, fileIndex: files.length, offset: originalsSink.totalBytes, sequence, complete: true, current: { zipFormat } } }, control);
|
|
204
209
|
}
|
|
205
210
|
phase = "variants";
|
|
206
211
|
sequence = 0;
|
|
@@ -213,12 +218,12 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
|
|
|
213
218
|
if (wantWebArchive) {
|
|
214
219
|
webSink = new B2Sink(deliveryId, packageId, "web", undefined, control, options.manifestVersion === 2);
|
|
215
220
|
sinks.push(webSink);
|
|
216
|
-
webZip = new ZipStream(webSink, options.manifestVersion === 2);
|
|
221
|
+
webZip = new ZipStream(webSink, options.manifestVersion === 2, zipFormat);
|
|
217
222
|
}
|
|
218
223
|
if (options.galleryEnabled) {
|
|
219
224
|
thumbsSink = new B2Sink(deliveryId, packageId, "thumbs", undefined, control, options.manifestVersion === 2);
|
|
220
225
|
sinks.push(thumbsSink);
|
|
221
|
-
thumbsZip = new ZipStream(thumbsSink, options.manifestVersion === 2);
|
|
226
|
+
thumbsZip = new ZipStream(thumbsSink, options.manifestVersion === 2, zipFormat);
|
|
222
227
|
}
|
|
223
228
|
if (webZip) {
|
|
224
229
|
let imageDone = 0;
|
|
@@ -494,9 +499,13 @@ export class ZipStream {
|
|
|
494
499
|
sink;
|
|
495
500
|
offset = 0;
|
|
496
501
|
entries = [];
|
|
497
|
-
|
|
502
|
+
format;
|
|
503
|
+
constructor(sink, utc = false, format = ZIP_FORMAT) {
|
|
504
|
+
if (![ZIP_FORMAT, LEGACY_ZIP_FORMAT].includes(format))
|
|
505
|
+
throw new Error("Formato del checkpoint ZIP non supportato.");
|
|
498
506
|
this.utc = utc;
|
|
499
507
|
this.sink = sink;
|
|
508
|
+
this.format = format;
|
|
500
509
|
}
|
|
501
510
|
restore(entries, files, offset) {
|
|
502
511
|
this.offset = offset;
|
|
@@ -509,22 +518,46 @@ export class ZipStream {
|
|
|
509
518
|
});
|
|
510
519
|
}
|
|
511
520
|
async add(nameValue, blob, progress, modified = new Date(), checkpoint, resume) {
|
|
521
|
+
await this.sinkGate();
|
|
512
522
|
const name = enc.encode(safePath(nameValue));
|
|
513
523
|
const size = blob.size;
|
|
514
524
|
const localOffset = resume?.localOffset ?? this.offset;
|
|
515
525
|
const zip64 = size >= 0xffffffff;
|
|
526
|
+
const legacy = this.format === LEGACY_ZIP_FORMAT;
|
|
527
|
+
if (resume && (resume.zipFormat || LEGACY_ZIP_FORMAT) !== this.format)
|
|
528
|
+
throw new Error("Formato del checkpoint ZIP diverso dal caricamento.");
|
|
529
|
+
// STORE has a known size, but needs a CRC before its local header can
|
|
530
|
+
// be self-contained. Two bounded reads avoid buffering whole photos.
|
|
531
|
+
let headerCrc = 0;
|
|
532
|
+
if (!legacy) {
|
|
533
|
+
let value = 0xffffffff;
|
|
534
|
+
progress?.(0, true);
|
|
535
|
+
for (let offset = 0; offset < size; offset += READ) {
|
|
536
|
+
await this.sinkGate();
|
|
537
|
+
const length = Math.min(READ, size - offset);
|
|
538
|
+
const chunk = new Uint8Array(await blob.slice(offset, offset + length).arrayBuffer());
|
|
539
|
+
if (chunk.length !== length)
|
|
540
|
+
throw new Error("Lettura sorgente incompleta durante la verifica.");
|
|
541
|
+
value = crc32Update(value, chunk);
|
|
542
|
+
progress?.(offset + length, true);
|
|
543
|
+
}
|
|
544
|
+
headerCrc = (value ^ 0xffffffff) >>> 0;
|
|
545
|
+
if (resume && resume.headerCrc !== headerCrc)
|
|
546
|
+
throw new Error("Il file è cambiato: il checksum non corrisponde al checkpoint.");
|
|
547
|
+
}
|
|
516
548
|
const extra = zip64 ? zip64Extra([size, size]) : new Uint8Array();
|
|
517
549
|
const local = bytes(30 + name.length + extra.length);
|
|
518
550
|
const v = view(local);
|
|
519
551
|
v.setUint32(0, 0x04034b50, true);
|
|
520
552
|
v.setUint16(4, zip64 ? 45 : 20, true);
|
|
521
|
-
v.setUint16(6, 0x0808, true); // descriptor
|
|
553
|
+
v.setUint16(6, legacy ? 0x0808 : 0x0800, true); // UTF-8; descriptor only for legacy checkpoints
|
|
522
554
|
v.setUint16(8, 0, true); // STORE
|
|
523
555
|
const [time, date] = dosDate(modified, this.utc);
|
|
524
556
|
v.setUint16(10, time, true);
|
|
525
557
|
v.setUint16(12, date, true);
|
|
526
|
-
v.setUint32(
|
|
527
|
-
v.setUint32(
|
|
558
|
+
v.setUint32(14, legacy ? 0 : headerCrc, true);
|
|
559
|
+
v.setUint32(18, zip64 ? 0xffffffff : legacy ? 0 : size, true);
|
|
560
|
+
v.setUint32(22, zip64 ? 0xffffffff : legacy ? 0 : size, true);
|
|
528
561
|
v.setUint16(26, name.length, true);
|
|
529
562
|
v.setUint16(28, extra.length, true);
|
|
530
563
|
local.set(name, 30);
|
|
@@ -564,46 +597,56 @@ export class ZipStream {
|
|
|
564
597
|
read += chunk.byteLength;
|
|
565
598
|
await this.push(chunk);
|
|
566
599
|
if (checkpoint && this.offset % PART === 0)
|
|
567
|
-
await checkpoint({ name: nameValue, size, read, crc, chain, localOffset, dataOffset, time, date
|
|
600
|
+
await checkpoint({ name: nameValue, size, read, crc, chain, localOffset, dataOffset, time, date,
|
|
601
|
+
zipFormat: this.format, ...(!legacy ? { headerCrc } : {}) }, this.offset);
|
|
568
602
|
progress?.(read);
|
|
569
603
|
}
|
|
570
604
|
crc = (crc ^ 0xffffffff) >>> 0;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
605
|
+
if (!legacy && crc !== headerCrc)
|
|
606
|
+
throw new Error("Il file è cambiato durante il caricamento. Riseleziona i file e riprova.");
|
|
607
|
+
if (legacy) {
|
|
608
|
+
const descriptor = bytes(zip64 ? 24 : 16);
|
|
609
|
+
const d = view(descriptor);
|
|
610
|
+
d.setUint32(0, 0x08074b50, true);
|
|
611
|
+
d.setUint32(4, crc, true);
|
|
612
|
+
if (zip64) {
|
|
613
|
+
setU64(d, 8, size);
|
|
614
|
+
setU64(d, 16, size);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
d.setUint32(8, size, true);
|
|
618
|
+
d.setUint32(12, size, true);
|
|
619
|
+
}
|
|
620
|
+
await this.push(descriptor);
|
|
582
621
|
}
|
|
583
|
-
await this.push(descriptor);
|
|
584
622
|
this.entries.push({ name, size, crc, localOffset, time, date });
|
|
585
623
|
return { offset: dataOffset, length: size, crc32: crc };
|
|
586
624
|
}
|
|
587
625
|
async close() {
|
|
588
626
|
const centralOffset = this.offset;
|
|
589
627
|
for (const entry of this.entries) {
|
|
590
|
-
const
|
|
591
|
-
const
|
|
628
|
+
const legacy = this.format === LEGACY_ZIP_FORMAT;
|
|
629
|
+
const largeSize = entry.size >= 0xffffffff;
|
|
630
|
+
const largeOffset = entry.localOffset >= 0xffffffff;
|
|
631
|
+
const large = largeSize || largeOffset;
|
|
632
|
+
const values = legacy && large ? [entry.size, entry.size, entry.localOffset]
|
|
633
|
+
: [...(largeSize ? [entry.size, entry.size] : []), ...(largeOffset ? [entry.localOffset] : [])];
|
|
634
|
+
const extra = values.length ? zip64Extra(values) : new Uint8Array();
|
|
592
635
|
const central = bytes(46 + entry.name.length + extra.length);
|
|
593
636
|
const v = view(central);
|
|
594
637
|
v.setUint32(0, 0x02014b50, true);
|
|
595
638
|
v.setUint16(4, 45, true);
|
|
596
639
|
v.setUint16(6, large ? 45 : 20, true);
|
|
597
|
-
v.setUint16(8, 0x0808, true);
|
|
640
|
+
v.setUint16(8, legacy ? 0x0808 : 0x0800, true);
|
|
598
641
|
v.setUint16(10, 0, true);
|
|
599
642
|
v.setUint16(12, entry.time, true);
|
|
600
643
|
v.setUint16(14, entry.date, true);
|
|
601
644
|
v.setUint32(16, entry.crc, true);
|
|
602
|
-
v.setUint32(20, large ? 0xffffffff : entry.size, true);
|
|
603
|
-
v.setUint32(24, large ? 0xffffffff : entry.size, true);
|
|
645
|
+
v.setUint32(20, largeSize || (legacy && large) ? 0xffffffff : entry.size, true);
|
|
646
|
+
v.setUint32(24, largeSize || (legacy && large) ? 0xffffffff : entry.size, true);
|
|
604
647
|
v.setUint16(28, entry.name.length, true);
|
|
605
648
|
v.setUint16(30, extra.length, true);
|
|
606
|
-
v.setUint32(42, large ? 0xffffffff : entry.localOffset, true);
|
|
649
|
+
v.setUint32(42, largeOffset || (legacy && large) ? 0xffffffff : entry.localOffset, true);
|
|
607
650
|
central.set(entry.name, 46);
|
|
608
651
|
central.set(extra, 46 + entry.name.length);
|
|
609
652
|
await this.push(central);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinali-studio/adminkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
|
|
6
6
|
"license": "0BSD",
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
"react-dom": "^19.2.8",
|
|
105
105
|
"react-hook-form": "^7.85.0",
|
|
106
106
|
"typescript": "^5.9.3",
|
|
107
|
+
"unzipper": "0.12.5",
|
|
107
108
|
"wrangler": "4.114.0",
|
|
108
109
|
"zod": "^4.4.3"
|
|
109
110
|
},
|