@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
package/dist/app/app.css
CHANGED
|
@@ -1216,14 +1216,14 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
1216
1216
|
border-bottom: 1px solid var(--ak-line);
|
|
1217
1217
|
}
|
|
1218
1218
|
.dl-cover-head p { margin: 3px 0 0; color: var(--ak-ink-quiet); font-size: 11px; }
|
|
1219
|
-
.dl-cover-actions { display: flex; gap: 6px; flex: none; }
|
|
1220
|
-
.dl-cover-grid { padding: 14px; display: grid; grid-template-columns: repeat(auto-fill,minmax(
|
|
1219
|
+
.dl-cover-actions { display: flex; flex-wrap: wrap; gap: 6px; flex: none; }
|
|
1220
|
+
.dl-cover-grid { padding: 14px; display: grid; grid-template-columns: repeat(auto-fill,minmax(min(150px,100%),1fr)); align-items: start; gap: 10px; }
|
|
1221
1221
|
.dl-cover-grid button {
|
|
1222
1222
|
position: relative; min-width: 0; padding: 0; overflow: hidden; cursor: pointer;
|
|
1223
1223
|
border: 2px solid transparent; border-radius: 6px; background: var(--ak-bg);
|
|
1224
1224
|
}
|
|
1225
1225
|
.dl-cover-grid button.is-chosen { border-color: var(--ak-accent); }
|
|
1226
|
-
.dl-cover-grid img { width: 100%;
|
|
1226
|
+
.dl-cover-grid img { width: 100%; height: auto; display: block; object-fit: contain; }
|
|
1227
1227
|
.dl-cover-grid em {
|
|
1228
1228
|
position: absolute; top: 5px; right: 5px; width: 18px; height: 18px; display: grid; place-items: center;
|
|
1229
1229
|
border-radius: 50%; background: var(--ak-accent); color: #fff; font: 600 11px/1 var(--ak-font); font-style: normal;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "../theme.css";
|
|
2
|
+
import "./app.css";
|
|
3
|
+
import "./next.css";
|
|
4
|
+
export interface DeliveryAdminOptions {
|
|
5
|
+
name: string;
|
|
6
|
+
storagePrefix: string;
|
|
7
|
+
root?: HTMLElement | null;
|
|
8
|
+
websiteUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Delivery-only entry: no editorial, GitHub, booking or R2 hooks are mounted. */
|
|
11
|
+
export declare function mountDeliveryAdmin(options: DeliveryAdminOptions): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { StrictMode, useEffect, useState } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { setStoragePrefix } from "../index.js";
|
|
5
|
+
import { applyTheme, initialTheme, useTheme } from "./lib/theme.js";
|
|
6
|
+
import { useHashRoute } from "./lib/router.js";
|
|
7
|
+
import { useDeliveries } from "./lib/deliveries.js";
|
|
8
|
+
import { useUploadCompletions } from "./lib/upload-queue.js";
|
|
9
|
+
import { DeliveriesList, DeliveriesTotal, DeliveryWorkspace, NewDeliveryButton } from "./routes/Deliveries.js";
|
|
10
|
+
import { UploadDock } from "./routes/UploadDock.js";
|
|
11
|
+
import "../theme.css";
|
|
12
|
+
import "./app.css";
|
|
13
|
+
import "./next.css";
|
|
14
|
+
/** Delivery-only entry: no editorial, GitHub, booking or R2 hooks are mounted. */
|
|
15
|
+
export function mountDeliveryAdmin(options) {
|
|
16
|
+
setStoragePrefix(options.storagePrefix);
|
|
17
|
+
applyTheme(initialTheme());
|
|
18
|
+
const root = options.root ?? document.getElementById("root");
|
|
19
|
+
if (!root)
|
|
20
|
+
throw new Error("adminkit: elemento di montaggio non trovato");
|
|
21
|
+
createRoot(root).render(_jsx(StrictMode, { children: _jsx(DeliveryAdmin, { ...options }) }));
|
|
22
|
+
}
|
|
23
|
+
function DeliveryAdmin({ name, websiteUrl }) {
|
|
24
|
+
const [route, go] = useHashRoute();
|
|
25
|
+
const id = route[0] === "deliveries" ? route[1] : undefined;
|
|
26
|
+
const api = useDeliveries(id);
|
|
27
|
+
const { theme, toggle } = useTheme();
|
|
28
|
+
const completions = useUploadCompletions();
|
|
29
|
+
const [draftKey, setDraftKey] = useState(0);
|
|
30
|
+
const [selectedId, setSelectedId] = useState();
|
|
31
|
+
useEffect(() => { if (completions > 0)
|
|
32
|
+
void api.load(); }, [completions, api.load]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const prevent = (e) => e.preventDefault();
|
|
35
|
+
window.addEventListener("dragover", prevent);
|
|
36
|
+
window.addEventListener("drop", prevent);
|
|
37
|
+
return () => { window.removeEventListener("dragover", prevent); window.removeEventListener("drop", prevent); };
|
|
38
|
+
}, []);
|
|
39
|
+
const open = (value) => { setSelectedId(undefined); go(`/deliveries/${value}`); };
|
|
40
|
+
// Never render the previous transfer while the requested detail is loading.
|
|
41
|
+
const detail = id && api.detail?.id === id ? api.detail : null;
|
|
42
|
+
return _jsxs("div", { className: "app", children: [_jsxs("nav", { className: "rail", "aria-label": "Navigazione", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: name }), _jsx("span", { className: "rail-brand-role", children: "Consegne" })] }), _jsx("button", { className: "rail-item is-on", onClick: () => go("/deliveries"), children: "Transfer" }), _jsxs("div", { className: "rail-foot", children: [websiteUrl && _jsx("a", { className: "rail-item", href: websiteUrl, target: "_blank", rel: "noreferrer", children: "Sito dello studio" }), _jsx("button", { className: "rail-item", onClick: toggle, children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] })] }), _jsxs("aside", { className: "col", "aria-label": "Elenco transfer", children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Transfer" }), _jsx(NewDeliveryButton, { onCreate: () => { setSelectedId(undefined); setDraftKey(v => v + 1); go("/deliveries"); } })] }), _jsx("div", { className: "col-body", children: !api.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" }) : api.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
43
|
+
: _jsx(DeliveriesList, { rows: api.rows, current: id || selectedId, onOpen: open }) }), _jsx("div", { className: "dl-col-foot", children: api.rows && _jsx(DeliveriesTotal, { rows: api.rows }) })] }), _jsx("main", { className: "pane", children: _jsx("div", { className: "pane-body", children: api.error ? _jsxs("div", { role: "alert", children: [_jsx("p", { className: "ak-error", children: api.error }), _jsx("button", { className: "ak-btn", onClick: () => void api.refresh(), children: "Riprova" })] })
|
|
44
|
+
: id && !detail ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
45
|
+
: _jsx(DeliveryWorkspace, { delivery: detail, busy: api.busy, onAction: api.action, onRefresh: api.refresh, onReload: api.load, onRead: api.read, onDeleted: () => go("/deliveries"), onOpen: open, onSelected: setSelectedId }, id || `new-${draftKey}`) }) }), _jsx(UploadDock, {})] });
|
|
46
|
+
}
|
package/dist/app/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ComponentType } from "react";
|
|
1
2
|
import { type SiteAdminConfig } from "./site.js";
|
|
2
3
|
export type { SiteAdminConfig } from "./site.js";
|
|
3
4
|
export interface MountOptions {
|
|
@@ -5,6 +6,8 @@ export interface MountOptions {
|
|
|
5
6
|
site: SiteAdminConfig;
|
|
6
7
|
/** L'elemento in cui montare. Default: `#root`. */
|
|
7
8
|
root?: HTMLElement | null;
|
|
9
|
+
/** Site-owned editorial workspace, sharing the kit bootstrap and media library. */
|
|
10
|
+
workspace?: ComponentType;
|
|
8
11
|
/**
|
|
9
12
|
* Namespace delle preferenze salvate nel browser (ordinamento dei media, ultima cartella
|
|
10
13
|
* aperta nel selettore).
|
|
@@ -15,4 +18,4 @@ export interface MountOptions {
|
|
|
15
18
|
*/
|
|
16
19
|
storagePrefix: string;
|
|
17
20
|
}
|
|
18
|
-
export declare function mountAdmin({ site, root, storagePrefix }: MountOptions): void;
|
|
21
|
+
export declare function mountAdmin({ site, root, storagePrefix, workspace }: MountOptions): void;
|
package/dist/app/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { setStoragePrefix } from "../index.js";
|
|
|
18
18
|
import { App } from "./App.js";
|
|
19
19
|
import { applyTheme, initialTheme } from "./lib/theme.js";
|
|
20
20
|
import { setSiteConfig } from "./site.js";
|
|
21
|
-
export function mountAdmin({ site, root, storagePrefix }) {
|
|
21
|
+
export function mountAdmin({ site, root, storagePrefix, workspace }) {
|
|
22
22
|
setSiteConfig(site);
|
|
23
23
|
setStoragePrefix(storagePrefix);
|
|
24
24
|
/* SINCRONO, prima del render: se il tema lo applicasse solo l'effetto dentro `App`, ci sarebbe
|
|
@@ -29,5 +29,6 @@ export function mountAdmin({ site, root, storagePrefix }) {
|
|
|
29
29
|
const el = root ?? document.getElementById("root");
|
|
30
30
|
if (!el)
|
|
31
31
|
throw new Error("adminkit: elemento di montaggio non trovato (atteso #root)");
|
|
32
|
-
|
|
32
|
+
const Workspace = workspace ?? App;
|
|
33
|
+
createRoot(el).render(_jsx(StrictMode, { children: _jsx(Workspace, {}) }));
|
|
33
34
|
}
|
|
@@ -7,6 +7,8 @@ export interface DeliverySummary {
|
|
|
7
7
|
licenseNotes: string;
|
|
8
8
|
ctaEnabled: boolean;
|
|
9
9
|
ctaLabel: string;
|
|
10
|
+
downloadOriginalsLabel?: string;
|
|
11
|
+
downloadWebLabel?: string;
|
|
10
12
|
ctaUrl: string;
|
|
11
13
|
expiresAt: string | null;
|
|
12
14
|
filesExpiresAt: string | null;
|
|
@@ -21,12 +23,15 @@ export interface DeliverySummary {
|
|
|
21
23
|
downloadCount: number;
|
|
22
24
|
}
|
|
23
25
|
export interface DeliveryPackage {
|
|
26
|
+
manifestVersion?: 1 | 2;
|
|
27
|
+
nextCursor?: number | null;
|
|
24
28
|
id: string;
|
|
25
29
|
deliveryId: string;
|
|
26
30
|
label: string;
|
|
27
31
|
position: number;
|
|
28
32
|
galleryEnabled: boolean;
|
|
29
33
|
webEnabled: boolean;
|
|
34
|
+
webFormat?: "webp" | "jpg";
|
|
30
35
|
state: "uploading" | "ready" | "failed";
|
|
31
36
|
sourceFingerprint: string;
|
|
32
37
|
fileCount: number;
|
|
@@ -37,6 +42,8 @@ export interface DeliveryPackage {
|
|
|
37
42
|
images: DeliveryPackageImage[];
|
|
38
43
|
}
|
|
39
44
|
export interface DeliveryPackageImage {
|
|
45
|
+
width?: number;
|
|
46
|
+
height?: number;
|
|
40
47
|
index: number;
|
|
41
48
|
name: string;
|
|
42
49
|
previewUrl: string;
|
|
@@ -72,6 +79,7 @@ export interface DeliveryLink {
|
|
|
72
79
|
lastDownloadAt: string | null;
|
|
73
80
|
}
|
|
74
81
|
export interface Delivery extends DeliverySummary {
|
|
82
|
+
canShortenLinks?: boolean;
|
|
75
83
|
packages: DeliveryPackage[];
|
|
76
84
|
links: DeliveryLink[];
|
|
77
85
|
heroes: DeliveryHero[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Richieste recuperabili delle consegne. Usare retry solo per operazioni idempotenti. */
|
|
2
|
+
export declare class UploadCancelled extends Error {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
export interface UploadControl {
|
|
6
|
+
writerToken?: string;
|
|
7
|
+
gate(): Promise<void>;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
networkWaiting?(waiting: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class DeliveryRequestError extends Error {
|
|
12
|
+
readonly retryable: boolean;
|
|
13
|
+
readonly retryAfter: number;
|
|
14
|
+
constructor(message: string, retryable?: boolean, retryAfter?: number);
|
|
15
|
+
}
|
|
16
|
+
export declare function checkCancelled(control?: UploadControl): void;
|
|
17
|
+
export declare function deliveryGate(control?: UploadControl): Promise<void>;
|
|
18
|
+
export declare function waitForRetry(ms: number, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
export declare function retryDelivery<T>(operation: () => Promise<T>, control?: UploadControl, policy?: {
|
|
20
|
+
maxElapsedMs: number;
|
|
21
|
+
baseDelayMs: number;
|
|
22
|
+
}): Promise<T>;
|
|
23
|
+
export declare function deliveryJson<T>(url: string, body: Record<string, unknown>, control?: UploadControl, retry?: boolean): Promise<T>;
|
|
24
|
+
/** La callback viene richiamata a ogni tentativo: le firme scadute non vengono riusate. */
|
|
25
|
+
export declare function deliveryPut(sign: () => Promise<string>, body: BodyInit, contentType: string, control?: UploadControl): Promise<string>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** Richieste recuperabili delle consegne. Usare retry solo per operazioni idempotenti. */
|
|
2
|
+
export class UploadCancelled extends Error {
|
|
3
|
+
constructor() { super("Caricamento annullato"); this.name = "UploadCancelled"; }
|
|
4
|
+
}
|
|
5
|
+
export class DeliveryRequestError extends Error {
|
|
6
|
+
retryable;
|
|
7
|
+
retryAfter;
|
|
8
|
+
constructor(message, retryable = false, retryAfter = 0) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.retryable = retryable;
|
|
11
|
+
this.retryAfter = retryAfter;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function checkCancelled(control) {
|
|
15
|
+
if (control?.signal?.aborted)
|
|
16
|
+
throw new UploadCancelled();
|
|
17
|
+
}
|
|
18
|
+
export async function deliveryGate(control) {
|
|
19
|
+
checkCancelled(control);
|
|
20
|
+
if (!control)
|
|
21
|
+
return;
|
|
22
|
+
let abort;
|
|
23
|
+
try {
|
|
24
|
+
await Promise.race([
|
|
25
|
+
control.gate(),
|
|
26
|
+
new Promise((_, reject) => {
|
|
27
|
+
abort = () => reject(new UploadCancelled());
|
|
28
|
+
control.signal?.addEventListener("abort", abort, { once: true });
|
|
29
|
+
if (control.signal?.aborted)
|
|
30
|
+
abort();
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
checkCancelled(control);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (abort)
|
|
37
|
+
control.signal?.removeEventListener("abort", abort);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function waitForRetry(ms, signal) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
reject(new UploadCancelled());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const timer = setTimeout(() => { signal?.removeEventListener("abort", abort); resolve(); }, ms);
|
|
47
|
+
function abort() { clearTimeout(timer); reject(new UploadCancelled()); }
|
|
48
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function retryDelivery(operation, control, policy = { maxElapsedMs: 30 * 60_000, baseDelayMs: 1000 }) {
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
let waiting = false;
|
|
54
|
+
try {
|
|
55
|
+
for (let attempt = 0;; attempt++) {
|
|
56
|
+
checkCancelled(control);
|
|
57
|
+
await deliveryGate(control);
|
|
58
|
+
try {
|
|
59
|
+
const result = await operation();
|
|
60
|
+
checkCancelled(control);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
checkCancelled(control);
|
|
65
|
+
if (!(error instanceof DeliveryRequestError) || !error.retryable ||
|
|
66
|
+
Date.now() - started >= policy.maxElapsedMs)
|
|
67
|
+
throw error;
|
|
68
|
+
if (!waiting) {
|
|
69
|
+
waiting = true;
|
|
70
|
+
control?.networkWaiting?.(true);
|
|
71
|
+
}
|
|
72
|
+
const delay = error.retryAfter || Math.min(30_000, policy.baseDelayMs * 2 ** Math.min(attempt, 5));
|
|
73
|
+
await waitForRetry(delay + Math.floor(Math.random() * policy.baseDelayMs / 4), control?.signal);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (waiting)
|
|
79
|
+
control?.networkWaiting?.(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Il timeout comprende anche la lettura della risposta, non solo gli header. */
|
|
83
|
+
async function request(url, init, read, control, timeoutMs = 60_000) {
|
|
84
|
+
checkCancelled(control);
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const abort = () => controller.abort();
|
|
87
|
+
control?.signal?.addEventListener("abort", abort, { once: true });
|
|
88
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
89
|
+
try {
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await fetch(url, { ...init, signal: controller.signal });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
checkCancelled(control);
|
|
96
|
+
throw new DeliveryRequestError("Connessione interrotta. Attendo e riprovo…", true);
|
|
97
|
+
}
|
|
98
|
+
return await read(response);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
checkCancelled(control);
|
|
102
|
+
if (controller.signal.aborted)
|
|
103
|
+
throw new DeliveryRequestError("La richiesta non risponde. Riprovo…", true);
|
|
104
|
+
if (error instanceof TypeError)
|
|
105
|
+
throw new DeliveryRequestError("Risposta interrotta. Riprovo…", true);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
control?.signal?.removeEventListener("abort", abort);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function httpError(res, message, renewSignature = false) {
|
|
114
|
+
const transient = [408, 429, 500, 502, 503, 504].includes(res.status) || (renewSignature && res.status === 403);
|
|
115
|
+
const header = res.headers.get("retry-after");
|
|
116
|
+
const seconds = Number(header);
|
|
117
|
+
const retryAfter = header ? (Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - Date.now()) : 0;
|
|
118
|
+
return new DeliveryRequestError(message || `Richiesta non riuscita (HTTP ${res.status})`, transient, Number.isFinite(retryAfter) ? Math.max(0, Math.min(60_000, retryAfter)) : 0);
|
|
119
|
+
}
|
|
120
|
+
export function deliveryJson(url, body, control, retry = true) {
|
|
121
|
+
const operation = () => request(url, {
|
|
122
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(control?.writerToken ? { ...body, writerToken: control.writerToken } : body),
|
|
123
|
+
}, async (res) => {
|
|
124
|
+
let value;
|
|
125
|
+
try {
|
|
126
|
+
value = await res.json();
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
throw httpError(res);
|
|
131
|
+
if (error instanceof TypeError || res.headers.get("content-type")?.includes("application/json")) {
|
|
132
|
+
throw new DeliveryRequestError("La risposta si è interrotta. Riprovo…", true);
|
|
133
|
+
}
|
|
134
|
+
throw new DeliveryRequestError("Risposta non valida. Verifica l’accesso al pannello e riprendi il caricamento.");
|
|
135
|
+
}
|
|
136
|
+
if (!res.ok || value.ok !== true) {
|
|
137
|
+
const error = httpError(res, value.error);
|
|
138
|
+
throw value.retryable === false ? new DeliveryRequestError(error.message) : error;
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}, control);
|
|
142
|
+
return retry ? retryDelivery(operation, control) : operation();
|
|
143
|
+
}
|
|
144
|
+
/** La callback viene richiamata a ogni tentativo: le firme scadute non vengono riusate. */
|
|
145
|
+
export function deliveryPut(sign, body, contentType, control) {
|
|
146
|
+
return retryDelivery(async () => {
|
|
147
|
+
const url = await sign();
|
|
148
|
+
await deliveryGate(control);
|
|
149
|
+
return request(url, { method: "PUT", body, headers: { "Content-Type": contentType } }, async (res) => {
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
await res.body?.cancel();
|
|
152
|
+
throw httpError(res, undefined, true);
|
|
153
|
+
}
|
|
154
|
+
const etag = res.headers.get("etag") || "";
|
|
155
|
+
await res.body?.cancel();
|
|
156
|
+
return etag;
|
|
157
|
+
}, control, 30 * 60_000);
|
|
158
|
+
}, control);
|
|
159
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type UploadControl } from "./delivery-network.js";
|
|
2
|
+
export { UploadCancelled, type UploadControl } from "./delivery-network.js";
|
|
1
3
|
export interface DeliveryUploadProgress {
|
|
2
4
|
phase: "originals" | "derivatives" | "finalizing";
|
|
3
5
|
file: string;
|
|
@@ -20,7 +22,7 @@ export interface DeliveryManifestEntry {
|
|
|
20
22
|
height?: number;
|
|
21
23
|
}
|
|
22
24
|
export interface DeliveryManifest {
|
|
23
|
-
version: 1;
|
|
25
|
+
version: 1 | 2;
|
|
24
26
|
createdAt: string;
|
|
25
27
|
entries: DeliveryManifestEntry[];
|
|
26
28
|
skipped: {
|
|
@@ -33,9 +35,11 @@ interface ZipMember {
|
|
|
33
35
|
length: number;
|
|
34
36
|
crc32: number;
|
|
35
37
|
}
|
|
36
|
-
interface UploadOptions {
|
|
38
|
+
export interface UploadOptions {
|
|
39
|
+
manifestVersion?: 1 | 2;
|
|
37
40
|
galleryEnabled: boolean;
|
|
38
41
|
webEnabled: boolean;
|
|
42
|
+
webFormat?: "webp" | "jpg";
|
|
39
43
|
}
|
|
40
44
|
interface UploadResult {
|
|
41
45
|
manifest: DeliveryManifest;
|
|
@@ -43,28 +47,43 @@ interface UploadResult {
|
|
|
43
47
|
sourceBytes: number;
|
|
44
48
|
storedBytes: number;
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
interface MemberCheckpoint {
|
|
51
|
+
name: string;
|
|
52
|
+
size: number;
|
|
53
|
+
read: number;
|
|
54
|
+
crc: number;
|
|
55
|
+
chain: string;
|
|
56
|
+
localOffset: number;
|
|
57
|
+
dataOffset: number;
|
|
58
|
+
time: number;
|
|
59
|
+
date: number;
|
|
51
60
|
}
|
|
52
61
|
export declare function uploadDeliveryPackage(deliveryId: string, packageId: string, selected: File[], options: UploadOptions, onProgress: (progress: DeliveryUploadProgress) => void, control?: UploadControl): Promise<UploadResult>;
|
|
53
62
|
interface ZipSink {
|
|
63
|
+
gate?(): Promise<void>;
|
|
54
64
|
write(data: Uint8Array): Promise<void>;
|
|
55
65
|
close(): Promise<void>;
|
|
56
66
|
}
|
|
57
67
|
export declare class ZipStream {
|
|
68
|
+
private utc;
|
|
58
69
|
private sink;
|
|
59
70
|
private offset;
|
|
60
71
|
private entries;
|
|
61
|
-
constructor(sink: ZipSink);
|
|
62
|
-
|
|
72
|
+
constructor(sink: ZipSink, utc?: boolean);
|
|
73
|
+
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>;
|
|
63
75
|
close(): Promise<void>;
|
|
76
|
+
private sinkGate;
|
|
64
77
|
private push;
|
|
65
78
|
}
|
|
66
79
|
export declare function setDeliveryRelativePath(file: File, path: string): void;
|
|
67
80
|
export declare function deliveryRelativeName(file: File): string;
|
|
81
|
+
/** Solo metadati: nessuna lettura dei file o richiesta di rete prima del rifiuto. */
|
|
82
|
+
export declare function preflightDeliverySelection(files: File[], options?: UploadOptions): {
|
|
83
|
+
sourceBytes: number;
|
|
84
|
+
archiveBytes: number;
|
|
85
|
+
manifestBytes: number;
|
|
86
|
+
};
|
|
68
87
|
export declare function deliverySelectionName(files: File[]): string;
|
|
69
88
|
export declare function uploadDeliveryHeroObject(deliveryId: string, packageId: string, file: File): Promise<{
|
|
70
89
|
type: "object";
|
|
@@ -74,5 +93,4 @@ export declare function uploadDeliveryHeroObject(deliveryId: string, packageId:
|
|
|
74
93
|
height: number;
|
|
75
94
|
bytes: number;
|
|
76
95
|
}>;
|
|
77
|
-
export declare function deliverySourceFingerprint(files: File[]): Promise<string>;
|
|
78
|
-
export {};
|
|
96
|
+
export declare function deliverySourceFingerprint(files: File[], version?: 1 | 2): Promise<string>;
|