@better-s3/react 3.1045.1 → 3.1045.2

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/index.js CHANGED
@@ -1,2 +1,1023 @@
1
- import {validateFile}from'@better-s3/server';export{createS3Api as createPresignApi,createS3Api,validateFile}from'@better-s3/server';import {useState,useRef,useCallback}from'react';function K(e,a){if(!e)return a;let o=e.match(/filename\*=UTF-8''([^;,\s]+)/i);if(o)try{return decodeURIComponent(o[1])}catch{}let d=e.match(/filename="([^"]+)"/i);return d?d[1]:a}function ee(e){if(e===0)return "0 B";let a=["B","KB","MB","GB","TB"],o=Math.floor(Math.log(e)/Math.log(1024));return `${(e/Math.pow(1024,o)).toFixed(o===0?0:1)} ${a[o]}`}async function R(e,a,o){let d;for(let s=0;s<=a;s++)try{return await e()}catch(g){if(g.name==="AbortError")throw g;if(d=g,s<a){let c=1e3*2**s;if(await new Promise(t=>setTimeout(t,c)),o?.aborted)throw new DOMException("Upload aborted","AbortError")}}throw d}function B(e,a,o,d,s){return new Promise((g,c)=>{let t=new XMLHttpRequest,n=()=>{t.abort(),c(new DOMException("Upload aborted","AbortError"));};s?.addEventListener("abort",n,{once:true}),t.upload.addEventListener("progress",i=>{i.lengthComputable&&d?.({loaded:i.loaded,total:i.total,percent:Math.round(i.loaded/i.total*100)});}),t.addEventListener("load",()=>{s?.removeEventListener("abort",n),t.status>=200&&t.status<300?(d?.({loaded:e.size,total:e.size,percent:100}),g()):c(new Error(`Upload failed: ${t.status} ${t.statusText}`));}),t.addEventListener("error",()=>{s?.removeEventListener("abort",n),c(new Error("Upload failed: network error"));}),t.addEventListener("abort",()=>{s?.removeEventListener("abort",n),c(new DOMException("Upload aborted","AbortError"));});let p=new FormData;for(let[i,r]of Object.entries(o))p.append(i,r);p.append("file",e),t.open("POST",a),t.send(p);})}function q(e,a,o,d,s){return new Promise((g,c)=>{let t=new XMLHttpRequest,n=()=>{t.abort(),c(new DOMException("Upload aborted","AbortError"));};s?.addEventListener("abort",n,{once:true}),t.upload.addEventListener("progress",p=>{p.lengthComputable&&d?.({loaded:p.loaded,total:p.total,percent:Math.round(p.loaded/p.total*100)});}),t.addEventListener("load",()=>{s?.removeEventListener("abort",n),t.status>=200&&t.status<300?(d?.({loaded:e.size,total:e.size,percent:100}),g()):c(new Error(`Upload failed: ${t.status} ${t.statusText}`));}),t.addEventListener("error",()=>{s?.removeEventListener("abort",n),c(new Error("Upload failed: network error"));}),t.addEventListener("abort",()=>{s?.removeEventListener("abort",n),c(new DOMException("Upload aborted","AbortError"));}),t.open("PUT",a);for(let[p,i]of Object.entries(o))t.setRequestHeader(p,i);t.send(e);})}function G(e,a,o,d,s,g){return new Promise((c,t)=>{let n=new XMLHttpRequest,p=()=>{n.abort(),t(new DOMException("Upload aborted","AbortError"));};g?.addEventListener("abort",p,{once:true}),n.upload.addEventListener("progress",i=>{i.lengthComputable&&(o.bytes=i.loaded,s());}),n.addEventListener("load",()=>{g?.removeEventListener("abort",p),n.status>=200&&n.status<300?(o.bytes=e.size,s(),c()):t(new Error(`Part upload failed: ${n.status}`));}),n.addEventListener("error",()=>{g?.removeEventListener("abort",p),t(new Error("Part upload failed: network error"));}),n.addEventListener("abort",()=>{g?.removeEventListener("abort",p),t(new DOMException("Upload aborted","AbortError"));}),n.open("PUT",a),n.send(e);})}async function X(e,a,o,d,s,g,c,t){let n=t?.contentType??a.type,{uploadId:p,key:i}=await e.multipart.init({key:o,contentType:n,fileSize:a.size,fileName:t?.fileName!==null?t?.fileName??a.name:void 0,metadata:t?.metadata,bucket:t?.bucket,acl:t?.acl}),r=Math.ceil(a.size/d),U=[],l=Array.from({length:r},()=>({bytes:0})),u=()=>{let f=l.reduce((m,h)=>m+h.bytes,0);g?.({loaded:f,total:a.size,percent:Math.round(f/a.size*100)});};try{for(let m=0;m<r;m+=s){if(c?.aborted)throw new DOMException("Upload aborted","AbortError");let h=Math.min(m+s,r),P=[];for(let w=m;w<h;w++){let S=w*d,F=Math.min(S+d,a.size),y=a.slice(S,F),v=w+1;P.push(R(async()=>{let{presignedUrl:O}=await e.multipart.signPart({key:i,uploadId:p,partNumber:v,partSize:y.size,bucket:t?.bucket});return l[w].bytes=0,await G(y,O,l[w],a.size,u,c),{partNumber:v}},3,c));}let b=await Promise.all(P);U.push(...b);}U.sort((m,h)=>m.partNumber-h.partNumber);let f=await e.multipart.complete({key:i,uploadId:p,parts:U,bucket:t?.bucket});return g?.({loaded:a.size,total:a.size,percent:100}),f.eTag}catch(f){throw e.multipart.abort({key:i,uploadId:p,bucket:t?.bucket}).catch(()=>{}),f}}async function E(e,a,o,d={},s={},g,c){let t=d.multipartThreshold??31457280,n=d.multipart===true&&a.size>=t,p=d.concurrentParts??3,i=c?.contentType??a.type,r;return n?(s.onPhaseChange?.("uploading"),r=await X(e,a,o,10485760,p,s.onProgress,g,c)):(await R(async()=>{s.onPhaseChange?.("presigning");let l=await e.upload({key:o,contentType:i,fileSize:a.size,fileName:c?.fileName!==null?c?.fileName??a.name:void 0,metadata:c?.metadata,bucket:c?.bucket,acl:c?.acl});s.onPhaseChange?.("uploading"),l.method==="put"?await q(a,l.url,l.headers??{},s.onProgress,g):await B(a,l.url,l.fields??{},s.onProgress,g);},3,g),s.onPhaseChange?.("finalizing"),r=(await e.confirm({key:o,bucket:c?.bucket})).eTag),{key:o,eTag:r}}async function x(e,a,o={},d={},s,g){let c=a.map(U=>({...U,status:"pending",progress:{loaded:0,total:U.file.size,percent:0},result:null,error:null})),t=()=>{let U=c.reduce((u,f)=>u+f.progress.loaded,0),l=c.reduce((u,f)=>u+f.progress.total,0);d.onTotalProgress?.({loaded:U,total:l,percent:l>0?Math.round(U/l*100):0});},n=0,p=async()=>{for(;n<c.length;){if(s?.aborted)return;let U=n++,l=c[U];l.status="uploading";try{let u=await E(e,l.file,l.objectKey,o,{onProgress:f=>{l.progress=f,d.onFileProgress?.(l.id,f),t();}},s,g?.(l.file));l.status="success",l.result=u,l.progress={loaded:l.file.size,total:l.file.size,percent:100},d.onFileSuccess?.(l.id,u),t();}catch(u){if(u.name==="AbortError"){l.status="error",l.error="Upload cancelled";return}let f=u instanceof Error?u.message:"Upload failed";l.status="error",l.error=f,d.onFileError?.(l.id,f),t();}}},i=o.concurrentFiles??2,r=Array.from({length:Math.min(i,a.length)},()=>p());return await Promise.all(r),c}var se={loaded:0,total:0,percent:0},D={phase:"idle",progress:se,error:null,result:null,fileName:null,fileSize:null};function C(e){let[a,o]=useState(D),d=useRef(e);d.current=e;let s=useRef(null),g=useCallback(async(n,p,i)=>{o({...D,phase:"validating",fileName:n.name,fileSize:n.size});let r=d.current,U=validateFile(n,{accept:r.accept,maxFileSize:r.maxFileSize});if(U){o(u=>({...u,phase:"error",error:U})),r.onError?.(n,new Error(U),"validating");return}if(r.beforeUpload&&!await r.beforeUpload(n)){o(f=>({...f,phase:"error",error:"Upload blocked by beforeUpload hook"})),r.onError?.(n,new Error("blocked"),"validating");return}o(u=>({...u,phase:"presigning"})),r.onUploadStart?.(n,p);let l=new AbortController;s.current=l;try{let u=await E(r.api,n,p,{multipart:r.multipart,multipartThreshold:r.multipartThreshold,concurrentParts:r.concurrentParts},{onProgress:f=>{o(m=>({...m,progress:f})),r.onProgress?.(n,f);},onPhaseChange:f=>o(m=>({...m,phase:f}))},l.signal,i);o(f=>({...f,phase:"success",result:u,progress:{loaded:n.size,total:n.size,percent:100}})),await r.onSuccess?.(n,u);}catch(u){if(u.name==="AbortError"){r.onCancel?.(n),o(D);return}let f=u instanceof Error?u.message:"Upload failed";o(m=>({...m,phase:"error",error:f})),r.onError?.(n,u,"uploading");}finally{s.current=null;}},[]),c=useCallback(()=>{s.current?.abort(),o(D);},[]),t=useCallback(()=>{s.current?.abort(),o(D);},[]);return {...a,upload:g,cancel:c,reset:t}}var ae={loaded:0,total:0,percent:0},A={phase:"idle",files:[],totalProgress:ae,error:null},ie=0;function pe(){return `file-${++ie}`}function L(e){let[a,o]=useState(A),d=useRef(e);d.current=e;let s=useRef(null),g=useRef(new Map),c=useCallback(async(p,i)=>{let r=d.current,U=[],l=[],u=new Map;if(o(m=>({...m,phase:"validating",error:null})),r.maxFiles&&p.length>r.maxFiles){let m=`Too many files. Maximum is ${r.maxFiles}.`;o(h=>({...h,phase:"error",error:m})),r.onError?.(new Error(m));return}for(let m of p){let h=validateFile(m,{accept:r.accept,maxFileSize:r.maxFileSize});if(h){let P=`${m.name}: ${h}`;o(b=>({...b,phase:"error",error:P})),r.onError?.(new Error(P));return}}if(r.beforeUpload&&!await r.beforeUpload(p)){o(h=>({...h,phase:"error",error:"Upload blocked by beforeUpload hook"})),r.onError?.(new Error("blocked"));return}for(let m of p){let h=pe(),P=i(m);U.push({id:h,file:m,objectKey:P}),u.set(h,m),l.push({id:h,fileName:m.name,fileSize:m.size,status:"pending",progress:{loaded:0,total:m.size,percent:0},error:null});}g.current=u,o({phase:"uploading",files:l,totalProgress:{loaded:0,total:p.reduce((m,h)=>m+h.size,0),percent:0},error:null}),r.onUploadStart?.(p);let f=new AbortController;s.current=f;try{let m=await x(r.api,U,{multipart:r.multipart,multipartThreshold:r.multipartThreshold,concurrentParts:r.concurrentParts,concurrentFiles:r.concurrentFiles},{onFileProgress:(b,w)=>{o(F=>({...F,files:F.files.map(y=>y.id===b?{...y,status:"uploading",progress:w}:y)}));let S=u.get(b);S&&r.onFileProgress?.(S,w);},onFileSuccess:(b,w)=>{o(F=>({...F,files:F.files.map(y=>y.id===b?{...y,status:"success",progress:{loaded:y.fileSize,total:y.fileSize,percent:100}}:y)}));let S=u.get(b);S&&r.onFileSuccess?.(S,w);},onFileError:(b,w)=>{o(F=>({...F,files:F.files.map(y=>y.id===b?{...y,status:"error",error:w}:y)}));let S=u.get(b);S&&r.onFileError?.(S,w);},onTotalProgress:b=>{o(w=>({...w,totalProgress:b})),r.onProgress?.(b);}},f.signal,b=>{let w=r.getUploadOptions?.(b);return r.uploadOptions?{...r.uploadOptions,...w}:w??{}}),h=m.some(b=>b.status==="error"),P=m.filter(b=>b.result!==null).map(b=>b.result);o(b=>({...b,phase:h?"error":"success",error:h?`${m.filter(w=>w.status==="error").length} file(s) failed`:null,totalProgress:h?b.totalProgress:{loaded:b.totalProgress.total,total:b.totalProgress.total,percent:100}})),h||await r.onSuccess?.(P);}catch(m){if(m.name==="AbortError"){r.onCancel?.(),o(A);return}let h=m instanceof Error?m.message:"Upload failed";o(P=>({...P,phase:"error",error:h})),r.onError?.(m);}finally{s.current=null;}},[]),t=useCallback(()=>{s.current?.abort(),o(A);},[]),n=useCallback(()=>{s.current?.abort(),o(A);},[]);return {...a,upload:c,cancel:t,reset:n}}var Y={loaded:0,total:0,percent:0};function ce(e){let a=(e.maxFiles??1)>1,o={api:e.api,accept:e.accept,maxFileSize:e.maxFileSize,multipart:e.multipart,multipartThreshold:e.multipartThreshold,concurrentParts:e.concurrentParts,beforeUpload:e.beforeUpload,onUploadStart:e.onUploadStart,onProgress:e.onProgress,onSuccess:e.onSuccess,onError:e.onError,onCancel:e.onCancel},d={api:e.api,accept:e.accept,maxFileSize:e.maxFileSize,maxFiles:e.maxFiles,multipart:e.multipart,multipartThreshold:e.multipartThreshold,concurrentParts:e.concurrentParts,concurrentFiles:e.concurrentFiles,uploadOptions:e.uploadOptions,getUploadOptions:e.getUploadOptions,beforeUpload:e.beforeUpload,onUploadStart:e.onUploadStart,onProgress:e.onProgress,onSuccess:e.onSuccess,onError:e.onError,onCancel:e.onCancel,onFileProgress:e.onFileProgress,onFileSuccess:e.onFileSuccess,onFileError:e.onFileError},s=C(o),g=L(d),c=useRef(null),[t,n]=useState(null),p=l=>typeof e.objectKey=="function"?e.objectKey(l):e.objectKey,i=async l=>{if(a){if(!l?.length)return;await g.upload(Array.from(l),p);}else {let u=l?.[0];if(!u)return;n({name:u.name,size:u.size}),await s.upload(u,p(u),{...e.uploadOptions,...e.getUploadOptions?.(u)});}},r=()=>c.current?.click(),U=a?g.phase==="uploading":s.phase==="uploading";return {mode:a?"multi":"single",phase:a?g.phase:s.phase,fileInfo:a?null:t,progress:a?Y:s.progress,files:a?g.files:[],totalProgress:a?g.totalProgress:Y,error:a?g.error:s.error,isUploading:U,handleFiles:i,openFilePicker:r,cancel:a?g.cancel:s.cancel,reset:a?g.reset:()=>{s.reset(),n(null);},inputProps:{ref:c,type:"file",...a&&{multiple:true},accept:e.accept?.join(","),hidden:true,onChange:l=>{i(l.target.files),l.target.value="";}},dropHandlers:{onDragOver:l=>{l.preventDefault(),l.stopPropagation();},onDrop:l=>{l.preventDefault(),l.stopPropagation(),U||i(l.dataTransfer.files);}}}}var Z={phase:"idle",error:null,url:null,expiresIn:null};function fe(e){let[a,o]=useState(Z),d=useRef(e);d.current=e;let s=useCallback(async(t,n)=>{let p=d.current;o({phase:"presigning",error:null,url:null,expiresIn:null});try{let i=await p.api.download(t,{fileName:n,bucket:p.bucket});return o({phase:"idle",error:null,url:i.url,expiresIn:i.expiresIn}),{url:i.url,expiresIn:i.expiresIn}}catch(i){let r=i instanceof Error?i.message:"Download failed";return o({phase:"error",error:r,url:null,expiresIn:null}),p.onError?.(t,i),null}},[]),g=useCallback(async(t,n)=>{let p=d.current;if(p.beforeDownload&&!await p.beforeDownload(t)){o({phase:"error",error:"Download blocked by beforeDownload hook",url:null,expiresIn:null}),p.onError?.(t,new Error("blocked"));return}let i=await s(t,n);i&&(window.location.href=i.url,p.onInitiated?.(t));},[s]),c=useCallback(()=>{o(Z);},[]);return {...a,download:g,presign:s,reset:c}}var Q={loaded:0,total:0,percent:0},M={phase:"idle",progress:Q,error:null,fileName:null,fileSize:null};function be(e){let[a,o]=useState(M),d=useRef(e);d.current=e;let s=useRef(null),g=useCallback(async(n,p)=>{let i=n.split("/").pop()??n,r=d.current;if(r.beforeDownload&&!await r.beforeDownload(n)){o(l=>({...l,phase:"error",error:"Download blocked by beforeDownload hook"})),r.onError?.(n,new Error("blocked"),"presigning");return}o({phase:"presigning",progress:Q,error:null,fileName:p??null,fileSize:null});try{let{url:U}=await r.api.download(n,{fileName:p,bucket:r.bucket});o(y=>({...y,phase:"downloading"})),r.onDownloadStart?.(n);let l=new AbortController;s.current=l;let u=await fetch(U,{signal:l.signal});if(!u.ok)throw new Error(u.status===404?"File not found":`Download failed (${u.status})`);let f=Number(u.headers.get("content-length")||0),m=p??K(u.headers.get("content-disposition"),i);o(y=>({...y,fileName:m,fileSize:f||null}));let h=u.body?.getReader();if(!h)throw new Error("ReadableStream not supported");let P=[],b=0;for(;;){let{done:y,value:v}=await h.read();if(y)break;P.push(v),b+=v.byteLength;let O=f>0?Math.round(b/f*100):0,$={loaded:b,total:f,percent:O};o(W=>({...W,progress:$})),r.onProgress?.(n,$);}let w=new Blob(P),S=URL.createObjectURL(w),F=document.createElement("a");F.href=S,F.download=m??i,F.click(),URL.revokeObjectURL(S),o(y=>({...y,phase:"success",fileSize:w.size,progress:{loaded:w.size,total:w.size,percent:100}})),await r.onSuccess?.(n,m??i);}catch(U){if(U.name==="AbortError"){r.onCancel?.(n),o(M);return}let l=U instanceof Error?U.message:"Download failed";o(u=>({...u,phase:"error",error:l})),r.onError?.(n,U,"downloading");}finally{s.current=null;}},[]),c=useCallback(()=>{s.current?.abort(),o(M);},[]),t=useCallback(()=>{s.current?.abort(),o(M);},[]);return {...a,download:g,cancel:c,reset:t}}var _={phase:"idle",error:null};function we(e){let[a,o]=useState(_),[d,s]=useState(null),g=useRef(e);g.current=e;let c=useCallback(i=>{s(i),o({phase:"confirming",error:null});},[]),t=useCallback(async()=>{if(!d)return;let i=g.current;if(i.beforeDelete&&!await i.beforeDelete(d)){o({phase:"error",error:"Delete blocked by beforeDelete hook"}),i.onError?.(d,new Error("blocked"),"confirming"),s(null);return}o({phase:"deleting",error:null}),i.onDeleteStart?.(d);try{await i.api.delete(d,{bucket:i.bucket}),o({phase:"success",error:null}),await i.onSuccess?.(d),s(null);}catch(r){let U=r instanceof Error?r.message:"Delete failed";o({phase:"error",error:U}),i.onError?.(d,r,"deleting");}},[d]),n=useCallback(()=>{s(null),o(_);},[]),p=useCallback(()=>{s(null),o(_);},[]);return {...a,pendingKey:d,requestDelete:c,confirmDelete:t,cancelDelete:n,reset:p}}export{ee as formatFileSize,E as uploadFile,x as uploadFiles,we as useDelete,fe as useDownload,be as useFetchDownload,L as useMultiUpload,C as useUpload,ce as useUploadControls};//# sourceMappingURL=index.js.map
1
+ import { validateFile } from '@better-s3/server';
2
+ export { createS3Api as createPresignApi, createS3Api, validateFile } from '@better-s3/server';
3
+ import { useState, useRef, useCallback } from 'react';
4
+
5
+ // src/helpers.ts
6
+ function parseContentDispositionFilename(header, fallback) {
7
+ if (!header) return fallback;
8
+ const starMatch = header.match(/filename\*=UTF-8''([^;,\s]+)/i);
9
+ if (starMatch) {
10
+ try {
11
+ return decodeURIComponent(starMatch[1]);
12
+ } catch {
13
+ }
14
+ }
15
+ const match = header.match(/filename="([^"]+)"/i);
16
+ if (match) return match[1];
17
+ return fallback;
18
+ }
19
+ function formatFileSize(bytes) {
20
+ if (bytes === 0) return "0 B";
21
+ const units = ["B", "KB", "MB", "GB", "TB"];
22
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
23
+ const size = bytes / Math.pow(1024, i);
24
+ return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
25
+ }
26
+
27
+ // src/upload/constants.ts
28
+ var DEFAULT_MULTIPART_THRESHOLD = 30 * 1024 * 1024;
29
+ var DEFAULT_PART_SIZE = 10 * 1024 * 1024;
30
+ var MAX_RETRIES = 3;
31
+ var RETRY_BASE_DELAY = 1e3;
32
+ var DEFAULT_CONCURRENT_PARTS = 3;
33
+ var DEFAULT_CONCURRENT_FILES = 2;
34
+
35
+ // src/upload/retry.ts
36
+ async function withRetry(fn, retries, signal) {
37
+ let lastError;
38
+ for (let attempt = 0; attempt <= retries; attempt++) {
39
+ try {
40
+ return await fn();
41
+ } catch (err) {
42
+ if (err.name === "AbortError") throw err;
43
+ lastError = err;
44
+ if (attempt < retries) {
45
+ const delay = RETRY_BASE_DELAY * 2 ** attempt;
46
+ await new Promise((r) => setTimeout(r, delay));
47
+ if (signal?.aborted)
48
+ throw new DOMException("Upload aborted", "AbortError");
49
+ }
50
+ }
51
+ }
52
+ throw lastError;
53
+ }
54
+
55
+ // src/upload/simple.ts
56
+ function uploadSimple(file, url, fields, onProgress, signal) {
57
+ return new Promise((resolve, reject) => {
58
+ const xhr = new XMLHttpRequest();
59
+ const onAbort = () => {
60
+ xhr.abort();
61
+ reject(new DOMException("Upload aborted", "AbortError"));
62
+ };
63
+ signal?.addEventListener("abort", onAbort, { once: true });
64
+ xhr.upload.addEventListener("progress", (e) => {
65
+ if (e.lengthComputable) {
66
+ onProgress?.({
67
+ loaded: e.loaded,
68
+ total: e.total,
69
+ percent: Math.round(e.loaded / e.total * 100)
70
+ });
71
+ }
72
+ });
73
+ xhr.addEventListener("load", () => {
74
+ signal?.removeEventListener("abort", onAbort);
75
+ if (xhr.status >= 200 && xhr.status < 300) {
76
+ onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
77
+ resolve();
78
+ } else {
79
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
80
+ }
81
+ });
82
+ xhr.addEventListener("error", () => {
83
+ signal?.removeEventListener("abort", onAbort);
84
+ reject(new Error("Upload failed: network error"));
85
+ });
86
+ xhr.addEventListener("abort", () => {
87
+ signal?.removeEventListener("abort", onAbort);
88
+ reject(new DOMException("Upload aborted", "AbortError"));
89
+ });
90
+ const formData = new FormData();
91
+ for (const [k, v] of Object.entries(fields)) {
92
+ formData.append(k, v);
93
+ }
94
+ formData.append("file", file);
95
+ xhr.open("POST", url);
96
+ xhr.send(formData);
97
+ });
98
+ }
99
+ function uploadPut(file, url, headers, onProgress, signal) {
100
+ return new Promise((resolve, reject) => {
101
+ const xhr = new XMLHttpRequest();
102
+ const onAbort = () => {
103
+ xhr.abort();
104
+ reject(new DOMException("Upload aborted", "AbortError"));
105
+ };
106
+ signal?.addEventListener("abort", onAbort, { once: true });
107
+ xhr.upload.addEventListener("progress", (e) => {
108
+ if (e.lengthComputable) {
109
+ onProgress?.({
110
+ loaded: e.loaded,
111
+ total: e.total,
112
+ percent: Math.round(e.loaded / e.total * 100)
113
+ });
114
+ }
115
+ });
116
+ xhr.addEventListener("load", () => {
117
+ signal?.removeEventListener("abort", onAbort);
118
+ if (xhr.status >= 200 && xhr.status < 300) {
119
+ onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
120
+ resolve();
121
+ } else {
122
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
123
+ }
124
+ });
125
+ xhr.addEventListener("error", () => {
126
+ signal?.removeEventListener("abort", onAbort);
127
+ reject(new Error("Upload failed: network error"));
128
+ });
129
+ xhr.addEventListener("abort", () => {
130
+ signal?.removeEventListener("abort", onAbort);
131
+ reject(new DOMException("Upload aborted", "AbortError"));
132
+ });
133
+ xhr.open("PUT", url);
134
+ for (const [k, v] of Object.entries(headers)) {
135
+ xhr.setRequestHeader(k, v);
136
+ }
137
+ xhr.send(file);
138
+ });
139
+ }
140
+
141
+ // src/upload/part.ts
142
+ function uploadPart(blob, presignedUrl, partLoaded, totalSize, reportProgress, signal) {
143
+ return new Promise((resolve, reject) => {
144
+ const xhr = new XMLHttpRequest();
145
+ const onAbort = () => {
146
+ xhr.abort();
147
+ reject(new DOMException("Upload aborted", "AbortError"));
148
+ };
149
+ signal?.addEventListener("abort", onAbort, { once: true });
150
+ xhr.upload.addEventListener("progress", (e) => {
151
+ if (e.lengthComputable) {
152
+ partLoaded.bytes = e.loaded;
153
+ reportProgress();
154
+ }
155
+ });
156
+ xhr.addEventListener("load", () => {
157
+ signal?.removeEventListener("abort", onAbort);
158
+ if (xhr.status >= 200 && xhr.status < 300) {
159
+ partLoaded.bytes = blob.size;
160
+ reportProgress();
161
+ resolve();
162
+ } else {
163
+ reject(new Error(`Part upload failed: ${xhr.status}`));
164
+ }
165
+ });
166
+ xhr.addEventListener("error", () => {
167
+ signal?.removeEventListener("abort", onAbort);
168
+ reject(new Error("Part upload failed: network error"));
169
+ });
170
+ xhr.addEventListener("abort", () => {
171
+ signal?.removeEventListener("abort", onAbort);
172
+ reject(new DOMException("Upload aborted", "AbortError"));
173
+ });
174
+ xhr.open("PUT", presignedUrl);
175
+ xhr.send(blob);
176
+ });
177
+ }
178
+
179
+ // src/upload/multipart.ts
180
+ async function uploadMultipart(api, file, objectKey, partSize, concurrentParts, onProgress, signal, requestOptions) {
181
+ const contentType = requestOptions?.contentType ?? file.type;
182
+ const { uploadId, key } = await api.multipart.init({
183
+ key: objectKey,
184
+ contentType,
185
+ fileSize: file.size,
186
+ fileName: requestOptions?.fileName !== null ? requestOptions?.fileName ?? file.name : void 0,
187
+ metadata: requestOptions?.metadata,
188
+ bucket: requestOptions?.bucket,
189
+ acl: requestOptions?.acl
190
+ });
191
+ const totalParts = Math.ceil(file.size / partSize);
192
+ const parts = [];
193
+ const partProgress = Array.from(
194
+ { length: totalParts },
195
+ () => ({ bytes: 0 })
196
+ );
197
+ const reportProgress = () => {
198
+ const loaded = partProgress.reduce((sum, p) => sum + p.bytes, 0);
199
+ onProgress?.({
200
+ loaded,
201
+ total: file.size,
202
+ percent: Math.round(loaded / file.size * 100)
203
+ });
204
+ };
205
+ try {
206
+ for (let batchStart = 0; batchStart < totalParts; batchStart += concurrentParts) {
207
+ if (signal?.aborted) {
208
+ throw new DOMException("Upload aborted", "AbortError");
209
+ }
210
+ const batchEnd = Math.min(batchStart + concurrentParts, totalParts);
211
+ const batch = [];
212
+ for (let i = batchStart; i < batchEnd; i++) {
213
+ const start = i * partSize;
214
+ const end = Math.min(start + partSize, file.size);
215
+ const blob = file.slice(start, end);
216
+ const partNumber = i + 1;
217
+ batch.push(
218
+ withRetry(
219
+ async () => {
220
+ const { presignedUrl } = await api.multipart.signPart({
221
+ key,
222
+ uploadId,
223
+ partNumber,
224
+ // Pass the exact byte count so the server binds Content-Length
225
+ // into the HMAC signature. S3 will then reject any PUT whose
226
+ // body size differs from blob.size with SignatureDoesNotMatch.
227
+ partSize: blob.size,
228
+ bucket: requestOptions?.bucket
229
+ });
230
+ partProgress[i].bytes = 0;
231
+ await uploadPart(
232
+ blob,
233
+ presignedUrl,
234
+ partProgress[i],
235
+ file.size,
236
+ reportProgress,
237
+ signal
238
+ );
239
+ return { partNumber };
240
+ },
241
+ MAX_RETRIES,
242
+ signal
243
+ )
244
+ );
245
+ }
246
+ const batchResults = await Promise.all(batch);
247
+ parts.push(...batchResults);
248
+ }
249
+ parts.sort((a, b) => a.partNumber - b.partNumber);
250
+ const result = await api.multipart.complete({
251
+ key,
252
+ uploadId,
253
+ parts,
254
+ bucket: requestOptions?.bucket
255
+ });
256
+ onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
257
+ return result.eTag;
258
+ } catch (err) {
259
+ api.multipart.abort({ key, uploadId, bucket: requestOptions?.bucket }).catch(() => {
260
+ });
261
+ throw err;
262
+ }
263
+ }
264
+
265
+ // src/upload/upload-file.ts
266
+ async function uploadFile(api, file, objectKey, config = {}, callbacks = {}, signal, requestOptions) {
267
+ const threshold = config.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
268
+ const useMultipart = config.multipart === true && file.size >= threshold;
269
+ const concurrentParts = config.concurrentParts ?? DEFAULT_CONCURRENT_PARTS;
270
+ const contentType = requestOptions?.contentType ?? file.type;
271
+ let eTag;
272
+ if (useMultipart) {
273
+ callbacks.onPhaseChange?.("uploading");
274
+ eTag = await uploadMultipart(
275
+ api,
276
+ file,
277
+ objectKey,
278
+ DEFAULT_PART_SIZE,
279
+ concurrentParts,
280
+ callbacks.onProgress,
281
+ signal,
282
+ requestOptions
283
+ );
284
+ } else {
285
+ await withRetry(
286
+ async () => {
287
+ callbacks.onPhaseChange?.("presigning");
288
+ const presign = await api.upload({
289
+ key: objectKey,
290
+ contentType,
291
+ fileSize: file.size,
292
+ fileName: requestOptions?.fileName !== null ? requestOptions?.fileName ?? file.name : void 0,
293
+ metadata: requestOptions?.metadata,
294
+ bucket: requestOptions?.bucket,
295
+ acl: requestOptions?.acl
296
+ });
297
+ callbacks.onPhaseChange?.("uploading");
298
+ if (presign.method === "put") {
299
+ await uploadPut(
300
+ file,
301
+ presign.url,
302
+ presign.headers ?? {},
303
+ callbacks.onProgress,
304
+ signal
305
+ );
306
+ } else {
307
+ await uploadSimple(
308
+ file,
309
+ presign.url,
310
+ presign.fields ?? {},
311
+ callbacks.onProgress,
312
+ signal
313
+ );
314
+ }
315
+ },
316
+ MAX_RETRIES,
317
+ signal
318
+ );
319
+ callbacks.onPhaseChange?.("finalizing");
320
+ const confirmed = await api.confirm({
321
+ key: objectKey,
322
+ bucket: requestOptions?.bucket
323
+ });
324
+ eTag = confirmed.eTag;
325
+ }
326
+ return { key: objectKey, eTag };
327
+ }
328
+
329
+ // src/upload/upload-files.ts
330
+ async function uploadFiles(api, items, config = {}, callbacks = {}, signal, getRequestOptions) {
331
+ const results = items.map((item) => ({
332
+ ...item,
333
+ status: "pending",
334
+ progress: { loaded: 0, total: item.file.size, percent: 0 },
335
+ result: null,
336
+ error: null
337
+ }));
338
+ const reportTotalProgress = () => {
339
+ const loaded = results.reduce((sum, r) => sum + r.progress.loaded, 0);
340
+ const total = results.reduce((sum, r) => sum + r.progress.total, 0);
341
+ callbacks.onTotalProgress?.({
342
+ loaded,
343
+ total,
344
+ percent: total > 0 ? Math.round(loaded / total * 100) : 0
345
+ });
346
+ };
347
+ let nextIndex = 0;
348
+ const processNext = async () => {
349
+ while (nextIndex < results.length) {
350
+ if (signal?.aborted) return;
351
+ const idx = nextIndex++;
352
+ const item = results[idx];
353
+ item.status = "uploading";
354
+ try {
355
+ const result = await uploadFile(
356
+ api,
357
+ item.file,
358
+ item.objectKey,
359
+ config,
360
+ {
361
+ onProgress: (progress) => {
362
+ item.progress = progress;
363
+ callbacks.onFileProgress?.(item.id, progress);
364
+ reportTotalProgress();
365
+ }
366
+ },
367
+ signal,
368
+ getRequestOptions?.(item.file)
369
+ );
370
+ item.status = "success";
371
+ item.result = result;
372
+ item.progress = {
373
+ loaded: item.file.size,
374
+ total: item.file.size,
375
+ percent: 100
376
+ };
377
+ callbacks.onFileSuccess?.(item.id, result);
378
+ reportTotalProgress();
379
+ } catch (err) {
380
+ if (err.name === "AbortError") {
381
+ item.status = "error";
382
+ item.error = "Upload cancelled";
383
+ return;
384
+ }
385
+ const message = err instanceof Error ? err.message : "Upload failed";
386
+ item.status = "error";
387
+ item.error = message;
388
+ callbacks.onFileError?.(item.id, message);
389
+ reportTotalProgress();
390
+ }
391
+ }
392
+ };
393
+ const concurrentFiles = config.concurrentFiles ?? DEFAULT_CONCURRENT_FILES;
394
+ const workers = Array.from(
395
+ { length: Math.min(concurrentFiles, items.length) },
396
+ () => processNext()
397
+ );
398
+ await Promise.all(workers);
399
+ return results;
400
+ }
401
+ var INITIAL_PROGRESS = { loaded: 0, total: 0, percent: 0 };
402
+ var INITIAL_STATE = {
403
+ phase: "idle",
404
+ progress: INITIAL_PROGRESS,
405
+ error: null,
406
+ result: null,
407
+ fileName: null,
408
+ fileSize: null
409
+ };
410
+ function useUpload(options) {
411
+ const [state, setState] = useState(INITIAL_STATE);
412
+ const optionsRef = useRef(options);
413
+ optionsRef.current = options;
414
+ const abortRef = useRef(null);
415
+ const upload = useCallback(
416
+ async (file, objectKey, requestOptions) => {
417
+ setState({
418
+ ...INITIAL_STATE,
419
+ phase: "validating",
420
+ fileName: file.name,
421
+ fileSize: file.size
422
+ });
423
+ const opts = optionsRef.current;
424
+ const validationError = validateFile(file, {
425
+ accept: opts.accept,
426
+ maxFileSize: opts.maxFileSize
427
+ });
428
+ if (validationError) {
429
+ setState((s) => ({ ...s, phase: "error", error: validationError }));
430
+ opts.onError?.(file, new Error(validationError), "validating");
431
+ return;
432
+ }
433
+ if (opts.beforeUpload) {
434
+ const allowed = await opts.beforeUpload(file);
435
+ if (!allowed) {
436
+ setState((s) => ({
437
+ ...s,
438
+ phase: "error",
439
+ error: "Upload blocked by beforeUpload hook"
440
+ }));
441
+ opts.onError?.(file, new Error("blocked"), "validating");
442
+ return;
443
+ }
444
+ }
445
+ setState((s) => ({ ...s, phase: "presigning" }));
446
+ opts.onUploadStart?.(file, objectKey);
447
+ const controller = new AbortController();
448
+ abortRef.current = controller;
449
+ try {
450
+ const result = await uploadFile(
451
+ opts.api,
452
+ file,
453
+ objectKey,
454
+ {
455
+ multipart: opts.multipart,
456
+ multipartThreshold: opts.multipartThreshold,
457
+ concurrentParts: opts.concurrentParts
458
+ },
459
+ {
460
+ onProgress: (progress) => {
461
+ setState((s) => ({ ...s, progress }));
462
+ opts.onProgress?.(file, progress);
463
+ },
464
+ onPhaseChange: (phase) => setState((s) => ({ ...s, phase }))
465
+ },
466
+ controller.signal,
467
+ requestOptions
468
+ );
469
+ setState((s) => ({
470
+ ...s,
471
+ phase: "success",
472
+ result,
473
+ progress: { loaded: file.size, total: file.size, percent: 100 }
474
+ }));
475
+ await opts.onSuccess?.(file, result);
476
+ } catch (err) {
477
+ if (err.name === "AbortError") {
478
+ opts.onCancel?.(file);
479
+ setState(INITIAL_STATE);
480
+ return;
481
+ }
482
+ const message = err instanceof Error ? err.message : "Upload failed";
483
+ setState((s) => ({ ...s, phase: "error", error: message }));
484
+ opts.onError?.(file, err, "uploading");
485
+ } finally {
486
+ abortRef.current = null;
487
+ }
488
+ },
489
+ []
490
+ );
491
+ const cancel = useCallback(() => {
492
+ abortRef.current?.abort();
493
+ setState(INITIAL_STATE);
494
+ }, []);
495
+ const reset = useCallback(() => {
496
+ abortRef.current?.abort();
497
+ setState(INITIAL_STATE);
498
+ }, []);
499
+ return { ...state, upload, cancel, reset };
500
+ }
501
+ var INITIAL_PROGRESS2 = { loaded: 0, total: 0, percent: 0 };
502
+ var INITIAL_STATE2 = {
503
+ phase: "idle",
504
+ files: [],
505
+ totalProgress: INITIAL_PROGRESS2,
506
+ error: null
507
+ };
508
+ var nextId = 0;
509
+ function generateId() {
510
+ return `file-${++nextId}`;
511
+ }
512
+ function useMultiUpload(options) {
513
+ const [state, setState] = useState(INITIAL_STATE2);
514
+ const optionsRef = useRef(options);
515
+ optionsRef.current = options;
516
+ const abortRef = useRef(null);
517
+ const fileMapRef = useRef(/* @__PURE__ */ new Map());
518
+ const upload = useCallback(
519
+ async (files, resolveKey) => {
520
+ const opts = optionsRef.current;
521
+ const items = [];
522
+ const fileStates = [];
523
+ const fileMap = /* @__PURE__ */ new Map();
524
+ setState((s) => ({ ...s, phase: "validating", error: null }));
525
+ if (opts.maxFiles && files.length > opts.maxFiles) {
526
+ const msg = `Too many files. Maximum is ${opts.maxFiles}.`;
527
+ setState((s) => ({ ...s, phase: "error", error: msg }));
528
+ opts.onError?.(new Error(msg));
529
+ return;
530
+ }
531
+ for (const file of files) {
532
+ const validationError = validateFile(file, {
533
+ accept: opts.accept,
534
+ maxFileSize: opts.maxFileSize
535
+ });
536
+ if (validationError) {
537
+ const msg = `${file.name}: ${validationError}`;
538
+ setState((s) => ({ ...s, phase: "error", error: msg }));
539
+ opts.onError?.(new Error(msg));
540
+ return;
541
+ }
542
+ }
543
+ if (opts.beforeUpload) {
544
+ const allowed = await opts.beforeUpload(files);
545
+ if (!allowed) {
546
+ setState((s) => ({
547
+ ...s,
548
+ phase: "error",
549
+ error: "Upload blocked by beforeUpload hook"
550
+ }));
551
+ opts.onError?.(new Error("blocked"));
552
+ return;
553
+ }
554
+ }
555
+ for (const file of files) {
556
+ const id = generateId();
557
+ const objectKey = resolveKey(file);
558
+ items.push({ id, file, objectKey });
559
+ fileMap.set(id, file);
560
+ fileStates.push({
561
+ id,
562
+ fileName: file.name,
563
+ fileSize: file.size,
564
+ status: "pending",
565
+ progress: { loaded: 0, total: file.size, percent: 0 },
566
+ error: null
567
+ });
568
+ }
569
+ fileMapRef.current = fileMap;
570
+ setState({
571
+ phase: "uploading",
572
+ files: fileStates,
573
+ totalProgress: {
574
+ loaded: 0,
575
+ total: files.reduce((s, f) => s + f.size, 0),
576
+ percent: 0
577
+ },
578
+ error: null
579
+ });
580
+ opts.onUploadStart?.(files);
581
+ const controller = new AbortController();
582
+ abortRef.current = controller;
583
+ try {
584
+ const results = await uploadFiles(
585
+ opts.api,
586
+ items,
587
+ {
588
+ multipart: opts.multipart,
589
+ multipartThreshold: opts.multipartThreshold,
590
+ concurrentParts: opts.concurrentParts,
591
+ concurrentFiles: opts.concurrentFiles
592
+ },
593
+ {
594
+ onFileProgress: (id, progress) => {
595
+ setState((s) => ({
596
+ ...s,
597
+ files: s.files.map(
598
+ (f) => f.id === id ? { ...f, status: "uploading", progress } : f
599
+ )
600
+ }));
601
+ const file = fileMap.get(id);
602
+ if (file) opts.onFileProgress?.(file, progress);
603
+ },
604
+ onFileSuccess: (id, result) => {
605
+ setState((s) => ({
606
+ ...s,
607
+ files: s.files.map(
608
+ (f) => f.id === id ? {
609
+ ...f,
610
+ status: "success",
611
+ progress: {
612
+ loaded: f.fileSize,
613
+ total: f.fileSize,
614
+ percent: 100
615
+ }
616
+ } : f
617
+ )
618
+ }));
619
+ const file = fileMap.get(id);
620
+ if (file) opts.onFileSuccess?.(file, result);
621
+ },
622
+ onFileError: (id, error) => {
623
+ setState((s) => ({
624
+ ...s,
625
+ files: s.files.map(
626
+ (f) => f.id === id ? { ...f, status: "error", error } : f
627
+ )
628
+ }));
629
+ const file = fileMap.get(id);
630
+ if (file) opts.onFileError?.(file, error);
631
+ },
632
+ onTotalProgress: (progress) => {
633
+ setState((s) => ({ ...s, totalProgress: progress }));
634
+ opts.onProgress?.(progress);
635
+ }
636
+ },
637
+ controller.signal,
638
+ (file) => {
639
+ const perFile = opts.getUploadOptions?.(file);
640
+ if (!opts.uploadOptions) return perFile ?? {};
641
+ return { ...opts.uploadOptions, ...perFile };
642
+ }
643
+ );
644
+ const hasErrors = results.some((r) => r.status === "error");
645
+ const successResults = results.filter((r) => r.result !== null).map((r) => r.result);
646
+ setState((s) => ({
647
+ ...s,
648
+ phase: hasErrors ? "error" : "success",
649
+ error: hasErrors ? `${results.filter((r) => r.status === "error").length} file(s) failed` : null,
650
+ totalProgress: hasErrors ? s.totalProgress : {
651
+ loaded: s.totalProgress.total,
652
+ total: s.totalProgress.total,
653
+ percent: 100
654
+ }
655
+ }));
656
+ if (!hasErrors) {
657
+ await opts.onSuccess?.(successResults);
658
+ }
659
+ } catch (err) {
660
+ if (err.name === "AbortError") {
661
+ opts.onCancel?.();
662
+ setState(INITIAL_STATE2);
663
+ return;
664
+ }
665
+ const message = err instanceof Error ? err.message : "Upload failed";
666
+ setState((s) => ({ ...s, phase: "error", error: message }));
667
+ opts.onError?.(err);
668
+ } finally {
669
+ abortRef.current = null;
670
+ }
671
+ },
672
+ []
673
+ );
674
+ const cancel = useCallback(() => {
675
+ abortRef.current?.abort();
676
+ setState(INITIAL_STATE2);
677
+ }, []);
678
+ const reset = useCallback(() => {
679
+ abortRef.current?.abort();
680
+ setState(INITIAL_STATE2);
681
+ }, []);
682
+ return { ...state, upload, cancel, reset };
683
+ }
684
+ var INITIAL_PROGRESS3 = { loaded: 0, total: 0, percent: 0 };
685
+ function useUploadControls(options) {
686
+ const isMulti = (options.maxFiles ?? 1) > 1;
687
+ const singleOpts = {
688
+ api: options.api,
689
+ accept: options.accept,
690
+ maxFileSize: options.maxFileSize,
691
+ multipart: options.multipart,
692
+ multipartThreshold: options.multipartThreshold,
693
+ concurrentParts: options.concurrentParts,
694
+ beforeUpload: options.beforeUpload,
695
+ onUploadStart: options.onUploadStart,
696
+ onProgress: options.onProgress,
697
+ onSuccess: options.onSuccess,
698
+ onError: options.onError,
699
+ onCancel: options.onCancel
700
+ };
701
+ const multiOpts = {
702
+ api: options.api,
703
+ accept: options.accept,
704
+ maxFileSize: options.maxFileSize,
705
+ maxFiles: options.maxFiles,
706
+ multipart: options.multipart,
707
+ multipartThreshold: options.multipartThreshold,
708
+ concurrentParts: options.concurrentParts,
709
+ concurrentFiles: options.concurrentFiles,
710
+ uploadOptions: options.uploadOptions,
711
+ getUploadOptions: options.getUploadOptions,
712
+ beforeUpload: options.beforeUpload,
713
+ onUploadStart: options.onUploadStart,
714
+ onProgress: options.onProgress,
715
+ onSuccess: options.onSuccess,
716
+ onError: options.onError,
717
+ onCancel: options.onCancel,
718
+ onFileProgress: options.onFileProgress,
719
+ onFileSuccess: options.onFileSuccess,
720
+ onFileError: options.onFileError
721
+ };
722
+ const single = useUpload(singleOpts);
723
+ const multi = useMultiUpload(multiOpts);
724
+ const inputRef = useRef(null);
725
+ const [fileInfo, setFileInfo] = useState(null);
726
+ const resolveKey = (file) => typeof options.objectKey === "function" ? options.objectKey(file) : options.objectKey;
727
+ const handleFiles = async (files) => {
728
+ if (isMulti) {
729
+ if (!files?.length) return;
730
+ await multi.upload(Array.from(files), resolveKey);
731
+ } else {
732
+ const file = files?.[0];
733
+ if (!file) return;
734
+ setFileInfo({ name: file.name, size: file.size });
735
+ await single.upload(file, resolveKey(file), {
736
+ ...options.uploadOptions,
737
+ ...options.getUploadOptions?.(file)
738
+ });
739
+ }
740
+ };
741
+ const openFilePicker = () => inputRef.current?.click();
742
+ const isUploading = isMulti ? multi.phase === "uploading" : single.phase === "uploading";
743
+ return {
744
+ mode: isMulti ? "multi" : "single",
745
+ phase: isMulti ? multi.phase : single.phase,
746
+ fileInfo: isMulti ? null : fileInfo,
747
+ progress: isMulti ? INITIAL_PROGRESS3 : single.progress,
748
+ files: isMulti ? multi.files : [],
749
+ totalProgress: isMulti ? multi.totalProgress : INITIAL_PROGRESS3,
750
+ error: isMulti ? multi.error : single.error,
751
+ isUploading,
752
+ handleFiles,
753
+ openFilePicker,
754
+ cancel: isMulti ? multi.cancel : single.cancel,
755
+ reset: isMulti ? multi.reset : () => {
756
+ single.reset();
757
+ setFileInfo(null);
758
+ },
759
+ inputProps: {
760
+ ref: inputRef,
761
+ type: "file",
762
+ ...isMulti && { multiple: true },
763
+ accept: options.accept?.join(","),
764
+ hidden: true,
765
+ onChange: (e) => {
766
+ handleFiles(e.target.files);
767
+ e.target.value = "";
768
+ }
769
+ },
770
+ dropHandlers: {
771
+ onDragOver: (e) => {
772
+ e.preventDefault();
773
+ e.stopPropagation();
774
+ },
775
+ onDrop: (e) => {
776
+ e.preventDefault();
777
+ e.stopPropagation();
778
+ if (!isUploading) handleFiles(e.dataTransfer.files);
779
+ }
780
+ }
781
+ };
782
+ }
783
+ var INITIAL_STATE3 = {
784
+ phase: "idle",
785
+ error: null,
786
+ url: null,
787
+ expiresIn: null
788
+ };
789
+ function useDownload(options) {
790
+ const [state, setState] = useState(INITIAL_STATE3);
791
+ const optionsRef = useRef(options);
792
+ optionsRef.current = options;
793
+ const presign = useCallback(async (key, downloadName) => {
794
+ const opts = optionsRef.current;
795
+ setState({ phase: "presigning", error: null, url: null, expiresIn: null });
796
+ try {
797
+ const result = await opts.api.download(key, {
798
+ fileName: downloadName,
799
+ bucket: opts.bucket
800
+ });
801
+ setState({
802
+ phase: "idle",
803
+ error: null,
804
+ url: result.url,
805
+ expiresIn: result.expiresIn
806
+ });
807
+ return { url: result.url, expiresIn: result.expiresIn };
808
+ } catch (err) {
809
+ const message = err instanceof Error ? err.message : "Download failed";
810
+ setState({ phase: "error", error: message, url: null, expiresIn: null });
811
+ opts.onError?.(key, err);
812
+ return null;
813
+ }
814
+ }, []);
815
+ const download = useCallback(
816
+ async (key, downloadName) => {
817
+ const opts = optionsRef.current;
818
+ if (opts.beforeDownload) {
819
+ const allowed = await opts.beforeDownload(key);
820
+ if (!allowed) {
821
+ setState({
822
+ phase: "error",
823
+ error: "Download blocked by beforeDownload hook",
824
+ url: null,
825
+ expiresIn: null
826
+ });
827
+ opts.onError?.(key, new Error("blocked"));
828
+ return;
829
+ }
830
+ }
831
+ const result = await presign(key, downloadName);
832
+ if (!result) return;
833
+ window.location.href = result.url;
834
+ opts.onInitiated?.(key);
835
+ },
836
+ [presign]
837
+ );
838
+ const reset = useCallback(() => {
839
+ setState(INITIAL_STATE3);
840
+ }, []);
841
+ return { ...state, download, presign, reset };
842
+ }
843
+ var INITIAL_PROGRESS4 = {
844
+ loaded: 0,
845
+ total: 0,
846
+ percent: 0
847
+ };
848
+ var INITIAL_STATE4 = {
849
+ phase: "idle",
850
+ progress: INITIAL_PROGRESS4,
851
+ error: null,
852
+ fileName: null,
853
+ fileSize: null
854
+ };
855
+ function useFetchDownload(options) {
856
+ const [state, setState] = useState(INITIAL_STATE4);
857
+ const optionsRef = useRef(options);
858
+ optionsRef.current = options;
859
+ const abortRef = useRef(null);
860
+ const download = useCallback(async (key, downloadName) => {
861
+ const fallback = key.split("/").pop() ?? key;
862
+ const opts = optionsRef.current;
863
+ if (opts.beforeDownload) {
864
+ const allowed = await opts.beforeDownload(key);
865
+ if (!allowed) {
866
+ setState((s) => ({
867
+ ...s,
868
+ phase: "error",
869
+ error: "Download blocked by beforeDownload hook"
870
+ }));
871
+ opts.onError?.(key, new Error("blocked"), "presigning");
872
+ return;
873
+ }
874
+ }
875
+ setState({
876
+ phase: "presigning",
877
+ progress: INITIAL_PROGRESS4,
878
+ error: null,
879
+ fileName: downloadName ?? null,
880
+ fileSize: null
881
+ });
882
+ try {
883
+ const { url } = await opts.api.download(key, {
884
+ fileName: downloadName,
885
+ bucket: opts.bucket
886
+ });
887
+ setState((s) => ({ ...s, phase: "downloading" }));
888
+ opts.onDownloadStart?.(key);
889
+ const controller = new AbortController();
890
+ abortRef.current = controller;
891
+ const res = await fetch(url, { signal: controller.signal });
892
+ if (!res.ok) {
893
+ throw new Error(
894
+ res.status === 404 ? "File not found" : `Download failed (${res.status})`
895
+ );
896
+ }
897
+ const contentLength = Number(res.headers.get("content-length") || 0);
898
+ const name = downloadName ?? parseContentDispositionFilename(
899
+ res.headers.get("content-disposition"),
900
+ fallback
901
+ );
902
+ setState((s) => ({
903
+ ...s,
904
+ fileName: name,
905
+ fileSize: contentLength || null
906
+ }));
907
+ const reader = res.body?.getReader();
908
+ if (!reader) throw new Error("ReadableStream not supported");
909
+ const chunks = [];
910
+ let loaded = 0;
911
+ while (true) {
912
+ const { done, value } = await reader.read();
913
+ if (done) break;
914
+ chunks.push(value);
915
+ loaded += value.byteLength;
916
+ const percent = contentLength > 0 ? Math.round(loaded / contentLength * 100) : 0;
917
+ const progress = {
918
+ loaded,
919
+ total: contentLength,
920
+ percent
921
+ };
922
+ setState((s) => ({ ...s, progress }));
923
+ opts.onProgress?.(key, progress);
924
+ }
925
+ const blob = new Blob(chunks);
926
+ const blobUrl = URL.createObjectURL(blob);
927
+ const anchor = document.createElement("a");
928
+ anchor.href = blobUrl;
929
+ anchor.download = name ?? fallback;
930
+ anchor.click();
931
+ URL.revokeObjectURL(blobUrl);
932
+ setState((s) => ({
933
+ ...s,
934
+ phase: "success",
935
+ fileSize: blob.size,
936
+ progress: { loaded: blob.size, total: blob.size, percent: 100 }
937
+ }));
938
+ await opts.onSuccess?.(key, name ?? fallback);
939
+ } catch (err) {
940
+ if (err.name === "AbortError") {
941
+ opts.onCancel?.(key);
942
+ setState(INITIAL_STATE4);
943
+ return;
944
+ }
945
+ const message = err instanceof Error ? err.message : "Download failed";
946
+ setState((s) => ({ ...s, phase: "error", error: message }));
947
+ opts.onError?.(key, err, "downloading");
948
+ } finally {
949
+ abortRef.current = null;
950
+ }
951
+ }, []);
952
+ const cancel = useCallback(() => {
953
+ abortRef.current?.abort();
954
+ setState(INITIAL_STATE4);
955
+ }, []);
956
+ const reset = useCallback(() => {
957
+ abortRef.current?.abort();
958
+ setState(INITIAL_STATE4);
959
+ }, []);
960
+ return { ...state, download, cancel, reset };
961
+ }
962
+ var INITIAL_STATE5 = {
963
+ phase: "idle",
964
+ error: null
965
+ };
966
+ function useDelete(options) {
967
+ const [state, setState] = useState(INITIAL_STATE5);
968
+ const [pendingKey, setPendingKey] = useState(null);
969
+ const optionsRef = useRef(options);
970
+ optionsRef.current = options;
971
+ const requestDelete = useCallback((key) => {
972
+ setPendingKey(key);
973
+ setState({ phase: "confirming", error: null });
974
+ }, []);
975
+ const confirmDelete = useCallback(async () => {
976
+ if (!pendingKey) return;
977
+ const opts = optionsRef.current;
978
+ if (opts.beforeDelete) {
979
+ const allowed = await opts.beforeDelete(pendingKey);
980
+ if (!allowed) {
981
+ setState({
982
+ phase: "error",
983
+ error: "Delete blocked by beforeDelete hook"
984
+ });
985
+ opts.onError?.(pendingKey, new Error("blocked"), "confirming");
986
+ setPendingKey(null);
987
+ return;
988
+ }
989
+ }
990
+ setState({ phase: "deleting", error: null });
991
+ opts.onDeleteStart?.(pendingKey);
992
+ try {
993
+ await opts.api.delete(pendingKey, { bucket: opts.bucket });
994
+ setState({ phase: "success", error: null });
995
+ await opts.onSuccess?.(pendingKey);
996
+ setPendingKey(null);
997
+ } catch (err) {
998
+ const message = err instanceof Error ? err.message : "Delete failed";
999
+ setState({ phase: "error", error: message });
1000
+ opts.onError?.(pendingKey, err, "deleting");
1001
+ }
1002
+ }, [pendingKey]);
1003
+ const cancelDelete = useCallback(() => {
1004
+ setPendingKey(null);
1005
+ setState(INITIAL_STATE5);
1006
+ }, []);
1007
+ const reset = useCallback(() => {
1008
+ setPendingKey(null);
1009
+ setState(INITIAL_STATE5);
1010
+ }, []);
1011
+ return {
1012
+ ...state,
1013
+ pendingKey,
1014
+ requestDelete,
1015
+ confirmDelete,
1016
+ cancelDelete,
1017
+ reset
1018
+ };
1019
+ }
1020
+
1021
+ export { formatFileSize, uploadFile, uploadFiles, useDelete, useDownload, useFetchDownload, useMultiUpload, useUpload, useUploadControls };
1022
+ //# sourceMappingURL=index.js.map
2
1023
  //# sourceMappingURL=index.js.map