@better-s3/react 3.1045.1 → 3.1046.0

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.
Files changed (44) hide show
  1. package/dist/api.d.ts +30 -0
  2. package/dist/helpers/build-object-key.d.ts +20 -0
  3. package/dist/helpers/format-eta.d.ts +12 -0
  4. package/dist/helpers/format-file-size.d.ts +11 -0
  5. package/dist/helpers/format-speed.d.ts +8 -0
  6. package/dist/helpers/format-upload-progress.d.ts +17 -0
  7. package/dist/helpers/get-file-extension.d.ts +13 -0
  8. package/dist/helpers/index.d.ts +10 -0
  9. package/dist/helpers/parse-content-disposition.d.ts +18 -0
  10. package/dist/helpers/speed-tracker.d.ts +17 -0
  11. package/dist/helpers/truncate-filename.d.ts +9 -0
  12. package/dist/helpers/validate-file.d.ts +22 -0
  13. package/dist/hooks/index.d.ts +7 -0
  14. package/dist/{use-delete.d.ts → hooks/use-delete.d.ts} +4 -3
  15. package/dist/{use-download.d.ts → hooks/use-download.d.ts} +5 -10
  16. package/dist/hooks/use-fetch-download.d.ts +23 -0
  17. package/dist/hooks/use-multi-upload-controls.d.ts +33 -0
  18. package/dist/{use-multi-upload.d.ts → hooks/use-multi-upload.d.ts} +4 -3
  19. package/dist/hooks/use-upload-controls.d.ts +44 -0
  20. package/dist/hooks/use-upload.d.ts +75 -0
  21. package/dist/index.d.ts +13 -8
  22. package/dist/index.js +1417 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/internal-helpers.d.ts +9 -0
  25. package/dist/s3-provider.d.ts +47 -0
  26. package/dist/store/index.d.ts +3 -0
  27. package/dist/store/local-storage-store.d.ts +9 -0
  28. package/dist/store/memory-store.d.ts +14 -0
  29. package/dist/types/download.d.ts +19 -2
  30. package/dist/types/error.d.ts +14 -0
  31. package/dist/types/index.d.ts +3 -0
  32. package/dist/types/s3-api.d.ts +158 -0
  33. package/dist/types/upload-store.d.ts +54 -0
  34. package/dist/types/upload.d.ts +56 -0
  35. package/dist/upload/constants.d.ts +1 -1
  36. package/dist/upload/multipart.d.ts +4 -3
  37. package/dist/upload/retry.d.ts +2 -1
  38. package/dist/upload/upload-file.d.ts +5 -1
  39. package/dist/upload/upload-files.d.ts +1 -1
  40. package/package.json +1 -4
  41. package/dist/helpers.d.ts +0 -7
  42. package/dist/use-fetch-download.d.ts +0 -33
  43. package/dist/use-upload-controls.d.ts +0 -63
  44. package/dist/use-upload.d.ts +0 -19
package/dist/index.js CHANGED
@@ -1,2 +1,1418 @@
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 { createContext, useContext, useState, useRef, useCallback } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/types/error.ts
5
+ var S3UploadError = class extends Error {
6
+ constructor(message, code, statusCode, phase) {
7
+ super(message);
8
+ this.name = "S3UploadError";
9
+ this.code = code;
10
+ this.statusCode = statusCode;
11
+ this.phase = phase;
12
+ }
13
+ };
14
+
15
+ // src/api.ts
16
+ function createS3Client(input) {
17
+ return input;
18
+ }
19
+ var createS3Api = createS3Client;
20
+ var S3Context = createContext(null);
21
+ function S3Provider({
22
+ client,
23
+ children
24
+ }) {
25
+ return /* @__PURE__ */ jsx(S3Context.Provider, { value: client, children });
26
+ }
27
+ function useS3Client() {
28
+ const ctx = useContext(S3Context);
29
+ if (!ctx) {
30
+ throw new Error(
31
+ "[better-s3] No S3Api client found. Either wrap your app with <S3Provider client={...}> or pass `api` directly to the hook."
32
+ );
33
+ }
34
+ return ctx;
35
+ }
36
+
37
+ // src/store/local-storage-store.ts
38
+ var STORAGE_PREFIX = "better-s3:upload:";
39
+ function createLocalStorageStore() {
40
+ return {
41
+ get(key, fileSize) {
42
+ try {
43
+ const raw = localStorage.getItem(STORAGE_PREFIX + key);
44
+ if (!raw) return null;
45
+ const stored = JSON.parse(raw);
46
+ return stored.fileSize === fileSize ? stored : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ },
51
+ set(upload) {
52
+ try {
53
+ localStorage.setItem(
54
+ STORAGE_PREFIX + upload.key,
55
+ JSON.stringify(upload)
56
+ );
57
+ } catch {
58
+ }
59
+ },
60
+ delete(key) {
61
+ try {
62
+ localStorage.removeItem(STORAGE_PREFIX + key);
63
+ } catch {
64
+ }
65
+ }
66
+ };
67
+ }
68
+
69
+ // src/store/memory-store.ts
70
+ function createMemoryStore() {
71
+ const map = /* @__PURE__ */ new Map();
72
+ return {
73
+ get(key, fileSize) {
74
+ const stored = map.get(key);
75
+ if (!stored) return null;
76
+ return stored.fileSize === fileSize ? stored : null;
77
+ },
78
+ set(upload) {
79
+ map.set(upload.key, upload);
80
+ },
81
+ delete(key) {
82
+ map.delete(key);
83
+ }
84
+ };
85
+ }
86
+
87
+ // src/upload/constants.ts
88
+ var DEFAULT_MULTIPART_THRESHOLD = 50 * 1024 * 1024;
89
+ var DEFAULT_PART_SIZE = 5 * 1024 * 1024;
90
+ var MAX_RETRIES = 3;
91
+ var RETRY_BASE_DELAY = 1e3;
92
+ var DEFAULT_CONCURRENT_PARTS = 2;
93
+ var DEFAULT_CONCURRENT_FILES = 2;
94
+
95
+ // src/upload/retry.ts
96
+ async function withRetry(fn, retryConfig, signal) {
97
+ const maxRetries = retryConfig?.maxRetries ?? MAX_RETRIES;
98
+ const baseDelay = retryConfig?.baseDelay ?? RETRY_BASE_DELAY;
99
+ let lastError;
100
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
101
+ try {
102
+ return await fn();
103
+ } catch (err) {
104
+ if (err.name === "AbortError") throw err;
105
+ lastError = err;
106
+ if (attempt < maxRetries) {
107
+ const delay = baseDelay * 2 ** attempt;
108
+ await new Promise((r) => setTimeout(r, delay));
109
+ if (signal?.aborted)
110
+ throw new DOMException("Upload aborted", "AbortError");
111
+ }
112
+ }
113
+ }
114
+ throw lastError;
115
+ }
116
+
117
+ // src/upload/simple.ts
118
+ function uploadSimple(file, url, fields, onProgress, signal) {
119
+ return new Promise((resolve, reject) => {
120
+ const xhr = new XMLHttpRequest();
121
+ const onAbort = () => {
122
+ xhr.abort();
123
+ reject(new DOMException("Upload aborted", "AbortError"));
124
+ };
125
+ signal?.addEventListener("abort", onAbort, { once: true });
126
+ xhr.upload.addEventListener("progress", (e) => {
127
+ if (e.lengthComputable) {
128
+ onProgress?.({
129
+ loaded: e.loaded,
130
+ total: e.total,
131
+ percent: Math.round(e.loaded / e.total * 100)
132
+ });
133
+ }
134
+ });
135
+ xhr.addEventListener("load", () => {
136
+ signal?.removeEventListener("abort", onAbort);
137
+ if (xhr.status >= 200 && xhr.status < 300) {
138
+ onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
139
+ resolve();
140
+ } else {
141
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
142
+ }
143
+ });
144
+ xhr.addEventListener("error", () => {
145
+ signal?.removeEventListener("abort", onAbort);
146
+ reject(new Error("Upload failed: network error"));
147
+ });
148
+ xhr.addEventListener("abort", () => {
149
+ signal?.removeEventListener("abort", onAbort);
150
+ reject(new DOMException("Upload aborted", "AbortError"));
151
+ });
152
+ const formData = new FormData();
153
+ for (const [k, v] of Object.entries(fields)) {
154
+ formData.append(k, v);
155
+ }
156
+ formData.append("file", file);
157
+ xhr.open("POST", url);
158
+ xhr.send(formData);
159
+ });
160
+ }
161
+ function uploadPut(file, url, headers, onProgress, signal) {
162
+ return new Promise((resolve, reject) => {
163
+ const xhr = new XMLHttpRequest();
164
+ const onAbort = () => {
165
+ xhr.abort();
166
+ reject(new DOMException("Upload aborted", "AbortError"));
167
+ };
168
+ signal?.addEventListener("abort", onAbort, { once: true });
169
+ xhr.upload.addEventListener("progress", (e) => {
170
+ if (e.lengthComputable) {
171
+ onProgress?.({
172
+ loaded: e.loaded,
173
+ total: e.total,
174
+ percent: Math.round(e.loaded / e.total * 100)
175
+ });
176
+ }
177
+ });
178
+ xhr.addEventListener("load", () => {
179
+ signal?.removeEventListener("abort", onAbort);
180
+ if (xhr.status >= 200 && xhr.status < 300) {
181
+ onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
182
+ resolve();
183
+ } else {
184
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
185
+ }
186
+ });
187
+ xhr.addEventListener("error", () => {
188
+ signal?.removeEventListener("abort", onAbort);
189
+ reject(new Error("Upload failed: network error"));
190
+ });
191
+ xhr.addEventListener("abort", () => {
192
+ signal?.removeEventListener("abort", onAbort);
193
+ reject(new DOMException("Upload aborted", "AbortError"));
194
+ });
195
+ xhr.open("PUT", url);
196
+ for (const [k, v] of Object.entries(headers)) {
197
+ xhr.setRequestHeader(k, v);
198
+ }
199
+ xhr.send(file);
200
+ });
201
+ }
202
+
203
+ // src/upload/part.ts
204
+ function uploadPart(blob, presignedUrl, partLoaded, totalSize, reportProgress, signal) {
205
+ return new Promise((resolve, reject) => {
206
+ const xhr = new XMLHttpRequest();
207
+ const onAbort = () => {
208
+ xhr.abort();
209
+ reject(new DOMException("Upload aborted", "AbortError"));
210
+ };
211
+ signal?.addEventListener("abort", onAbort, { once: true });
212
+ xhr.upload.addEventListener("progress", (e) => {
213
+ if (e.lengthComputable) {
214
+ partLoaded.bytes = e.loaded;
215
+ reportProgress();
216
+ }
217
+ });
218
+ xhr.addEventListener("load", () => {
219
+ signal?.removeEventListener("abort", onAbort);
220
+ if (xhr.status >= 200 && xhr.status < 300) {
221
+ partLoaded.bytes = blob.size;
222
+ reportProgress();
223
+ resolve();
224
+ } else {
225
+ reject(new Error(`Part upload failed: ${xhr.status}`));
226
+ }
227
+ });
228
+ xhr.addEventListener("error", () => {
229
+ signal?.removeEventListener("abort", onAbort);
230
+ reject(new Error("Part upload failed: network error"));
231
+ });
232
+ xhr.addEventListener("abort", () => {
233
+ signal?.removeEventListener("abort", onAbort);
234
+ reject(new DOMException("Upload aborted", "AbortError"));
235
+ });
236
+ xhr.open("PUT", presignedUrl);
237
+ xhr.send(blob);
238
+ });
239
+ }
240
+
241
+ // src/upload/multipart.ts
242
+ function resolvePartSize(partIndex, totalParts, partSize, fileSize) {
243
+ return partIndex === totalParts - 1 ? fileSize - partIndex * partSize : partSize;
244
+ }
245
+ async function uploadMultipart(api, file, objectKey, partSize, concurrentParts, onProgress, signal, requestOptions, retryConfig, uploadStore, onPartUpload, onMultipartInit) {
246
+ const bucket = requestOptions?.bucket;
247
+ const contentType = requestOptions?.contentType ?? file.type;
248
+ const store = uploadStore != null && uploadStore !== false ? uploadStore : null;
249
+ let uploadId;
250
+ let key;
251
+ const completedPartNumbers = /* @__PURE__ */ new Set();
252
+ const existing = store ? await store.get(objectKey, file.size) : null;
253
+ if (existing) {
254
+ try {
255
+ const { parts: parts2 } = await api.multipart.listParts({
256
+ key: existing.key,
257
+ uploadId: existing.uploadId,
258
+ bucket: bucket ?? existing.bucket
259
+ });
260
+ uploadId = existing.uploadId;
261
+ key = existing.key;
262
+ for (const p of parts2) completedPartNumbers.add(p.partNumber);
263
+ onMultipartInit?.(uploadId, key);
264
+ } catch {
265
+ await store?.delete(objectKey);
266
+ const result = await api.multipart.init({
267
+ key: objectKey,
268
+ contentType,
269
+ fileSize: file.size,
270
+ fileName: requestOptions?.fileName !== null ? requestOptions?.fileName ?? file.name : void 0,
271
+ metadata: requestOptions?.metadata,
272
+ bucket,
273
+ acl: requestOptions?.acl
274
+ });
275
+ uploadId = result.uploadId;
276
+ key = result.key;
277
+ await store?.set({ uploadId, key, fileSize: file.size, bucket });
278
+ onMultipartInit?.(uploadId, key);
279
+ }
280
+ } else {
281
+ const result = await api.multipart.init({
282
+ key: objectKey,
283
+ contentType,
284
+ fileSize: file.size,
285
+ fileName: requestOptions?.fileName !== null ? requestOptions?.fileName ?? file.name : void 0,
286
+ metadata: requestOptions?.metadata,
287
+ bucket,
288
+ acl: requestOptions?.acl
289
+ });
290
+ uploadId = result.uploadId;
291
+ key = result.key;
292
+ await store?.set({ uploadId, key, fileSize: file.size, bucket });
293
+ onMultipartInit?.(uploadId, key);
294
+ }
295
+ const totalParts = Math.ceil(file.size / partSize);
296
+ const partProgress = Array.from(
297
+ { length: totalParts },
298
+ (_, i) => ({
299
+ bytes: completedPartNumbers.has(i + 1) ? resolvePartSize(i, totalParts, partSize, file.size) : 0
300
+ })
301
+ );
302
+ const parts = Array.from(
303
+ completedPartNumbers,
304
+ (n) => ({ partNumber: n })
305
+ );
306
+ const reportProgress = () => {
307
+ const loaded = partProgress.reduce((sum, p) => sum + p.bytes, 0);
308
+ onProgress?.({
309
+ loaded,
310
+ total: file.size,
311
+ percent: Math.round(loaded / file.size * 100)
312
+ });
313
+ };
314
+ if (completedPartNumbers.size > 0) {
315
+ reportProgress();
316
+ }
317
+ try {
318
+ for (let batchStart = 0; batchStart < totalParts; batchStart += concurrentParts) {
319
+ if (signal?.aborted) {
320
+ throw new DOMException("Upload aborted", "AbortError");
321
+ }
322
+ const batchEnd = Math.min(batchStart + concurrentParts, totalParts);
323
+ const batch = [];
324
+ for (let i = batchStart; i < batchEnd; i++) {
325
+ const partNumber = i + 1;
326
+ if (completedPartNumbers.has(partNumber)) continue;
327
+ const start = i * partSize;
328
+ const end = Math.min(start + partSize, file.size);
329
+ const blob = file.slice(start, end);
330
+ batch.push(
331
+ withRetry(
332
+ async () => {
333
+ const { presignedUrl } = await api.multipart.signPart({
334
+ key,
335
+ uploadId,
336
+ partNumber,
337
+ partSize: blob.size,
338
+ bucket
339
+ });
340
+ partProgress[i].bytes = 0;
341
+ await uploadPart(
342
+ blob,
343
+ presignedUrl,
344
+ partProgress[i],
345
+ file.size,
346
+ reportProgress,
347
+ signal
348
+ );
349
+ onPartUpload?.(partNumber, totalParts);
350
+ return { partNumber };
351
+ },
352
+ retryConfig,
353
+ signal
354
+ )
355
+ );
356
+ }
357
+ const batchResults = await Promise.all(batch);
358
+ parts.push(...batchResults);
359
+ }
360
+ parts.sort((a, b) => a.partNumber - b.partNumber);
361
+ const result = await api.multipart.complete({
362
+ key,
363
+ uploadId,
364
+ parts,
365
+ bucket
366
+ });
367
+ await store?.delete(objectKey);
368
+ onProgress?.({ loaded: file.size, total: file.size, percent: 100 });
369
+ return result.eTag;
370
+ } catch (err) {
371
+ if (store === null) {
372
+ api.multipart.abort({ key, uploadId, bucket }).catch(() => {
373
+ });
374
+ }
375
+ throw err;
376
+ }
377
+ }
378
+
379
+ // src/upload/upload-file.ts
380
+ async function uploadFile(api, file, objectKey, config = {}, callbacks = {}, signal, requestOptions) {
381
+ const threshold = config.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
382
+ const useMultipart = config.multipart === true && file.size >= threshold;
383
+ const concurrentParts = config.concurrentParts ?? DEFAULT_CONCURRENT_PARTS;
384
+ const partSize = requestOptions?.partSize ?? config.partSize ?? DEFAULT_PART_SIZE;
385
+ const contentType = requestOptions?.contentType ?? file.type;
386
+ let eTag;
387
+ if (useMultipart) {
388
+ callbacks.onPhaseChange?.("uploading");
389
+ eTag = await uploadMultipart(
390
+ api,
391
+ file,
392
+ objectKey,
393
+ partSize,
394
+ concurrentParts,
395
+ callbacks.onProgress,
396
+ signal,
397
+ requestOptions,
398
+ config.retry,
399
+ config.uploadStore,
400
+ callbacks.onPartUpload,
401
+ callbacks.onMultipartInit
402
+ );
403
+ } else {
404
+ await withRetry(
405
+ async () => {
406
+ callbacks.onPhaseChange?.("presigning");
407
+ const presign = await api.upload({
408
+ key: objectKey,
409
+ contentType,
410
+ fileSize: file.size,
411
+ fileName: requestOptions?.fileName !== null ? requestOptions?.fileName ?? file.name : void 0,
412
+ metadata: requestOptions?.metadata,
413
+ bucket: requestOptions?.bucket,
414
+ acl: requestOptions?.acl
415
+ });
416
+ callbacks.onPhaseChange?.("uploading");
417
+ if (presign.method === "put") {
418
+ await uploadPut(
419
+ file,
420
+ presign.url,
421
+ presign.headers ?? {},
422
+ callbacks.onProgress,
423
+ signal
424
+ );
425
+ } else {
426
+ await uploadSimple(
427
+ file,
428
+ presign.url,
429
+ presign.fields ?? {},
430
+ callbacks.onProgress,
431
+ signal
432
+ );
433
+ }
434
+ },
435
+ config.retry,
436
+ signal
437
+ );
438
+ callbacks.onPhaseChange?.("finalizing");
439
+ const confirmed = await api.confirm({
440
+ key: objectKey,
441
+ bucket: requestOptions?.bucket
442
+ });
443
+ eTag = confirmed.eTag;
444
+ }
445
+ return { key: objectKey, eTag };
446
+ }
447
+
448
+ // src/upload/upload-files.ts
449
+ async function uploadFiles(api, items, config = {}, callbacks = {}, signal, getRequestOptions) {
450
+ const results = items.map((item) => ({
451
+ ...item,
452
+ status: "pending",
453
+ progress: { loaded: 0, total: item.file.size, percent: 0 },
454
+ result: null,
455
+ error: null
456
+ }));
457
+ const reportTotalProgress = () => {
458
+ const loaded = results.reduce((sum, r) => sum + r.progress.loaded, 0);
459
+ const total = results.reduce((sum, r) => sum + r.progress.total, 0);
460
+ callbacks.onTotalProgress?.({
461
+ loaded,
462
+ total,
463
+ percent: total > 0 ? Math.round(loaded / total * 100) : 0
464
+ });
465
+ };
466
+ let nextIndex = 0;
467
+ const processNext = async () => {
468
+ while (nextIndex < results.length) {
469
+ if (signal?.aborted) return;
470
+ const idx = nextIndex++;
471
+ const item = results[idx];
472
+ item.status = "uploading";
473
+ try {
474
+ const result = await uploadFile(
475
+ api,
476
+ item.file,
477
+ item.objectKey,
478
+ config,
479
+ {
480
+ onProgress: (progress) => {
481
+ item.progress = progress;
482
+ callbacks.onFileProgress?.(item.id, progress);
483
+ reportTotalProgress();
484
+ }
485
+ },
486
+ signal,
487
+ getRequestOptions?.(item.file)
488
+ );
489
+ item.status = "success";
490
+ item.result = result;
491
+ item.progress = {
492
+ loaded: item.file.size,
493
+ total: item.file.size,
494
+ percent: 100
495
+ };
496
+ callbacks.onFileSuccess?.(item.id, result);
497
+ reportTotalProgress();
498
+ } catch (err) {
499
+ if (err.name === "AbortError") {
500
+ item.status = "error";
501
+ item.error = "Upload cancelled";
502
+ return;
503
+ }
504
+ const message = err instanceof Error ? err.message : "Upload failed";
505
+ item.status = "error";
506
+ item.error = message;
507
+ callbacks.onFileError?.(item.id, message);
508
+ reportTotalProgress();
509
+ }
510
+ }
511
+ };
512
+ const concurrentFiles = config.concurrentFiles ?? DEFAULT_CONCURRENT_FILES;
513
+ const workers = Array.from(
514
+ { length: Math.min(concurrentFiles, items.length) },
515
+ () => processNext()
516
+ );
517
+ await Promise.all(workers);
518
+ return results;
519
+ }
520
+
521
+ // src/helpers/format-file-size.ts
522
+ function formatFileSize(bytes) {
523
+ if (bytes === 0) return "0 B";
524
+ const units = ["B", "KB", "MB", "GB", "TB"];
525
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
526
+ const size = bytes / Math.pow(1024, i);
527
+ return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
528
+ }
529
+
530
+ // src/helpers/validate-file.ts
531
+ function validateFile(file, options) {
532
+ if (options.accept?.length) {
533
+ const allowed = options.accept.some((type) => {
534
+ if (type.startsWith(".")) {
535
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
536
+ }
537
+ if (type.endsWith("/*")) {
538
+ return file.type.startsWith(type.replace("/*", "/"));
539
+ }
540
+ return file.type === type;
541
+ });
542
+ if (!allowed) {
543
+ const ext = file.name.includes(".") ? file.name.split(".").pop() : null;
544
+ return `File type "${ext ? `.${ext}` : file.type || "unknown"}" is not allowed`;
545
+ }
546
+ }
547
+ if (file.size === 0) {
548
+ return "File is empty";
549
+ }
550
+ if (options.maxFileSize && file.size > options.maxFileSize) {
551
+ const maxMB = (options.maxFileSize / (1024 * 1024)).toFixed(1);
552
+ return `File size exceeds ${maxMB} MB limit`;
553
+ }
554
+ return null;
555
+ }
556
+
557
+ // src/helpers/parse-content-disposition.ts
558
+ function parseContentDispositionFilename(header, fallback) {
559
+ if (!header) return fallback;
560
+ const starMatch = header.match(/filename\*=UTF-8''([^;,\s]+)/i);
561
+ if (starMatch) {
562
+ try {
563
+ return decodeURIComponent(starMatch[1]);
564
+ } catch {
565
+ }
566
+ }
567
+ const match = header.match(/filename="([^"]+)"/i);
568
+ if (match) return match[1];
569
+ return fallback;
570
+ }
571
+
572
+ // src/helpers/format-upload-progress.ts
573
+ function formatUploadProgress(loaded, total, percent) {
574
+ const loadedStr = formatFileSize(loaded);
575
+ if (!total) return loadedStr;
576
+ return `${loadedStr} / ${formatFileSize(total)} (${Math.round(percent)}%)`;
577
+ }
578
+
579
+ // src/helpers/format-speed.ts
580
+ function formatSpeed(bytesPerSecond) {
581
+ return `${formatFileSize(bytesPerSecond)}/s`;
582
+ }
583
+
584
+ // src/helpers/format-eta.ts
585
+ function formatEta(remainingBytes, bytesPerSecond) {
586
+ if (bytesPerSecond <= 0 || remainingBytes <= 0) return null;
587
+ const totalSeconds = remainingBytes / bytesPerSecond;
588
+ if (totalSeconds < 60) return `${Math.ceil(totalSeconds)}s`;
589
+ const totalMinutes = totalSeconds / 60;
590
+ if (totalMinutes < 60) return `${Math.ceil(totalMinutes)}m`;
591
+ const hours = Math.floor(totalMinutes / 60);
592
+ const mins = Math.ceil(totalMinutes % 60);
593
+ return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
594
+ }
595
+
596
+ // src/helpers/build-object-key.ts
597
+ function buildObjectKey(...parts) {
598
+ return parts.map((p) => p.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
599
+ }
600
+
601
+ // src/helpers/get-file-extension.ts
602
+ function getFileExtension(filename) {
603
+ const dotIndex = filename.lastIndexOf(".");
604
+ if (dotIndex < 1) return "";
605
+ return filename.slice(dotIndex + 1).toLowerCase();
606
+ }
607
+
608
+ // src/helpers/truncate-filename.ts
609
+ function truncateFilename(name, maxChars = 26) {
610
+ if (name.length <= maxChars) return name;
611
+ const dotIndex = name.lastIndexOf(".");
612
+ if (dotIndex <= 0) {
613
+ return name.slice(0, maxChars - 1) + "\u2026";
614
+ }
615
+ const ext = name.slice(dotIndex);
616
+ const available = maxChars - ext.length - 1;
617
+ if (available <= 0) {
618
+ return name.slice(0, maxChars - 1) + "\u2026";
619
+ }
620
+ return name.slice(0, available) + "\u2026 " + ext;
621
+ }
622
+
623
+ // src/helpers/speed-tracker.ts
624
+ function createSpeedTracker(windowMs = 3e3) {
625
+ const samples = [];
626
+ return {
627
+ /**
628
+ * Record the latest cumulative `loaded` byte count.
629
+ * Returns the current speed in bytes/second (0 until at least 2 samples exist).
630
+ */
631
+ update(loaded) {
632
+ const now = Date.now();
633
+ samples.push({ t: now, loaded });
634
+ const cutoff = now - windowMs;
635
+ while (samples.length > 1 && samples[0].t < cutoff) {
636
+ samples.shift();
637
+ }
638
+ if (samples.length < 2) return 0;
639
+ const oldest = samples[0];
640
+ const newest = samples[samples.length - 1];
641
+ const deltaMs = newest.t - oldest.t;
642
+ const deltaBytes = newest.loaded - oldest.loaded;
643
+ return deltaMs > 0 ? Math.round(deltaBytes / deltaMs * 1e3) : 0;
644
+ },
645
+ reset() {
646
+ samples.length = 0;
647
+ }
648
+ };
649
+ }
650
+ function useLiveRef(value) {
651
+ const ref = useRef(value);
652
+ ref.current = value;
653
+ return ref;
654
+ }
655
+
656
+ // src/hooks/use-upload.ts
657
+ var INITIAL_PROGRESS = { loaded: 0, total: 0, percent: 0 };
658
+ var INITIAL_STATE = {
659
+ phase: "idle",
660
+ progress: INITIAL_PROGRESS,
661
+ error: null,
662
+ result: null,
663
+ fileName: null,
664
+ fileSize: null
665
+ };
666
+ function useUpload(options) {
667
+ const [state, setState] = useState(INITIAL_STATE);
668
+ const contextApi = useContext(S3Context);
669
+ const optsRef = useLiveRef(options);
670
+ const apiRef = useLiveRef(contextApi);
671
+ const abortRef = useRef(null);
672
+ const activeUploadRef = useRef(null);
673
+ const detachingRef = useRef(false);
674
+ const speedTrackerRef = useRef(createSpeedTracker());
675
+ const lastSpeedRef = useRef(void 0);
676
+ const lastSpeedUpdateRef = useRef(0);
677
+ const upload = useCallback(
678
+ async (file, objectKey, requestOptions) => {
679
+ setState({
680
+ ...INITIAL_STATE,
681
+ phase: "validating",
682
+ fileName: file.name,
683
+ fileSize: file.size
684
+ });
685
+ const opts = optsRef.current;
686
+ const api = opts.api ?? apiRef.current;
687
+ if (!api)
688
+ throw new Error(
689
+ "[better-s3] No S3Api client found. Pass `api` to useUpload or wrap with <S3Provider>."
690
+ );
691
+ const validationError = validateFile(file, {
692
+ accept: opts.accept,
693
+ maxFileSize: opts.maxFileSize
694
+ });
695
+ if (validationError) {
696
+ setState((s) => ({ ...s, phase: "error", error: validationError }));
697
+ opts.onError?.(file, new Error(validationError), "validating");
698
+ return;
699
+ }
700
+ if (opts.beforeUpload) {
701
+ const allowed = await opts.beforeUpload(file);
702
+ if (!allowed) {
703
+ setState((s) => ({
704
+ ...s,
705
+ phase: "error",
706
+ error: "Upload blocked by beforeUpload hook"
707
+ }));
708
+ opts.onError?.(file, new Error("blocked"), "validating");
709
+ return;
710
+ }
711
+ }
712
+ speedTrackerRef.current.reset();
713
+ lastSpeedRef.current = void 0;
714
+ lastSpeedUpdateRef.current = 0;
715
+ setState((s) => ({ ...s, phase: "presigning" }));
716
+ opts.onUploadStart?.(file, objectKey);
717
+ const controller = new AbortController();
718
+ abortRef.current = controller;
719
+ activeUploadRef.current = {
720
+ file,
721
+ objectKey,
722
+ serverKey: objectKey,
723
+ bucket: requestOptions?.bucket,
724
+ requestOptions
725
+ };
726
+ try {
727
+ const result = await uploadFile(
728
+ api,
729
+ file,
730
+ objectKey,
731
+ {
732
+ multipart: opts.multipart,
733
+ multipartThreshold: opts.multipartThreshold,
734
+ concurrentParts: opts.concurrentParts,
735
+ partSize: opts.partSize,
736
+ retry: opts.retry,
737
+ uploadStore: opts.uploadStore
738
+ },
739
+ {
740
+ onProgress: (progress) => {
741
+ const rawSpeed = speedTrackerRef.current.update(progress.loaded);
742
+ const now = Date.now();
743
+ if (rawSpeed > 0 && now - lastSpeedUpdateRef.current >= 500) {
744
+ lastSpeedRef.current = rawSpeed;
745
+ lastSpeedUpdateRef.current = now;
746
+ }
747
+ const speed = lastSpeedRef.current;
748
+ const p = speed ? { ...progress, speed } : progress;
749
+ setState((s) => ({ ...s, progress: p }));
750
+ opts.onProgress?.(file, p);
751
+ },
752
+ onPhaseChange: (phase) => setState((s) => ({ ...s, phase })),
753
+ onPartUpload: (partNumber, totalParts) => opts.onPartUpload?.(file, partNumber, totalParts),
754
+ onMultipartInit: (uploadId, serverKey) => {
755
+ if (activeUploadRef.current) {
756
+ activeUploadRef.current.uploadId = uploadId;
757
+ activeUploadRef.current.serverKey = serverKey;
758
+ }
759
+ opts.onMultipartInit?.(file, uploadId);
760
+ }
761
+ },
762
+ controller.signal,
763
+ requestOptions
764
+ );
765
+ setState((s) => ({
766
+ ...s,
767
+ phase: "success",
768
+ result,
769
+ progress: { loaded: file.size, total: file.size, percent: 100 }
770
+ }));
771
+ await opts.onSuccess?.(file, result);
772
+ } catch (err) {
773
+ if (err.name === "AbortError") {
774
+ if (detachingRef.current) {
775
+ detachingRef.current = false;
776
+ return;
777
+ }
778
+ opts.onCancel?.(file);
779
+ setState(INITIAL_STATE);
780
+ return;
781
+ }
782
+ const message = err instanceof Error ? err.message : "Upload failed";
783
+ setState((s) => ({ ...s, phase: "error", error: message }));
784
+ opts.onError?.(file, err, "uploading");
785
+ } finally {
786
+ abortRef.current = null;
787
+ activeUploadRef.current = null;
788
+ }
789
+ },
790
+ []
791
+ );
792
+ const cancel = useCallback(() => {
793
+ const opts = optsRef.current;
794
+ const api = opts.api ?? apiRef.current;
795
+ const active = activeUploadRef.current;
796
+ abortRef.current?.abort();
797
+ if (active && api) {
798
+ const { objectKey, serverKey, uploadId, bucket } = active;
799
+ const store = opts.uploadStore;
800
+ if (store != null && store !== false) {
801
+ void Promise.resolve(store.delete(objectKey)).catch(() => {
802
+ });
803
+ }
804
+ if (uploadId) {
805
+ api.multipart.abort({ key: serverKey, uploadId, bucket }).catch(() => {
806
+ });
807
+ }
808
+ }
809
+ setState(INITIAL_STATE);
810
+ }, []);
811
+ const detach = useCallback(() => {
812
+ const active = activeUploadRef.current;
813
+ if (!active) return;
814
+ detachingRef.current = true;
815
+ abortRef.current?.abort();
816
+ setState(INITIAL_STATE);
817
+ }, []);
818
+ const reset = useCallback(() => {
819
+ abortRef.current?.abort();
820
+ setState(INITIAL_STATE);
821
+ }, []);
822
+ return { ...state, upload, cancel, detach, reset };
823
+ }
824
+ var INITIAL_PROGRESS2 = { loaded: 0, total: 0, percent: 0 };
825
+ var INITIAL_STATE2 = {
826
+ phase: "idle",
827
+ files: [],
828
+ totalProgress: INITIAL_PROGRESS2,
829
+ error: null
830
+ };
831
+ function generateId() {
832
+ return crypto.randomUUID();
833
+ }
834
+ function useMultiUpload(options) {
835
+ const [state, setState] = useState(INITIAL_STATE2);
836
+ const contextApi = useContext(S3Context);
837
+ const optsRef = useLiveRef(options);
838
+ const apiRef = useLiveRef(contextApi);
839
+ const abortRef = useRef(null);
840
+ const resettingRef = useRef(false);
841
+ const fileMapRef = useRef(/* @__PURE__ */ new Map());
842
+ const fileSpeedTrackersRef = useRef(/* @__PURE__ */ new Map());
843
+ const totalSpeedTrackerRef = useRef(createSpeedTracker());
844
+ const fileLastSpeedRef = useRef(/* @__PURE__ */ new Map());
845
+ const fileLastSpeedUpdateRef = useRef(/* @__PURE__ */ new Map());
846
+ const lastTotalSpeedRef = useRef(void 0);
847
+ const lastTotalSpeedUpdateRef = useRef(0);
848
+ const upload = useCallback(
849
+ async (files, resolveKey) => {
850
+ const opts = optsRef.current;
851
+ const api = opts.api ?? apiRef.current;
852
+ if (!api)
853
+ throw new Error(
854
+ "[better-s3] No S3Api client found. Pass `api` to useMultiUpload or wrap with <S3Provider>."
855
+ );
856
+ const items = [];
857
+ const fileStates = [];
858
+ const fileMap = /* @__PURE__ */ new Map();
859
+ setState((s) => ({ ...s, phase: "validating", error: null }));
860
+ if (opts.maxFiles && files.length > opts.maxFiles) {
861
+ const msg = `Too many files. Maximum is ${opts.maxFiles}.`;
862
+ setState((s) => ({ ...s, phase: "error", error: msg }));
863
+ opts.onError?.(new Error(msg));
864
+ return;
865
+ }
866
+ for (const file of files) {
867
+ const validationError = validateFile(file, {
868
+ accept: opts.accept,
869
+ maxFileSize: opts.maxFileSize
870
+ });
871
+ if (validationError) {
872
+ const msg = `${file.name}: ${validationError}`;
873
+ setState((s) => ({ ...s, phase: "error", error: msg }));
874
+ opts.onError?.(new Error(msg));
875
+ return;
876
+ }
877
+ }
878
+ if (opts.beforeUpload) {
879
+ const allowed = await opts.beforeUpload(files);
880
+ if (!allowed) {
881
+ setState((s) => ({
882
+ ...s,
883
+ phase: "error",
884
+ error: "Upload blocked by beforeUpload hook"
885
+ }));
886
+ opts.onError?.(new Error("blocked"));
887
+ return;
888
+ }
889
+ }
890
+ for (const file of files) {
891
+ const id = generateId();
892
+ const objectKey = resolveKey(file);
893
+ items.push({ id, file, objectKey });
894
+ fileMap.set(id, file);
895
+ fileStates.push({
896
+ id,
897
+ fileName: file.name,
898
+ fileSize: file.size,
899
+ status: "pending",
900
+ progress: { loaded: 0, total: file.size, percent: 0 },
901
+ error: null
902
+ });
903
+ }
904
+ fileMapRef.current = fileMap;
905
+ setState({
906
+ phase: "uploading",
907
+ files: fileStates,
908
+ totalProgress: {
909
+ loaded: 0,
910
+ total: files.reduce((s, f) => s + f.size, 0),
911
+ percent: 0
912
+ },
913
+ error: null
914
+ });
915
+ opts.onUploadStart?.(files);
916
+ fileSpeedTrackersRef.current.clear();
917
+ for (const item of items) {
918
+ fileSpeedTrackersRef.current.set(item.id, createSpeedTracker());
919
+ }
920
+ totalSpeedTrackerRef.current.reset();
921
+ fileLastSpeedRef.current.clear();
922
+ fileLastSpeedUpdateRef.current.clear();
923
+ lastTotalSpeedRef.current = void 0;
924
+ lastTotalSpeedUpdateRef.current = 0;
925
+ const controller = new AbortController();
926
+ abortRef.current = controller;
927
+ try {
928
+ const results = await uploadFiles(
929
+ api,
930
+ items,
931
+ {
932
+ multipart: opts.multipart,
933
+ multipartThreshold: opts.multipartThreshold,
934
+ concurrentParts: opts.concurrentParts,
935
+ concurrentFiles: opts.concurrentFiles,
936
+ partSize: opts.partSize,
937
+ retry: opts.retry,
938
+ uploadStore: opts.uploadStore
939
+ },
940
+ {
941
+ onFileProgress: (id, progress) => {
942
+ const tracker = fileSpeedTrackersRef.current.get(id);
943
+ const rawSpeed = tracker ? tracker.update(progress.loaded) : 0;
944
+ const now = Date.now();
945
+ if (rawSpeed > 0 && now - (fileLastSpeedUpdateRef.current.get(id) ?? 0) >= 500) {
946
+ fileLastSpeedRef.current.set(id, rawSpeed);
947
+ fileLastSpeedUpdateRef.current.set(id, now);
948
+ }
949
+ const speed = fileLastSpeedRef.current.get(id);
950
+ const p = speed ? { ...progress, speed } : progress;
951
+ setState((s) => ({
952
+ ...s,
953
+ files: s.files.map(
954
+ (f) => f.id === id ? { ...f, status: "uploading", progress: p } : f
955
+ )
956
+ }));
957
+ const file = fileMap.get(id);
958
+ if (file) opts.onFileProgress?.(file, p);
959
+ },
960
+ onFileSuccess: (id, result) => {
961
+ setState((s) => ({
962
+ ...s,
963
+ files: s.files.map(
964
+ (f) => f.id === id ? {
965
+ ...f,
966
+ status: "success",
967
+ progress: {
968
+ loaded: f.fileSize,
969
+ total: f.fileSize,
970
+ percent: 100
971
+ }
972
+ } : f
973
+ )
974
+ }));
975
+ const file = fileMap.get(id);
976
+ if (file) opts.onFileSuccess?.(file, result);
977
+ },
978
+ onFileError: (id, error) => {
979
+ setState((s) => ({
980
+ ...s,
981
+ files: s.files.map(
982
+ (f) => f.id === id ? { ...f, status: "error", error } : f
983
+ )
984
+ }));
985
+ const file = fileMap.get(id);
986
+ if (file) opts.onFileError?.(file, error);
987
+ },
988
+ onTotalProgress: (progress) => {
989
+ const rawSpeed = totalSpeedTrackerRef.current.update(
990
+ progress.loaded
991
+ );
992
+ const now = Date.now();
993
+ if (rawSpeed > 0 && now - lastTotalSpeedUpdateRef.current >= 1e3) {
994
+ lastTotalSpeedRef.current = rawSpeed;
995
+ lastTotalSpeedUpdateRef.current = now;
996
+ }
997
+ const speed = lastTotalSpeedRef.current;
998
+ const p = speed ? { ...progress, speed } : progress;
999
+ setState((s) => ({ ...s, totalProgress: p }));
1000
+ opts.onProgress?.(p);
1001
+ }
1002
+ },
1003
+ controller.signal,
1004
+ (file) => {
1005
+ const perFile = opts.getUploadOptions?.(file);
1006
+ if (!opts.uploadOptions) return perFile ?? {};
1007
+ return { ...opts.uploadOptions, ...perFile };
1008
+ }
1009
+ );
1010
+ const hasErrors = results.some((r) => r.status === "error");
1011
+ const successResults = results.filter((r) => r.result !== null).map((r) => r.result);
1012
+ setState((s) => ({
1013
+ ...s,
1014
+ phase: hasErrors ? "error" : "success",
1015
+ error: hasErrors ? `${results.filter((r) => r.status === "error").length} file(s) failed` : null,
1016
+ totalProgress: hasErrors ? s.totalProgress : {
1017
+ loaded: s.totalProgress.total,
1018
+ total: s.totalProgress.total,
1019
+ percent: 100
1020
+ }
1021
+ }));
1022
+ if (!hasErrors) {
1023
+ await opts.onSuccess?.(successResults);
1024
+ }
1025
+ } catch (err) {
1026
+ if (err.name === "AbortError") {
1027
+ if (!resettingRef.current) opts.onCancel?.();
1028
+ resettingRef.current = false;
1029
+ setState(INITIAL_STATE2);
1030
+ return;
1031
+ }
1032
+ const message = err instanceof Error ? err.message : "Upload failed";
1033
+ setState((s) => ({ ...s, phase: "error", error: message }));
1034
+ opts.onError?.(err);
1035
+ } finally {
1036
+ abortRef.current = null;
1037
+ }
1038
+ },
1039
+ []
1040
+ );
1041
+ const cancel = useCallback(() => {
1042
+ abortRef.current?.abort();
1043
+ setState(INITIAL_STATE2);
1044
+ }, []);
1045
+ const reset = useCallback(() => {
1046
+ resettingRef.current = true;
1047
+ abortRef.current?.abort();
1048
+ setState(INITIAL_STATE2);
1049
+ }, []);
1050
+ return { ...state, upload, cancel, reset };
1051
+ }
1052
+ function useUploadControls(options) {
1053
+ const single = useUpload(options);
1054
+ const inputRef = useRef(null);
1055
+ const [fileInfo, setFileInfo] = useState(null);
1056
+ const resolveKey = (file) => typeof options.objectKey === "function" ? options.objectKey(file) : options.objectKey;
1057
+ const handleFiles = async (files) => {
1058
+ const file = files?.[0];
1059
+ if (!file) return;
1060
+ setFileInfo({ name: file.name, size: file.size });
1061
+ await single.upload(file, resolveKey(file), {
1062
+ ...options.uploadOptions,
1063
+ ...options.getUploadOptions?.(file)
1064
+ });
1065
+ };
1066
+ return {
1067
+ phase: single.phase,
1068
+ fileInfo,
1069
+ progress: single.progress,
1070
+ error: single.error,
1071
+ isUploading: single.phase === "uploading",
1072
+ handleFiles,
1073
+ openFilePicker: () => inputRef.current?.click(),
1074
+ cancel: single.cancel,
1075
+ detach: single.detach,
1076
+ reset: () => {
1077
+ single.reset();
1078
+ setFileInfo(null);
1079
+ },
1080
+ inputProps: {
1081
+ ref: inputRef,
1082
+ type: "file",
1083
+ accept: options.accept?.join(","),
1084
+ hidden: true,
1085
+ onChange: (e) => {
1086
+ handleFiles(e.target.files);
1087
+ e.target.value = "";
1088
+ }
1089
+ },
1090
+ dropHandlers: {
1091
+ onDragOver: (e) => {
1092
+ e.preventDefault();
1093
+ e.stopPropagation();
1094
+ },
1095
+ onDrop: (e) => {
1096
+ e.preventDefault();
1097
+ e.stopPropagation();
1098
+ if (single.phase !== "uploading") handleFiles(e.dataTransfer.files);
1099
+ }
1100
+ }
1101
+ };
1102
+ }
1103
+ function useMultiUploadControls(options) {
1104
+ const multi = useMultiUpload(options);
1105
+ const inputRef = useRef(null);
1106
+ const resolveKey = (file) => typeof options.objectKey === "function" ? options.objectKey(file) : options.objectKey;
1107
+ const handleFiles = async (files) => {
1108
+ if (!files?.length) return;
1109
+ await multi.upload(Array.from(files), resolveKey);
1110
+ };
1111
+ return {
1112
+ phase: multi.phase,
1113
+ files: multi.files,
1114
+ totalProgress: multi.totalProgress,
1115
+ error: multi.error,
1116
+ isUploading: multi.phase === "uploading",
1117
+ handleFiles,
1118
+ openFilePicker: () => inputRef.current?.click(),
1119
+ cancel: multi.cancel,
1120
+ reset: multi.reset,
1121
+ inputProps: {
1122
+ ref: inputRef,
1123
+ type: "file",
1124
+ multiple: true,
1125
+ accept: options.accept?.join(","),
1126
+ hidden: true,
1127
+ onChange: (e) => {
1128
+ handleFiles(e.target.files);
1129
+ e.target.value = "";
1130
+ }
1131
+ },
1132
+ dropHandlers: {
1133
+ onDragOver: (e) => {
1134
+ e.preventDefault();
1135
+ e.stopPropagation();
1136
+ },
1137
+ onDrop: (e) => {
1138
+ e.preventDefault();
1139
+ e.stopPropagation();
1140
+ if (multi.phase !== "uploading") handleFiles(e.dataTransfer.files);
1141
+ }
1142
+ }
1143
+ };
1144
+ }
1145
+ var INITIAL_STATE3 = {
1146
+ phase: "idle",
1147
+ error: null,
1148
+ url: null,
1149
+ expiresIn: null
1150
+ };
1151
+ function useDownload(options) {
1152
+ const [state, setState] = useState(INITIAL_STATE3);
1153
+ const contextApi = useContext(S3Context);
1154
+ const optsRef = useLiveRef(options);
1155
+ const apiRef = useLiveRef(contextApi);
1156
+ const presign = useCallback(async (key, downloadName) => {
1157
+ const opts = optsRef.current;
1158
+ const api = opts.api ?? apiRef.current;
1159
+ if (!api)
1160
+ throw new Error(
1161
+ "[better-s3] No S3Api client found. Pass `api` to useDownload or wrap with <S3Provider>."
1162
+ );
1163
+ setState({ phase: "presigning", error: null, url: null, expiresIn: null });
1164
+ try {
1165
+ const result = await api.download(key, {
1166
+ fileName: downloadName,
1167
+ bucket: opts.bucket
1168
+ });
1169
+ setState({
1170
+ phase: "idle",
1171
+ error: null,
1172
+ url: result.url,
1173
+ expiresIn: result.expiresIn
1174
+ });
1175
+ return { url: result.url, expiresIn: result.expiresIn };
1176
+ } catch (err) {
1177
+ const message = err instanceof Error ? err.message : "Download failed";
1178
+ setState({ phase: "error", error: message, url: null, expiresIn: null });
1179
+ opts.onError?.(key, err);
1180
+ return null;
1181
+ }
1182
+ }, []);
1183
+ const download = useCallback(
1184
+ async (key, downloadName) => {
1185
+ const opts = optsRef.current;
1186
+ if (opts.beforeDownload) {
1187
+ const allowed = await opts.beforeDownload(key);
1188
+ if (!allowed) {
1189
+ setState({
1190
+ phase: "error",
1191
+ error: "Download blocked by beforeDownload hook",
1192
+ url: null,
1193
+ expiresIn: null
1194
+ });
1195
+ opts.onError?.(key, new Error("blocked"));
1196
+ return;
1197
+ }
1198
+ }
1199
+ const result = await presign(key, downloadName);
1200
+ if (!result) return;
1201
+ window.location.href = result.url;
1202
+ opts.onInitiated?.(key);
1203
+ },
1204
+ [presign]
1205
+ );
1206
+ const reset = useCallback(() => setState(INITIAL_STATE3), []);
1207
+ return { ...state, download, presign, reset };
1208
+ }
1209
+ var INITIAL_PROGRESS3 = { loaded: 0, total: 0, percent: 0 };
1210
+ var INITIAL_STATE4 = {
1211
+ phase: "idle",
1212
+ progress: INITIAL_PROGRESS3,
1213
+ error: null,
1214
+ fileName: null,
1215
+ fileSize: null
1216
+ };
1217
+ function useFetchDownload(options) {
1218
+ const [state, setState] = useState(INITIAL_STATE4);
1219
+ const contextApi = useContext(S3Context);
1220
+ const optsRef = useLiveRef(options);
1221
+ const apiRef = useLiveRef(contextApi);
1222
+ const abortRef = useRef(null);
1223
+ const resettingRef = useRef(false);
1224
+ const speedTrackerRef = useRef(createSpeedTracker());
1225
+ const lastSpeedRef = useRef(void 0);
1226
+ const lastSpeedUpdateRef = useRef(0);
1227
+ const download = useCallback(async (key, downloadName) => {
1228
+ const fallback = key.split("/").pop() ?? key;
1229
+ const opts = optsRef.current;
1230
+ const api = opts.api ?? apiRef.current;
1231
+ if (!api)
1232
+ throw new Error(
1233
+ "[better-s3] No S3Api client found. Pass `api` to useFetchDownload or wrap with <S3Provider>."
1234
+ );
1235
+ if (opts.beforeDownload) {
1236
+ const allowed = await opts.beforeDownload(key);
1237
+ if (!allowed) {
1238
+ setState((s) => ({
1239
+ ...s,
1240
+ phase: "error",
1241
+ error: "Download blocked by beforeDownload hook"
1242
+ }));
1243
+ opts.onError?.(key, new Error("blocked"), "presigning");
1244
+ return;
1245
+ }
1246
+ }
1247
+ setState({
1248
+ phase: "presigning",
1249
+ progress: INITIAL_PROGRESS3,
1250
+ error: null,
1251
+ fileName: downloadName ?? null,
1252
+ fileSize: null
1253
+ });
1254
+ try {
1255
+ const { url } = await api.download(key, {
1256
+ fileName: downloadName,
1257
+ bucket: opts.bucket
1258
+ });
1259
+ setState((s) => ({ ...s, phase: "downloading" }));
1260
+ opts.onDownloadStart?.(key);
1261
+ speedTrackerRef.current.reset();
1262
+ lastSpeedRef.current = void 0;
1263
+ lastSpeedUpdateRef.current = 0;
1264
+ const controller = new AbortController();
1265
+ abortRef.current = controller;
1266
+ const res = await fetch(url, { signal: controller.signal });
1267
+ if (!res.ok) {
1268
+ throw new Error(
1269
+ res.status === 404 ? "File not found" : `Download failed (${res.status})`
1270
+ );
1271
+ }
1272
+ const contentLength = Number(res.headers.get("content-length") || 0);
1273
+ const name = downloadName ?? parseContentDispositionFilename(
1274
+ res.headers.get("content-disposition"),
1275
+ fallback
1276
+ );
1277
+ setState((s) => ({
1278
+ ...s,
1279
+ fileName: name,
1280
+ fileSize: contentLength || null
1281
+ }));
1282
+ const reader = res.body?.getReader();
1283
+ if (!reader) throw new Error("ReadableStream not supported");
1284
+ const chunks = [];
1285
+ let loaded = 0;
1286
+ while (true) {
1287
+ const { done, value } = await reader.read();
1288
+ if (done) break;
1289
+ chunks.push(value);
1290
+ loaded += value.byteLength;
1291
+ const percent = contentLength > 0 ? Math.round(loaded / contentLength * 100) : 0;
1292
+ const rawSpeed = speedTrackerRef.current.update(loaded);
1293
+ const now = Date.now();
1294
+ if (rawSpeed > 0 && now - lastSpeedUpdateRef.current >= 500) {
1295
+ lastSpeedRef.current = rawSpeed;
1296
+ lastSpeedUpdateRef.current = now;
1297
+ }
1298
+ const speed = lastSpeedRef.current;
1299
+ const progress = {
1300
+ loaded,
1301
+ total: contentLength,
1302
+ percent,
1303
+ ...speed && { speed }
1304
+ };
1305
+ setState((s) => ({ ...s, progress }));
1306
+ opts.onProgress?.(key, progress);
1307
+ }
1308
+ const blob = new Blob(chunks);
1309
+ const blobUrl = URL.createObjectURL(blob);
1310
+ const anchor = document.createElement("a");
1311
+ anchor.href = blobUrl;
1312
+ anchor.download = name ?? fallback;
1313
+ anchor.click();
1314
+ URL.revokeObjectURL(blobUrl);
1315
+ setState((s) => ({
1316
+ ...s,
1317
+ phase: "success",
1318
+ fileSize: blob.size,
1319
+ progress: { loaded: blob.size, total: blob.size, percent: 100 }
1320
+ }));
1321
+ await opts.onSuccess?.(key, name ?? fallback);
1322
+ } catch (err) {
1323
+ if (err.name === "AbortError") {
1324
+ if (!resettingRef.current) opts.onCancel?.(key);
1325
+ resettingRef.current = false;
1326
+ setState(INITIAL_STATE4);
1327
+ return;
1328
+ }
1329
+ const message = err instanceof Error ? err.message : "Download failed";
1330
+ setState((s) => ({ ...s, phase: "error", error: message }));
1331
+ opts.onError?.(key, err, "downloading");
1332
+ } finally {
1333
+ abortRef.current = null;
1334
+ }
1335
+ }, []);
1336
+ const cancel = useCallback(() => {
1337
+ abortRef.current?.abort();
1338
+ setState(INITIAL_STATE4);
1339
+ }, []);
1340
+ const reset = useCallback(() => {
1341
+ resettingRef.current = true;
1342
+ abortRef.current?.abort();
1343
+ setState(INITIAL_STATE4);
1344
+ }, []);
1345
+ return { ...state, download, cancel, reset };
1346
+ }
1347
+ var INITIAL_STATE5 = {
1348
+ phase: "idle",
1349
+ error: null,
1350
+ pendingKey: null
1351
+ };
1352
+ function useDelete(options) {
1353
+ const [state, setState] = useState(INITIAL_STATE5);
1354
+ const contextApi = useContext(S3Context);
1355
+ const optsRef = useLiveRef(options);
1356
+ const apiRef = useLiveRef(contextApi);
1357
+ const pendingKeyRef = useRef(null);
1358
+ const requestDelete = useCallback((key) => {
1359
+ pendingKeyRef.current = key;
1360
+ setState({ phase: "confirming", error: null, pendingKey: key });
1361
+ }, []);
1362
+ const confirmDelete = useCallback(async () => {
1363
+ const key = pendingKeyRef.current;
1364
+ if (!key) return;
1365
+ const opts = optsRef.current;
1366
+ const api = opts.api ?? apiRef.current;
1367
+ if (!api)
1368
+ throw new Error(
1369
+ "[better-s3] No S3Api client found. Pass `api` to useDelete or wrap with <S3Provider>."
1370
+ );
1371
+ if (opts.beforeDelete) {
1372
+ const allowed = await opts.beforeDelete(key);
1373
+ if (!allowed) {
1374
+ setState({
1375
+ phase: "error",
1376
+ error: "Delete blocked by beforeDelete hook",
1377
+ pendingKey: null
1378
+ });
1379
+ opts.onError?.(key, new Error("blocked"), "confirming");
1380
+ pendingKeyRef.current = null;
1381
+ return;
1382
+ }
1383
+ }
1384
+ setState((s) => ({ ...s, phase: "deleting", error: null }));
1385
+ opts.onDeleteStart?.(key);
1386
+ try {
1387
+ await api.delete(key, { bucket: opts.bucket });
1388
+ pendingKeyRef.current = null;
1389
+ setState({ phase: "success", error: null, pendingKey: null });
1390
+ await opts.onSuccess?.(key);
1391
+ } catch (err) {
1392
+ const message = err instanceof Error ? err.message : "Delete failed";
1393
+ setState((s) => ({ ...s, phase: "error", error: message }));
1394
+ opts.onError?.(key, err, "deleting");
1395
+ }
1396
+ }, []);
1397
+ const cancelDelete = useCallback(() => {
1398
+ pendingKeyRef.current = null;
1399
+ setState(INITIAL_STATE5);
1400
+ }, []);
1401
+ const reset = useCallback(() => {
1402
+ pendingKeyRef.current = null;
1403
+ setState(INITIAL_STATE5);
1404
+ }, []);
1405
+ return {
1406
+ phase: state.phase,
1407
+ error: state.error,
1408
+ pendingKey: state.pendingKey,
1409
+ requestDelete,
1410
+ confirmDelete,
1411
+ cancelDelete,
1412
+ reset
1413
+ };
1414
+ }
1415
+
1416
+ export { S3Context, S3Provider, S3UploadError, buildObjectKey, createLocalStorageStore, createMemoryStore, createS3Api as createPresignApi, createS3Api, createS3Client, createSpeedTracker, formatEta, formatFileSize, formatSpeed, formatUploadProgress, getFileExtension, parseContentDispositionFilename, truncateFilename, uploadFile, uploadFiles, useDelete, useDownload, useFetchDownload, useMultiUpload, useMultiUploadControls, useS3Client, useUpload, useUploadControls, validateFile };
1417
+ //# sourceMappingURL=index.js.map
2
1418
  //# sourceMappingURL=index.js.map