@lunora/storage 1.0.0-alpha.114 → 1.0.0-alpha.116
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/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-COgL1Ipz.mjs +1 -0
- package/dist/packem_shared/R2_PART_SIZE-Dw8vEttI.mjs +1 -0
- package/dist/packem_shared/createR2BindingUploadStorage-DuD9VXLN.mjs +1 -0
- package/dist/packem_shared/createR2UploadStorage-DGB1xJiD.mjs +1 -0
- package/dist/packem_shared/r2-upload-part-writer-SdQlWaAu.mjs +1 -0
- package/dist/upload.d.mts +52 -40
- package/dist/upload.d.ts +52 -40
- package/dist/upload.mjs +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-C0bIs07J.mjs +0 -1
- package/dist/packem_shared/R2_PART_SIZE-iEB-PTmG.mjs +0 -1
- package/dist/packem_shared/createR2BindingUploadStorage-ByDWP_UR.mjs +0 -1
- package/dist/packem_shared/r2-upload-part-writer-BE4HJRLY.mjs +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as w}from"@lunora/errors";import{ERRORS as E,parseTusMetadata as N,File as U}from"@visulima/storage";import{Rest as b,Multipart as A,Tus as v}from"@visulima/storage/handler/http/fetch";const S="1.0.0",T=(t,e)=>Response.json({error:e},{headers:{"content-type":"application/json","Tus-Resumable":S},status:t}),I=t=>{try{return{metadata:Object.fromEntries(Object.entries(N(t)))}}catch(e){return{error:e instanceof Error?e.message:"Invalid Upload-Metadata header"}}},F=t=>{const e=new Headers(t.headers),r=e.get("Tus-Extension");return e.delete("Tus-Checksum-Algorithm"),r!==null&&e.set("Tus-Extension",r.split(",").filter(o=>o.trim()!=="checksum").join(",")),new Response(t.body,{headers:e,status:t.status,statusText:t.statusText})},P={declared:{contentType:void 0,metadata:{}}},_=t=>({check:e=>{if(e.method!=="POST"&&e.method!=="PATCH")return P;const r=I(e.headers.get("Upload-Metadata")??"");return"error"in r?{refusal:T(400,{code:"BadRequestError",message:r.error,name:"BadRequestError"})}:!t&&e.headers.has("Upload-Checksum")?{refusal:T(400,{code:E.UNSUPPORTED_CHECKSUM_ALGORITHM,message:"Upload-Checksum is not supported on this upload route: its storage verifies no checksums",name:"BadRequestError"})}:{declared:{contentType:void 0,metadata:r.metadata}}},finish:(e,r)=>e.method==="OPTIONS"&&!t?F(r):r}),L=100*1024*1024,M=16*1024*1024,u=(t,e,r)=>{const o={"content-type":"application/json"};return t==="tus"&&(o["Tus-Resumable"]=S),Response.json({error:r},{headers:o,status:e})},c=t=>u(t,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),g=t=>u(t,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),y={"chunked-rest":new Set(["HEAD","OPTIONS","PATCH","POST","PUT"]),multipart:new Set(["OPTIONS","POST"]),tus:new Set(["DELETE","HEAD","OPTIONS","PATCH","POST"])},p=(t,e)=>{const r=u(t,405,{code:"METHOD_NOT_ALLOWED",message:e,name:"MethodNotAllowedError"});return r.headers.set("Allow",[...y[t]].join(", ")),r},k=["X-HTTP-Method-Override","X-HTTP-Method","X-Method-Override"],O=/^\d+$/u,C=/\.[^.]+$/u,D=/^[\w-]{1,255}$/u,H=(t,e)=>{const r=(new URL(t.url).pathname.split("/").findLast(Boolean)??"").replace(C,""),o=t.headers.get("Content-Length")?.trim()??"",n=O.test(o)?Number(o):Number.NaN;return D.test(r)&&Number.isSafeInteger(n)&&n<=e.maxUploadSize?r:void 0},z=t=>typeof t=="object"&&t!==null&&t.UploadErrorCode===E.FILE_NOT_FOUND,x=async(t,e)=>{try{return await e.getMeta(t),!0}catch(r){return!z(r)}},R=(t,e)=>{if(e==="multipart")return;let r;for(const o of["Upload-Length","X-Total-Size","Content-Length"]){const n=t.headers.get(o);if(n===null)continue;const a=O.test(n.trim())?Number(n):Number.NaN,s=Number.isSafeInteger(a)?a:Number.POSITIVE_INFINITY;(r===void 0||s>r)&&(r=s)}return r},j=(t,e,r,o)=>{const{method:n}=t;if(!y[e].has(n))return{refusal:p(e,`${n} is not allowed on this upload route: it is write-only. Serve stored files with ctx.storage.download() or a signed URL`)};const a=k.find(d=>t.headers.has(d));if(a!==void 0)return{refusal:p(e,`${a} is not allowed on this upload route: send the request with the method itself`)};const s=R(t,e);return s!==void 0&&s>r?{refusal:g(e)}:o.check(t)},B=t=>{try{const e=JSON.parse(t??"{}");return e!==null&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}},X={check:t=>{const e=t.headers.get("Content-Type");return{declared:{contentType:e===null||e===""?"application/octet-stream":e,metadata:B(t.headers.get("X-File-Metadata"))}}},finish:(t,e)=>e},$={check:()=>({declared:{contentType:void 0,metadata:{}}}),finish:(t,e)=>e},G=t=>{const e=new U({contentType:t.contentType,metadata:t.metadata}),r={};for(const[o,n]of Object.entries(t.metadata))r[o]=typeof n=="string"?n:JSON.stringify(n);return{contentType:e.contentType,metadata:r}},Y=(t,e)=>e!=="multipart"&&(t.method==="POST"||t.method==="PUT"),V=async(t,e,r,o)=>{const n=R(e.request,e.protocol);let a;try{a=await t({...e,...G(r),declaredSize:n})}catch{return c(e.protocol)}if(a!==void 0)return typeof a!="number"||!Number.isFinite(a)||a<0?c(e.protocol):n===void 0||n>Math.min(a,o)?g(e.protocol):void 0},J=(t,e)=>t==="chunked-rest"?new b(e):t==="multipart"?new A(e):new v(e),K=(t,e)=>t==="tus"?_(e.checksumTypes.length>0):t==="chunked-rest"?X:$,ee=t=>{const e=t.protocol??"tus",r=t.maxFileSize??L;if(!Number.isFinite(r)||r<0)throw new w("VALIDATION_ERROR",`@lunora/storage: maxFileSize must be a finite, non-negative number (received ${String(r)})`);const o={allowMethodOverride:!1,disableTerminationForFinishedUploads:!0,maxChecksumBufferSize:M,maxFileSize:r,storage:t.storage},n=J(e,o),a=K(e,t.storage),{authorize:s,maxFileSizeFor:d}=t;return s===void 0&&!t.silent&&!t.public&&console.warn("@lunora/storage: createUploadHandler() has no `authorize` — this mounts an unauthenticated, unbounded-write endpoint. Pass an RLS `authorize` gate, or set `public: true` (or `silent: true`) to confirm this bucket is intentionally open."),{fetch:async i=>{const l=j(i,e,r,a);if("refusal"in l)return l.refusal;const m={method:i.method,protocol:e,request:i,url:new URL(i.url)};if(s!==void 0)try{if(await s(m)!==!0)return c(e)}catch{return c(e)}if(d!==void 0&&Y(i,e)){const h=await V(d,m,l.declared,r);if(h!==void 0)return h}const f=e==="chunked-rest"&&i.method==="PUT"?H(i,t.storage):void 0;return f!==void 0&&await x(f,t.storage)?u(e,409,{code:"FileConflict",message:"A file already exists under this name",name:"ConflictError"}):a.finish(i,await n.fetch(i))},protocol:e}};export{L as DEFAULT_MAX_UPLOAD_BYTES,ee as createUploadHandler};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{N as t,a as o,R as r,o as e,p}from"./r2-upload-part-writer-SdQlWaAu.mjs";export{t as NAME_TAKEN,o as R2UploadPartWriter,r as R2_PART_SIZE,e as objectExists,p as putObject};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Readable as L}from"node:stream";import{MetaStorage as N,throwErrorCode as c,ERRORS as n,AbstractBaseStorage as g,File as y,UploadError as _}from"@visulima/storage";import{o as A,p as I,r as E,a as x,N as b}from"./r2-upload-part-writer-SdQlWaAu.mjs";import{R as G}from"./r2-upload-part-writer-SdQlWaAu.mjs";const k=5,O=["bytesWritten","ETag","id","name","size","status"],D=(d,t)=>JSON.stringify({...d,modifiedAt:void 0})===JSON.stringify({...t,modifiedAt:void 0});class U extends N{bucket;constructor(t,e){super({prefix:e,suffix:".json"}),this.bucket=t}segmentPrefix(t){return`${this.prefix}${t}/`}async read(t){const e=await this.bucket.get(this.getMetaName(t));if(e===null)return;const a=await e.text();return{etag:e.etag,state:JSON.parse(a)}}async create(t,e){return await this.bucket.put(this.getMetaName(t),JSON.stringify(e),{onlyIf:{etagDoesNotMatch:"*"}})!==null}async write(t,e,a){return(await this.bucket.put(this.getMetaName(t),JSON.stringify(e),{onlyIf:{etagMatches:a}}))?.etag}async swap(t,e){for(let a=0;a<k;a+=1){const r=await this.read(t);if(r===void 0)return c(n.FILE_NOT_FOUND);const s=e(r.state);if(s===r.state||await this.write(t,s,r.etag)!==void 0)return s}return c(n.FILE_CONFLICT,"The upload kept changing while it was being written; resume from the current offset")}async get(t){const e=await this.read(t);return e===void 0?c(n.FILE_NOT_FOUND):e.state.file}async save(t,e){return(await this.swap(t,r=>{const i={...e};for(const o of O)i[o]=r.file[o];return D(i,r.file)?r:{...r,file:i}})).file}async delete(t){await this.bucket.delete(this.getMetaName(t))}}const F="_lunora/uploads/",f=6e4,R=new Set(["checksum","concatenation","creation-defer-length"]),M=new Set([...O,"expiredAt"]),p=d=>c(n.FILE_CONFLICT,d),P=d=>d!==void 0&&d.expiresAt>Date.now(),T=32*1024*1024;class j extends g{sequentialWrites=!0;meta;bucket;constructor(t,e={}){super(e),this.bucket=t,this.meta=new U(t,e.statePrefix??F)}get tusExtension(){return super.tusExtension.filter(t=>!R.has(t))}async create(t){return this.instrumentOperation("create",async()=>{const e=t.size===void 0||t.size===""?Number.NaN:Number(t.size);if(!Number.isSafeInteger(e)||e<0)return c(n.INVALID_FILE_SIZE,"An upload must declare its size as a non-negative integer");const a=new y(t);if(a.size=e,a.name=this.namingFunction(a),g.assertSafeId(a.id),a.name.includes(this.meta.prefix))return c(n.INVALID_FILE_NAME,"An upload cannot be stored under the upload state prefix");await this.validate(a);const r=t.id!==void 0;if(r&&(await this.meta.read(a.id)!==void 0||await A(this.bucket,a.name)))return p(b);if(a.bytesWritten=0,a.status=e===0?"completed":"created",this.updateTimestamps(a),!await this.meta.create(a.id,{file:a,upload:{parts:[],segments:[],...r?{createOnly:r}:{}}}))return p(b);if(e===0)try{await I(this.bucket,a.name,new Uint8Array(0),a.contentType,r)}catch(s){throw await this.deleteMeta(a.id).catch(()=>{}),s}return await this.onCreate(a),a})}async write(t){return this.instrumentOperation("write",async()=>{const e=await this.meta.read(t.id);if(e===void 0)return c(n.FILE_NOT_FOUND);const a=await this.checkIfExpired(e.state.file);if(a.status==="completed"||!("body"in t)||!("start"in t))return a;this.checkPart(a,t);const r=await this.acquireLease(a.id,e.state,e.etag);try{return await this.writeLeased(a,e.state.upload,t.body,r)}catch(s){throw await this.releaseLease(a.id,r).catch(()=>{}),s}})}async get({id:t}){return this.instrumentOperation("get",async()=>{const{file:e,object:a}=await this.completedObject(t);if(a.size>T)return await a.body?.cancel(),c(n.REQUEST_ENTITY_TOO_LARGE,`get() buffers the whole file and refuses one over ${String(T)} bytes; use getStream(), ctx.storage.download() or a signed URL`);const r=Buffer.from(await a.arrayBuffer());return{content:r,contentType:e.contentType,ETag:a.etag,expiredAt:e.expiredAt,id:t,metadata:e.metadata,modifiedAt:e.modifiedAt,name:e.name,originalName:e.originalName,size:r.byteLength}})}async getStream({id:t}){return this.instrumentOperation("getStream",async()=>{const{file:e,object:a}=await this.completedObject(t),r=L.from(E(a.body));return{headers:{"Content-Length":String(a.size),"Content-Type":e.contentType,ETag:a.etag},size:a.size,stream:r}})}async delete({id:t}){return this.instrumentOperation("delete",async()=>{const e=await this.meta.read(t);if(e===void 0)return c(n.FILE_NOT_FOUND);const{file:a,upload:r}=e.state;a.status!=="completed"&&(await this.acquireLease(t,e.state,e.etag),r.uploadId!==void 0&&await this.bucket.resumeMultipartUpload(a.name,r.uploadId).abort().catch(()=>{}),await this.deleteSegments(t)),await this.deleteMeta(t);const s={...a,status:"deleted"};return await this.onDelete(s),s})}async update(t,e){const a=Object.fromEntries(Object.entries(e).filter(([r])=>!M.has(r)));return super.update(t,a)}async list(){return this.instrumentOperation("list",async()=>{const{prefix:t,suffix:e}=this.meta,a=[];let r;do{const s=await this.bucket.list({delimiter:"/",prefix:t,...r===void 0?{}:{cursor:r}});for(const i of s.objects)if(i.key.endsWith(e)){const o=new y({id:this.meta.getIdFromMetaName(i.key),metadata:{}});o.createdAt=(i.uploaded??new Date).toISOString(),o.modifiedAt=o.createdAt,a.push(o)}r=s.truncated===!0?s.cursor:void 0}while(r!==void 0);return a})}copy(){return this.unsupported("copy")}move(){return this.unsupported("move")}async finish(t,e,a){try{return await t.finish()}catch(r){throw a&&r.UploadErrorCode===n.FILE_CONFLICT&&(await this.deleteSegments(e).catch(()=>{}),await this.deleteMeta(e).catch(()=>{})),r}}async completedObject(t){const e=await this.checkIfExpired(await this.getMeta(t)),a=e.status==="completed"?await this.bucket.get(e.name):void 0;return a==null?c(n.FILE_NOT_FOUND):{file:e,object:a}}unsupported(t){return Promise.reject(new _(n.METHOD_NOT_ALLOWED,`${this.constructor.name} does not implement ${t}()`))}checkPart(t,e){e.start!==t.bytesWritten&&p(`Upload-Offset ${String(e.start)} does not match the upload's offset ${String(t.bytesWritten)}`),e.checksumAlgorithm!==void 0&&e.checksumAlgorithm!==""&&c(n.UNSUPPORTED_CHECKSUM_ALGORITHM),e.contentLength!==void 0&&e.start+e.contentLength>(t.size??0)&&c(n.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length")}async acquireLease(t,e,a){if(P(e.upload.lock))return p("Another request is writing to this upload");const r=crypto.randomUUID(),s={...e.upload,lock:{expiresAt:Date.now()+f,token:r}};return await this.meta.write(t,{...e,upload:s},a)===void 0?p("Another request is writing to this upload"):r}async changeLeased(t,e,a){return this.meta.swap(t,r=>r.upload.lock?.token!==e?p("This request's lease on the upload expired; resume from the current offset"):a(r))}async releaseLease(t,e){await this.changeLeased(t,e,a=>{const r={...a.upload};return delete r.lock,{...a,upload:r}})}async renewLease(t,e){await this.changeLeased(t,e,a=>({...a,upload:{...a.upload,lock:{expiresAt:Date.now()+f,token:e}}}))}async deleteSegments(t){const e=[];let a;do{const r=await this.bucket.list({prefix:this.meta.segmentPrefix(t),...a===void 0?{}:{cursor:a}});e.push(...r.objects.map(s=>s.key)),a=r.truncated===!0?r.cursor:void 0}while(a!==void 0);for(let r=0;r<e.length;r+=1e3)await this.bucket.delete(e.slice(r,r+1e3))}async pump(t,e,a,r,s){const i=E(e);let o=0,u=Date.now();try{for(;;){let h;try{h=await i.next()}catch(l){return l instanceof Error?l:new Error(String(l))}if(h.done===!0)return;if(o+=h.value.byteLength,o>a)return c(n.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length");Date.now()-u>=f/3&&(u=Date.now(),await this.renewLease(r,s)),await t.push(h.value)}}finally{await i.return(void 0).catch(()=>{})}}async writeLeased(t,e,a,r){const s=t.size??0,i=new x(this.bucket,t,e,this.meta.segmentPrefix(t.id),{confirm:async()=>this.renewLease(t.id,r),save:async(m,v)=>{await this.changeLeased(t.id,r,w=>({file:{...w.file,bytesWritten:v,status:"part"},upload:{...m,lock:w.upload.lock}}))},token:r}),o=await this.pump(i,a,s-t.bytesWritten,t.id,r),u=i.offset===s,h=u?await this.finish(i,t.id,e.createOnly===!0):void 0;if(!u)try{await i.keepTail()}catch(m){if(o===void 0)throw m}const l={bytesWritten:u?s:i.stored,modifiedAt:new Date().toISOString(),status:u?"completed":"part",...h===void 0?{}:{ETag:h}},S=u?{parts:[],segments:[]}:i.progress();if(await this.changeLeased(t.id,r,m=>({file:{...m.file,...l},upload:S})),await(u?this.deleteSegments(t.id).catch(()=>{}):i.dropConsumed()),o!==void 0)throw o;return{...t,...l}}}const B=(d,t={})=>new j(d,t);export{G as R2_PART_SIZE,B as createR2BindingUploadStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";import{AwsLightStorage as s}from"@visulima/storage/provider/aws-light";const n=new Set(["127.0.0.1","[::1]","localhost"]),h=t=>{let e;try{e=new URL(t)}catch{e=void 0}if(e?.protocol!=="https:"&&!(e?.protocol==="http:"&&n.has(e.hostname)))throw new c("VALIDATION_ERROR",`@lunora/storage: createR2UploadStorage's endpoint must be an absolute https:// URL such as https://<account>.r2.cloudflarestorage.com, or http:// to localhost (received ${JSON.stringify(t)})`);return e},u=t=>{const e=t.endpoint===void 0?new URL(`https://${t.accountId}.r2.cloudflarestorage.com`):h(t.endpoint),o=encodeURIComponent(t.bucket),r=e.pathname.split("/").filter(a=>a!=="");return!(t.endpoint!==void 0&&e.hostname.startsWith(`${t.bucket.toLowerCase()}.`))&&r.at(-1)!==o&&r.push(o),e.pathname=r.length===0?"/":`/${r.join("/")}/`,e.href},i=t=>new s({accessKeyId:t.accessKeyId,bucket:t.bucket,endpoint:u(t),path:t.path??"/",region:"auto",secretAccessKey:t.secretAccessKey,...t.partSize===void 0?{}:{partSize:t.partSize}});export{i as createR2UploadStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{throwErrorCode as c,ERRORS as u}from"@visulima/storage";const d=i=>{if(i instanceof Uint8Array)return i;if(typeof i=="string")return new TextEncoder().encode(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(i instanceof ArrayBuffer)return new Uint8Array(i);throw new TypeError("Unsupported upload body chunk")},l=i=>typeof i.getReader=="function",b=async function*(t){if(t!=null){if(t instanceof Uint8Array){yield t;return}if(typeof t=="object"&&l(t)){const e=t.getReader();try{for(;;){const{done:s,value:n}=await e.read();if(s)return;yield d(n)}}finally{e.releaseLock()}}if(typeof t=="object"&&Symbol.asyncIterator in t){for await(const e of t)yield d(e);return}throw new TypeError("Unsupported upload body")}};class f{size=0;chunks=[];push(t){t.byteLength>0&&(this.chunks.push(t),this.size+=t.byteLength)}copyFront(t,e,s){let n=0;for(const a of this.chunks){if(n>=s)break;const r=Math.min(a.byteLength,s-n);t.set(a.subarray(0,r),e+n),n+=r}}drop(t){let e=t;for(;e>0;){const s=this.chunks[0];if(s===void 0)break;s.byteLength<=e?(this.chunks.shift(),e-=s.byteLength):(this.chunks[0]=s.subarray(e),e=0)}this.size-=t-e}}const o=5*1024*1024,g=8,h=i=>i.reduce((t,e)=>t+e.size,0),p="A file already exists under this name",m=async(i,t,e,s,n)=>n?await i.put(t,e,{httpMetadata:{contentType:s},onlyIf:{etagDoesNotMatch:"*"}})??c(u.FILE_CONFLICT,p):i.put(t,e,{httpMetadata:{contentType:s}}),y=async(i,t)=>{if(i.head!==void 0)return await i.head(t)!==null;const e=await i.get(t);return await e?.body?.cancel(),e!==null};class I{parts;segments;uploadId;bucket;createOnly;consumed=[];file;lease;pending=new f;segmentPrefix;constructor(t,e,s,n,a){this.bucket=t,this.file=e,this.parts=[...s.parts],this.segments=[...s.segments],this.uploadId=s.uploadId,this.createOnly=s.createOnly===!0,this.segmentPrefix=n,this.lease=a}get stored(){return this.parts.length*o+h(this.segments)}get offset(){return this.stored+this.pending.size}progress(){return{parts:[...this.parts],segments:[...this.segments],...this.uploadId===void 0?{}:{uploadId:this.uploadId},...this.createOnly?{createOnly:!0}:{}}}async push(t){for(this.pending.push(t);h(this.segments)+this.pending.size>=o;)await this.flushPart()}async keepTail(){if(this.pending.size===0)return;const t=this.segments.length+1>g,e=t?this.parts.length*o:this.stored,s=t?h(this.segments)+this.pending.size:this.pending.size,n=t?await this.assemble(s):this.front(s),a=`${this.segmentPrefix}${String(e).padStart(16,"0")}-${this.lease.token}`;await this.bucket.put(a,n),t&&this.consumeSegments(),this.segments=[...this.segments,{key:a,size:s}],this.pending.drop(this.pending.size)}async finish(){const t=await this.assemble(h(this.segments)+this.pending.size);await this.lease.confirm();let e;if(this.uploadId===void 0)e=(await m(this.bucket,this.file.name,t,this.file.contentType,this.createOnly)).etag;else{if(this.createOnly&&await y(this.bucket,this.file.name))return await this.multipart(this.uploadId).abort().catch(()=>{}),c(u.FILE_CONFLICT,p);const s=this.multipart(this.uploadId);if(t.byteLength>0){const a=this.parts.length+1,r=await s.uploadPart(a,t);this.parts.push({etag:r.etag,partNumber:a})}e=(await s.complete(this.parts)).etag}return this.consumeSegments(),this.pending.drop(this.pending.size),e}async dropConsumed(){const t=this.consumed;this.consumed=[],t.length>0&&await this.bucket.delete(t).catch(()=>{})}async assemble(t){const e=new Uint8Array(t);let s=0;for(const n of this.segments){const a=await this.bucket.get(n.key);if(a===null)return c(u.STORAGE_ERROR,`Buffered upload segment ${n.key} is missing`);const r=new Uint8Array(await a.arrayBuffer());e.set(r,s),s+=r.byteLength}return this.pending.copyFront(e,s,t-s),e}front(t){const e=new Uint8Array(t);return this.pending.copyFront(e,0,t),e}consumeSegments(){this.consumed.push(...this.segments.map(t=>t.key)),this.segments=[]}multipart(t){return this.bucket.resumeMultipartUpload(this.file.name,t)}async flushPart(){const t=h(this.segments),e=await this.assemble(o);if(await this.lease.confirm(),this.uploadId===void 0){const a=await this.bucket.createMultipartUpload(this.file.name,{httpMetadata:{contentType:this.file.contentType}});this.uploadId=a.uploadId}const s=this.parts.length+1,n=await this.multipart(this.uploadId).uploadPart(s,e);this.parts.push({etag:n.etag,partNumber:s}),this.pending.drop(o-t),this.consumeSegments(),await this.lease.save(this.progress(),this.stored),await this.dropConsumed()}}export{p as N,o as R,I as a,y as o,m as p,b as r};
|
package/dist/upload.d.mts
CHANGED
|
@@ -18,7 +18,7 @@ type UploadProtocol = "chunked-rest" | "multipart" | "tus";
|
|
|
18
18
|
*/
|
|
19
19
|
declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
20
20
|
type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
|
|
21
|
-
/** A `@visulima/storage` storage provider (e.g.
|
|
21
|
+
/** A `@visulima/storage` storage provider (e.g. `createR2UploadStorage` or a memory provider in tests). */
|
|
22
22
|
type UploadStorage = UploadHandlerOptions["storage"];
|
|
23
23
|
/**
|
|
24
24
|
* The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
|
|
@@ -124,47 +124,12 @@ interface UploadHandler {
|
|
|
124
124
|
/** The protocol this handler speaks. */
|
|
125
125
|
protocol: UploadProtocol;
|
|
126
126
|
}
|
|
127
|
-
/** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
|
|
128
|
-
interface R2UploadStorageOptions {
|
|
129
|
-
/** R2 S3 API Access Key ID (from an R2 API token). */
|
|
130
|
-
accessKeyId: string;
|
|
131
|
-
/** Cloudflare account id — used to derive the R2 S3 endpoint host. */
|
|
132
|
-
accountId: string;
|
|
133
|
-
/** Target R2 bucket name. */
|
|
134
|
-
bucket: string;
|
|
135
|
-
/**
|
|
136
|
-
* Explicit R2 S3 endpoint. Defaults to
|
|
137
|
-
* `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
|
|
138
|
-
* jurisdiction (e.g. `<accountId>.eu.r2.cloudflarestorage.com`).
|
|
139
|
-
*/
|
|
140
|
-
endpoint?: string;
|
|
141
|
-
/** Client-side multipart part size (bytes or a size string like `"16MB"`). */
|
|
142
|
-
partSize?: number | string;
|
|
143
|
-
/**
|
|
144
|
-
* Path prefix the handler is mounted on (must match the client endpoint's
|
|
145
|
-
* path). Default `"/"`.
|
|
146
|
-
*/
|
|
147
|
-
path?: string;
|
|
148
|
-
}
|
|
149
127
|
/**
|
|
150
128
|
* Build an RLS-gated resumable upload handler over a `@visulima/storage`
|
|
151
129
|
* provider. Mount its {@link UploadHandler.fetch} on the route your client
|
|
152
130
|
* uploads to and drive it with `@visulima/storage-client`.
|
|
153
131
|
*/
|
|
154
132
|
declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
|
|
155
|
-
/**
|
|
156
|
-
* Build an R2-backed storage provider for {@link createUploadHandler} using
|
|
157
|
-
* `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
|
|
158
|
-
* AWS SDK). R2's S3 region alias is always `auto`.
|
|
159
|
-
*
|
|
160
|
-
* Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
|
|
161
|
-
* same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
|
|
162
|
-
* Worker the `aws-light` provider needs `nodejs_compat` (it imports
|
|
163
|
-
* `node:stream`).
|
|
164
|
-
*/
|
|
165
|
-
declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
|
|
166
|
-
secretAccessKey: string;
|
|
167
|
-
}) => AwsLightStorage;
|
|
168
133
|
/** Every part of an R2 multipart upload but the last is exactly this size: R2's 5 MiB minimum. */
|
|
169
134
|
declare const R2_PART_SIZE: number;
|
|
170
135
|
/** Options for {@link createR2BindingUploadStorage}. */
|
|
@@ -193,12 +158,59 @@ interface R2BindingUploadStorageOptions extends Omit<BaseStorageOptions, "metaSt
|
|
|
193
158
|
* Chunked-REST chunks have to arrive one at a time and in order. A chunk at any
|
|
194
159
|
* offset other than the upload's current one, or one sent while another is
|
|
195
160
|
* still streaming, is a `409` and stores nothing, and it never counts towards
|
|
196
|
-
* completion
|
|
197
|
-
*
|
|
198
|
-
* so over this provider it can upload only as one chunk; use TUS for
|
|
199
|
-
* resumable uploads.
|
|
161
|
+
* completion. The bundled `@visulima/storage-client` chunked-REST client
|
|
162
|
+
* sends one chunk at a time (since 1.0.8), so its uploads complete here.
|
|
200
163
|
*/
|
|
201
164
|
declare const createR2BindingUploadStorage: (bucket: R2UploadBucket, options?: R2BindingUploadStorageOptions) => UploadStorage;
|
|
165
|
+
/** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
|
|
166
|
+
interface R2UploadStorageOptions {
|
|
167
|
+
/** R2 S3 API Access Key ID (from an R2 API token). */
|
|
168
|
+
accessKeyId: string;
|
|
169
|
+
/** Cloudflare account id — used to derive the R2 S3 endpoint host. */
|
|
170
|
+
accountId: string;
|
|
171
|
+
/** Target R2 bucket name. */
|
|
172
|
+
bucket: string;
|
|
173
|
+
/**
|
|
174
|
+
* Explicit R2 S3 account endpoint, an absolute `https://` URL. The bucket
|
|
175
|
+
* is appended as a path segment, unless the endpoint already names it
|
|
176
|
+
* (as its last path segment, or as the host's first label). Defaults to
|
|
177
|
+
* `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
|
|
178
|
+
* jurisdiction (e.g. `https://<accountId>.eu.r2.cloudflarestorage.com`).
|
|
179
|
+
*/
|
|
180
|
+
endpoint?: string;
|
|
181
|
+
/** Client-side multipart part size (bytes or a size string like `"16MB"`). */
|
|
182
|
+
partSize?: number | string;
|
|
183
|
+
/**
|
|
184
|
+
* Path prefix the handler is mounted on (must match the client endpoint's
|
|
185
|
+
* path). Default `"/"`.
|
|
186
|
+
*/
|
|
187
|
+
path?: string;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Build an R2-backed storage provider for `createUploadHandler` using
|
|
191
|
+
* `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
|
|
192
|
+
* AWS SDK). R2's S3 region alias is always `auto`.
|
|
193
|
+
*
|
|
194
|
+
* Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
|
|
195
|
+
* same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
|
|
196
|
+
* Worker the `aws-light` provider needs `nodejs_compat` (it imports
|
|
197
|
+
* `node:stream`).
|
|
198
|
+
*
|
|
199
|
+
* The bucket is addressed path-style, `<endpoint>/<bucket>/<key>`.
|
|
200
|
+
*
|
|
201
|
+
* Each chunk becomes one multipart part, so every chunk but the last has to be
|
|
202
|
+
* the same size and at least 5 MiB, and chunks have to arrive in order (an
|
|
203
|
+
* out-of-order or repeated one is a `409`). The R2 binding provider
|
|
204
|
+
* (`./r2-binding-upload-storage`) has neither limit, needs no S3 credentials
|
|
205
|
+
* and runs under `wrangler dev` too.
|
|
206
|
+
*
|
|
207
|
+
* The provider takes no `@visulima/storage` limits of its own (its upload cap
|
|
208
|
+
* is the 5 TB default), so `maxFileSize` / `maxFileSizeFor` on the handler are
|
|
209
|
+
* the caps that apply.
|
|
210
|
+
*/
|
|
211
|
+
declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
|
|
212
|
+
secretAccessKey: string;
|
|
213
|
+
}) => AwsLightStorage;
|
|
202
214
|
export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
|
|
203
215
|
/**
|
|
204
216
|
* `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
|
package/dist/upload.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ type UploadProtocol = "chunked-rest" | "multipart" | "tus";
|
|
|
18
18
|
*/
|
|
19
19
|
declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
20
20
|
type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
|
|
21
|
-
/** A `@visulima/storage` storage provider (e.g.
|
|
21
|
+
/** A `@visulima/storage` storage provider (e.g. `createR2UploadStorage` or a memory provider in tests). */
|
|
22
22
|
type UploadStorage = UploadHandlerOptions["storage"];
|
|
23
23
|
/**
|
|
24
24
|
* The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
|
|
@@ -124,47 +124,12 @@ interface UploadHandler {
|
|
|
124
124
|
/** The protocol this handler speaks. */
|
|
125
125
|
protocol: UploadProtocol;
|
|
126
126
|
}
|
|
127
|
-
/** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
|
|
128
|
-
interface R2UploadStorageOptions {
|
|
129
|
-
/** R2 S3 API Access Key ID (from an R2 API token). */
|
|
130
|
-
accessKeyId: string;
|
|
131
|
-
/** Cloudflare account id — used to derive the R2 S3 endpoint host. */
|
|
132
|
-
accountId: string;
|
|
133
|
-
/** Target R2 bucket name. */
|
|
134
|
-
bucket: string;
|
|
135
|
-
/**
|
|
136
|
-
* Explicit R2 S3 endpoint. Defaults to
|
|
137
|
-
* `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
|
|
138
|
-
* jurisdiction (e.g. `<accountId>.eu.r2.cloudflarestorage.com`).
|
|
139
|
-
*/
|
|
140
|
-
endpoint?: string;
|
|
141
|
-
/** Client-side multipart part size (bytes or a size string like `"16MB"`). */
|
|
142
|
-
partSize?: number | string;
|
|
143
|
-
/**
|
|
144
|
-
* Path prefix the handler is mounted on (must match the client endpoint's
|
|
145
|
-
* path). Default `"/"`.
|
|
146
|
-
*/
|
|
147
|
-
path?: string;
|
|
148
|
-
}
|
|
149
127
|
/**
|
|
150
128
|
* Build an RLS-gated resumable upload handler over a `@visulima/storage`
|
|
151
129
|
* provider. Mount its {@link UploadHandler.fetch} on the route your client
|
|
152
130
|
* uploads to and drive it with `@visulima/storage-client`.
|
|
153
131
|
*/
|
|
154
132
|
declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
|
|
155
|
-
/**
|
|
156
|
-
* Build an R2-backed storage provider for {@link createUploadHandler} using
|
|
157
|
-
* `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
|
|
158
|
-
* AWS SDK). R2's S3 region alias is always `auto`.
|
|
159
|
-
*
|
|
160
|
-
* Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
|
|
161
|
-
* same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
|
|
162
|
-
* Worker the `aws-light` provider needs `nodejs_compat` (it imports
|
|
163
|
-
* `node:stream`).
|
|
164
|
-
*/
|
|
165
|
-
declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
|
|
166
|
-
secretAccessKey: string;
|
|
167
|
-
}) => AwsLightStorage;
|
|
168
133
|
/** Every part of an R2 multipart upload but the last is exactly this size: R2's 5 MiB minimum. */
|
|
169
134
|
declare const R2_PART_SIZE: number;
|
|
170
135
|
/** Options for {@link createR2BindingUploadStorage}. */
|
|
@@ -193,12 +158,59 @@ interface R2BindingUploadStorageOptions extends Omit<BaseStorageOptions, "metaSt
|
|
|
193
158
|
* Chunked-REST chunks have to arrive one at a time and in order. A chunk at any
|
|
194
159
|
* offset other than the upload's current one, or one sent while another is
|
|
195
160
|
* still streaming, is a `409` and stores nothing, and it never counts towards
|
|
196
|
-
* completion
|
|
197
|
-
*
|
|
198
|
-
* so over this provider it can upload only as one chunk; use TUS for
|
|
199
|
-
* resumable uploads.
|
|
161
|
+
* completion. The bundled `@visulima/storage-client` chunked-REST client
|
|
162
|
+
* sends one chunk at a time (since 1.0.8), so its uploads complete here.
|
|
200
163
|
*/
|
|
201
164
|
declare const createR2BindingUploadStorage: (bucket: R2UploadBucket, options?: R2BindingUploadStorageOptions) => UploadStorage;
|
|
165
|
+
/** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
|
|
166
|
+
interface R2UploadStorageOptions {
|
|
167
|
+
/** R2 S3 API Access Key ID (from an R2 API token). */
|
|
168
|
+
accessKeyId: string;
|
|
169
|
+
/** Cloudflare account id — used to derive the R2 S3 endpoint host. */
|
|
170
|
+
accountId: string;
|
|
171
|
+
/** Target R2 bucket name. */
|
|
172
|
+
bucket: string;
|
|
173
|
+
/**
|
|
174
|
+
* Explicit R2 S3 account endpoint, an absolute `https://` URL. The bucket
|
|
175
|
+
* is appended as a path segment, unless the endpoint already names it
|
|
176
|
+
* (as its last path segment, or as the host's first label). Defaults to
|
|
177
|
+
* `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
|
|
178
|
+
* jurisdiction (e.g. `https://<accountId>.eu.r2.cloudflarestorage.com`).
|
|
179
|
+
*/
|
|
180
|
+
endpoint?: string;
|
|
181
|
+
/** Client-side multipart part size (bytes or a size string like `"16MB"`). */
|
|
182
|
+
partSize?: number | string;
|
|
183
|
+
/**
|
|
184
|
+
* Path prefix the handler is mounted on (must match the client endpoint's
|
|
185
|
+
* path). Default `"/"`.
|
|
186
|
+
*/
|
|
187
|
+
path?: string;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Build an R2-backed storage provider for `createUploadHandler` using
|
|
191
|
+
* `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
|
|
192
|
+
* AWS SDK). R2's S3 region alias is always `auto`.
|
|
193
|
+
*
|
|
194
|
+
* Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
|
|
195
|
+
* same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
|
|
196
|
+
* Worker the `aws-light` provider needs `nodejs_compat` (it imports
|
|
197
|
+
* `node:stream`).
|
|
198
|
+
*
|
|
199
|
+
* The bucket is addressed path-style, `<endpoint>/<bucket>/<key>`.
|
|
200
|
+
*
|
|
201
|
+
* Each chunk becomes one multipart part, so every chunk but the last has to be
|
|
202
|
+
* the same size and at least 5 MiB, and chunks have to arrive in order (an
|
|
203
|
+
* out-of-order or repeated one is a `409`). The R2 binding provider
|
|
204
|
+
* (`./r2-binding-upload-storage`) has neither limit, needs no S3 credentials
|
|
205
|
+
* and runs under `wrangler dev` too.
|
|
206
|
+
*
|
|
207
|
+
* The provider takes no `@visulima/storage` limits of its own (its upload cap
|
|
208
|
+
* is the 5 TB default), so `maxFileSize` / `maxFileSizeFor` on the handler are
|
|
209
|
+
* the caps that apply.
|
|
210
|
+
*/
|
|
211
|
+
declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
|
|
212
|
+
secretAccessKey: string;
|
|
213
|
+
}) => AwsLightStorage;
|
|
202
214
|
export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
|
|
203
215
|
/**
|
|
204
216
|
* `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
|
package/dist/upload.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createR2BindingUploadStorage as o}from"./packem_shared/createR2BindingUploadStorage-
|
|
1
|
+
import{createR2BindingUploadStorage as o}from"./packem_shared/createR2BindingUploadStorage-DuD9VXLN.mjs";import{createR2UploadStorage as t}from"./packem_shared/createR2UploadStorage-DGB1xJiD.mjs";import{DEFAULT_MAX_UPLOAD_BYTES as d,createUploadHandler as R}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-COgL1Ipz.mjs";import{R as _}from"./packem_shared/r2-upload-part-writer-SdQlWaAu.mjs";export{d as DEFAULT_MAX_UPLOAD_BYTES,_ as R2_PART_SIZE,o as createR2BindingUploadStorage,t as createR2UploadStorage,R as createUploadHandler};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/storage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.116",
|
|
4
4
|
"description": "R2-backed storage for Lunora: typed buckets and signed URLs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@lunora/errors": "1.0.0-alpha.50",
|
|
54
54
|
"@lunora/platform": "1.0.0-alpha.55",
|
|
55
|
-
"@visulima/storage": "2.0.
|
|
55
|
+
"@visulima/storage": "2.0.33",
|
|
56
56
|
"aws4fetch": "1.0.20"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as f}from"@lunora/errors";import{File as y}from"@visulima/storage";import{Rest as E,Multipart as R,Tus as O}from"@visulima/storage/handler/http/fetch";import{AwsLightStorage as S}from"@visulima/storage/provider/aws-light";const A=100*1024*1024,b="1.0.0",p=(e,t,r)=>{const o={"content-type":"application/json"};return e==="tus"&&(o["Tus-Resumable"]=b),Response.json({error:r},{headers:o,status:t})},d=e=>p(e,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),T=e=>p(e,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),g={"chunked-rest":new Set(["HEAD","OPTIONS","PATCH","POST","PUT"]),multipart:new Set(["OPTIONS","POST"]),tus:new Set(["DELETE","HEAD","OPTIONS","PATCH","POST"])},v=(e,t)=>{const r=p(e,405,{code:"METHOD_NOT_ALLOWED",message:`${t} is not allowed on this upload route: it is write-only. Serve stored files with ctx.storage.download() or a signed URL`,name:"MethodNotAllowedError"});return r.headers.set("Allow",[...g[e]].join(", ")),r},N=/^\d+$/u,w=(e,t)=>{if(t==="multipart")return;let r;for(const o of["Upload-Length","X-Total-Size","Content-Length"]){const n=e.headers.get(o);if(n===null)continue;const s=N.test(n.trim())?Number(n):Number.NaN,i=Number.isSafeInteger(s)?s:Number.POSITIVE_INFINITY;(r===void 0||i>r)&&(r=i)}return r},I=e=>{const t={};for(const[r,o]of e.split(",").map(n=>n.split(" ")))r!==void 0&&r!==""&&(t[r]=o===void 0||o===""?"":Buffer.from(o,"base64").toString());return t},U=e=>{try{const t=JSON.parse(e??"{}");return t!==null&&typeof t=="object"&&!Array.isArray(t)?t:{}}catch{return{}}},L=(e,t)=>{const r=t==="tus"?I(e.headers.get("Upload-Metadata")??""):U(e.headers.get("X-File-Metadata")),o=e.headers.get("Content-Type"),s=t==="tus"?void 0:o===null||o===""?"application/octet-stream":o,i=new y({contentType:s,metadata:r}),l={};for(const[a,c]of Object.entries(r))l[a]=typeof c=="string"?c:JSON.stringify(c);return{contentType:i.contentType,metadata:l}},z=(e,t)=>t!=="multipart"&&(e.method==="POST"||e.method==="PUT"),F=async(e,t,r)=>{const o=w(t.request,t.protocol);let n;try{n=await e({...t,...L(t.request,t.protocol),declaredSize:o})}catch{return d(t.protocol)}if(n!==void 0)return typeof n!="number"||!Number.isFinite(n)||n<0?d(t.protocol):o===void 0||o>Math.min(n,r)?T(t.protocol):void 0},P=(e,t)=>e==="chunked-rest"?new E(t):e==="multipart"?new R(t):new O(t),H=e=>{const t=e.protocol??"tus",r=e.maxFileSize??A;if(!Number.isFinite(r)||r<0)throw new f("VALIDATION_ERROR",`@lunora/storage: maxFileSize must be a finite, non-negative number (received ${String(r)})`);if(t==="chunked-rest"&&e.storage instanceof S)throw new f("VALIDATION_ERROR",`@lunora/storage: chunked REST is not supported over createR2UploadStorage (R2's S3 API), which stores chunks in arrival order and can corrupt the file. Use protocol "tus", or createR2BindingUploadStorage`);const o={disableTerminationForFinishedUploads:!0,maxFileSize:r,storage:e.storage},n=P(t,o),{authorize:s,maxFileSizeFor:i}=e;return s===void 0&&!e.silent&&!e.public&&console.warn("@lunora/storage: createUploadHandler() has no `authorize` — this mounts an unauthenticated, unbounded-write endpoint. Pass an RLS `authorize` gate, or set `public: true` (or `silent: true`) to confirm this bucket is intentionally open."),{fetch:async a=>{const{method:c}=a;if(!g[t].has(c))return v(t,c);const h=w(a,t);if(h!==void 0&&h>r)return T(t);const m={method:a.method,protocol:t,request:a,url:new URL(a.url)};if(s!==void 0)try{if(await s(m)!==!0)return d(t)}catch{return d(t)}if(i!==void 0&&z(a,t)){const u=await F(i,m,r);if(u!==void 0)return u}return n.fetch(a)},protocol:t}},j=e=>new S({accessKeyId:e.accessKeyId,bucket:e.bucket,endpoint:e.endpoint??`https://${e.accountId}.r2.cloudflarestorage.com`,path:e.path??"/",region:"auto",secretAccessKey:e.secretAccessKey,...e.partSize===void 0?{}:{partSize:e.partSize}});export{A as DEFAULT_MAX_UPLOAD_BYTES,j as createR2UploadStorage,H as createUploadHandler};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as R,R as o}from"./r2-upload-part-writer-BE4HJRLY.mjs";export{R as R2UploadPartWriter,o as R2_PART_SIZE};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Readable as L}from"node:stream";import{MetaStorage as N,throwErrorCode as d,ERRORS as c,AbstractBaseStorage as y,File as E,UploadError as k}from"@visulima/storage";import{r as b,a as A}from"./r2-upload-part-writer-BE4HJRLY.mjs";import{R as H}from"./r2-upload-part-writer-BE4HJRLY.mjs";const x=5,T=["bytesWritten","ETag","id","name","size","status"],w="_chunks",I="_chunkedUpload",O=i=>i.metadata[I]===!0,U=i=>{if(!O(i))return i;const t=i.bytesWritten,e=t>0?[{length:t,offset:0}]:[];return{...i,metadata:Object.fromEntries([...Object.entries(i.metadata),[w,e]])}},D=i=>!O(i)||!(w in i.metadata)?i:{...i,metadata:Object.fromEntries(Object.entries(i.metadata).filter(([t])=>t!==w))},R=(i,t)=>JSON.stringify({...i,modifiedAt:void 0})===JSON.stringify({...t,modifiedAt:void 0});class F extends N{bucket;constructor(t,e){super({prefix:e,suffix:".json"}),this.bucket=t}segmentPrefix(t){return`${this.prefix}${t}/`}async read(t){const e=await this.bucket.get(this.getMetaName(t));if(e===null)return;const a=await e.text();return{etag:e.etag,state:JSON.parse(a)}}async create(t,e){return await this.bucket.put(this.getMetaName(t),JSON.stringify(e),{onlyIf:{etagDoesNotMatch:"*"}})!==null}async write(t,e,a){return(await this.bucket.put(this.getMetaName(t),JSON.stringify(e),{onlyIf:{etagMatches:a}}))?.etag}async swap(t,e){for(let a=0;a<x;a+=1){const s=await this.read(t);if(s===void 0)return d(c.FILE_NOT_FOUND);const r=e(s.state);if(r===s.state||await this.write(t,r,s.etag)!==void 0)return r}return d(c.FILE_CONFLICT,"The upload kept changing while it was being written; resume from the current offset")}async get(t){const e=await this.read(t);if(e===void 0)throw new Error(`Upload state not found for id: ${t}`);return U(e.state.file)}async save(t,e){return(await this.swap(t,s=>{const n=D({...e});for(const o of T)n[o]=s.file[o];return R(n,s.file)?s:{...s,file:n}})).file}async delete(t){await this.bucket.delete(this.getMetaName(t))}}const M="_lunora/uploads/",f=6e4,j=new Set(["checksum","concatenation","creation-defer-length"]),P=new Set([...T,"expiredAt"]),m=i=>d(c.FILE_CONFLICT,i),C=i=>i!==void 0&&i.expiresAt>Date.now(),S=32*1024*1024;class z extends y{meta;bucket;constructor(t,e={}){super(e),this.bucket=t,this.meta=new F(t,e.statePrefix??M)}get tusExtension(){return super.tusExtension.filter(t=>!j.has(t))}async create(t){return this.instrumentOperation("create",async()=>{const e=t.size===void 0||t.size===""?Number.NaN:Number(t.size);if(!Number.isSafeInteger(e)||e<0)return d(c.INVALID_FILE_SIZE,"An upload must declare its size as a non-negative integer");const a=new E(t);if(a.size=e,a.name=this.namingFunction(a),y.assertSafeId(a.id),a.name.includes(this.meta.prefix))return d(c.INVALID_FILE_NAME,"An upload cannot be stored under the upload state prefix");await this.validate(a);const s=await this.meta.read(a.id);return s!==void 0?s.state.file:(a.bytesWritten=0,a.status="created",this.updateTimestamps(a),e===0&&(await this.bucket.put(a.name,new Uint8Array(0),{httpMetadata:{contentType:a.contentType}}),a.status="completed"),await this.meta.create(a.id,{file:a,upload:{parts:[],segments:[]}})?(await this.onCreate(a),a):(await this.meta.read(a.id))?.state.file??m("The upload is being created by another request"))})}async write(t){return this.instrumentOperation("write",async()=>{const e=await this.meta.read(t.id);if(e===void 0)return d(c.FILE_NOT_FOUND);const a=await this.checkIfExpired(e.state.file);if(a.status==="completed"||!("body"in t)||!("start"in t))return a;this.checkPart(a,t);const s=await this.acquireLease(a.id,e.state,e.etag);try{return await this.writeLeased(a,e.state.upload,t.body,s)}catch(r){throw await this.releaseLease(a.id,s).catch(()=>{}),r}})}async get({id:t}){return this.instrumentOperation("get",async()=>{const{file:e,object:a}=await this.completedObject(t);if(a.size>S)return await a.body?.cancel(),d(c.REQUEST_ENTITY_TOO_LARGE,`get() buffers the whole file and refuses one over ${String(S)} bytes; use getStream(), ctx.storage.download() or a signed URL`);const s=Buffer.from(await a.arrayBuffer());return{content:s,contentType:e.contentType,ETag:a.etag,expiredAt:e.expiredAt,id:t,metadata:e.metadata,modifiedAt:e.modifiedAt,name:e.name,originalName:e.originalName,size:s.byteLength}})}async getStream({id:t}){return this.instrumentOperation("getStream",async()=>{const{file:e,object:a}=await this.completedObject(t),s=L.from(b(a.body));return{headers:{"Content-Length":String(a.size),"Content-Type":e.contentType,ETag:a.etag},size:a.size,stream:s}})}async delete({id:t}){return this.instrumentOperation("delete",async()=>{const e=await this.meta.read(t);if(e===void 0)return d(c.FILE_NOT_FOUND);const{file:a,upload:s}=e.state;a.status!=="completed"&&(await this.acquireLease(t,e.state,e.etag),s.uploadId!==void 0&&await this.bucket.resumeMultipartUpload(a.name,s.uploadId).abort().catch(()=>{}),await this.deleteSegments(t)),await this.deleteMeta(t);const r={...a,status:"deleted"};return await this.onDelete(r),r})}async update(t,e){const a=Object.fromEntries(Object.entries(e).filter(([s])=>!P.has(s)));return super.update(t,a)}async list(){return this.instrumentOperation("list",async()=>{const{prefix:t,suffix:e}=this.meta,a=[];let s;do{const r=await this.bucket.list({delimiter:"/",prefix:t,...s===void 0?{}:{cursor:s}});for(const n of r.objects)if(n.key.endsWith(e)){const o=new E({id:this.meta.getIdFromMetaName(n.key),metadata:{}});o.createdAt=(n.uploaded??new Date).toISOString(),o.modifiedAt=o.createdAt,a.push(o)}s=r.truncated===!0?r.cursor:void 0}while(s!==void 0);return a})}copy(){return this.unsupported("copy")}move(){return this.unsupported("move")}async completedObject(t){const e=await this.checkIfExpired(await this.getMeta(t)),a=e.status==="completed"?await this.bucket.get(e.name):void 0;return a==null?d(c.FILE_NOT_FOUND):{file:e,object:a}}unsupported(t){return Promise.reject(new k(c.METHOD_NOT_ALLOWED,`${this.constructor.name} does not implement ${t}()`))}checkPart(t,e){e.start!==t.bytesWritten&&m(`Upload-Offset ${String(e.start)} does not match the upload's offset ${String(t.bytesWritten)}`),e.checksumAlgorithm!==void 0&&e.checksumAlgorithm!==""&&d(c.UNSUPPORTED_CHECKSUM_ALGORITHM),e.contentLength!==void 0&&e.start+e.contentLength>(t.size??0)&&d(c.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length")}async acquireLease(t,e,a){if(C(e.upload.lock))return m("Another request is writing to this upload");const s=crypto.randomUUID(),r={...e.upload,lock:{expiresAt:Date.now()+f,token:s}};return await this.meta.write(t,{...e,upload:r},a)===void 0?m("Another request is writing to this upload"):s}async changeLeased(t,e,a){return this.meta.swap(t,s=>s.upload.lock?.token!==e?m("This request's lease on the upload expired; resume from the current offset"):a(s))}async releaseLease(t,e){await this.changeLeased(t,e,a=>{const s={...a.upload};return delete s.lock,{...a,upload:s}})}async renewLease(t,e){await this.changeLeased(t,e,a=>({...a,upload:{...a.upload,lock:{expiresAt:Date.now()+f,token:e}}}))}async deleteSegments(t){const e=[];let a;do{const s=await this.bucket.list({prefix:this.meta.segmentPrefix(t),...a===void 0?{}:{cursor:a}});e.push(...s.objects.map(r=>r.key)),a=s.truncated===!0?s.cursor:void 0}while(a!==void 0);for(let s=0;s<e.length;s+=1e3)await this.bucket.delete(e.slice(s,s+1e3))}async pump(t,e,a,s,r){const n=b(e);let o=0,u=Date.now();try{for(;;){let h;try{h=await n.next()}catch(l){return l instanceof Error?l:new Error(String(l))}if(h.done===!0)return;if(o+=h.value.byteLength,o>a)return d(c.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length");Date.now()-u>=f/3&&(u=Date.now(),await this.renewLease(s,r)),await t.push(h.value)}}finally{await n.return(void 0).catch(()=>{})}}async writeLeased(t,e,a,s){const r=t.size??0,n=new A(this.bucket,t,e,this.meta.segmentPrefix(t.id),{confirm:async()=>this.renewLease(t.id,s),save:async(p,v)=>{await this.changeLeased(t.id,s,g=>({file:{...g.file,bytesWritten:v,status:"part"},upload:{...p,lock:g.upload.lock}}))},token:s}),o=await this.pump(n,a,r-t.bytesWritten,t.id,s),u=n.offset===r,h=u?await n.finish():void 0;if(!u)try{await n.keepTail()}catch(p){if(o===void 0)throw p}const l={bytesWritten:u?r:n.stored,modifiedAt:new Date().toISOString(),status:u?"completed":"part",...h===void 0?{}:{ETag:h}},_=u?{parts:[],segments:[]}:n.progress();if(await this.changeLeased(t.id,s,p=>({file:{...p.file,...l},upload:_})),await(u?this.deleteSegments(t.id).catch(()=>{}):n.dropConsumed()),o!==void 0)throw o;return{...t,...l}}}const q=(i,t={})=>new z(i,t);export{H as R2_PART_SIZE,q as createR2BindingUploadStorage};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{throwErrorCode as d,ERRORS as u}from"@visulima/storage";const p=i=>{if(i instanceof Uint8Array)return i;if(typeof i=="string")return new TextEncoder().encode(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(i instanceof ArrayBuffer)return new Uint8Array(i);throw new TypeError("Unsupported upload body chunk")},c=i=>typeof i.getReader=="function",m=async function*(t){if(t!=null){if(t instanceof Uint8Array){yield t;return}if(typeof t=="object"&&c(t)){const e=t.getReader();try{for(;;){const{done:s,value:n}=await e.read();if(s)return;yield p(n)}}finally{e.releaseLock()}}if(typeof t=="object"&&Symbol.asyncIterator in t){for await(const e of t)yield p(e);return}throw new TypeError("Unsupported upload body")}};class l{size=0;chunks=[];push(t){t.byteLength>0&&(this.chunks.push(t),this.size+=t.byteLength)}copyFront(t,e,s){let n=0;for(const a of this.chunks){if(n>=s)break;const r=Math.min(a.byteLength,s-n);t.set(a.subarray(0,r),e+n),n+=r}}drop(t){let e=t;for(;e>0;){const s=this.chunks[0];if(s===void 0)break;s.byteLength<=e?(this.chunks.shift(),e-=s.byteLength):(this.chunks[0]=s.subarray(e),e=0)}this.size-=t-e}}const o=5*1024*1024,f=8,h=i=>i.reduce((t,e)=>t+e.size,0);class y{parts;segments;uploadId;bucket;consumed=[];file;lease;pending=new l;segmentPrefix;constructor(t,e,s,n,a){this.bucket=t,this.file=e,this.parts=[...s.parts],this.segments=[...s.segments],this.uploadId=s.uploadId,this.segmentPrefix=n,this.lease=a}get stored(){return this.parts.length*o+h(this.segments)}get offset(){return this.stored+this.pending.size}progress(){return{parts:[...this.parts],segments:[...this.segments],...this.uploadId===void 0?{}:{uploadId:this.uploadId}}}async push(t){for(this.pending.push(t);h(this.segments)+this.pending.size>=o;)await this.flushPart()}async keepTail(){if(this.pending.size===0)return;const t=this.segments.length+1>f,e=t?this.parts.length*o:this.stored,s=t?h(this.segments)+this.pending.size:this.pending.size,n=t?await this.assemble(s):this.front(s),a=`${this.segmentPrefix}${String(e).padStart(16,"0")}-${this.lease.token}`;await this.bucket.put(a,n),t&&this.consumeSegments(),this.segments=[...this.segments,{key:a,size:s}],this.pending.drop(this.pending.size)}async finish(){const t=await this.assemble(h(this.segments)+this.pending.size);await this.lease.confirm();let e;if(this.uploadId===void 0)e=(await this.bucket.put(this.file.name,t,{httpMetadata:{contentType:this.file.contentType}})).etag;else{const s=this.multipart(this.uploadId);if(t.byteLength>0){const a=this.parts.length+1,r=await s.uploadPart(a,t);this.parts.push({etag:r.etag,partNumber:a})}e=(await s.complete(this.parts)).etag}return this.consumeSegments(),this.pending.drop(this.pending.size),e}async dropConsumed(){const t=this.consumed;this.consumed=[],t.length>0&&await this.bucket.delete(t).catch(()=>{})}async assemble(t){const e=new Uint8Array(t);let s=0;for(const n of this.segments){const a=await this.bucket.get(n.key);if(a===null)return d(u.STORAGE_ERROR,`Buffered upload segment ${n.key} is missing`);const r=new Uint8Array(await a.arrayBuffer());e.set(r,s),s+=r.byteLength}return this.pending.copyFront(e,s,t-s),e}front(t){const e=new Uint8Array(t);return this.pending.copyFront(e,0,t),e}consumeSegments(){this.consumed.push(...this.segments.map(t=>t.key)),this.segments=[]}multipart(t){return this.bucket.resumeMultipartUpload(this.file.name,t)}async flushPart(){const t=h(this.segments),e=await this.assemble(o);if(await this.lease.confirm(),this.uploadId===void 0){const a=await this.bucket.createMultipartUpload(this.file.name,{httpMetadata:{contentType:this.file.contentType}});this.uploadId=a.uploadId}const s=this.parts.length+1,n=await this.multipart(this.uploadId).uploadPart(s,e);this.parts.push({etag:n.etag,partNumber:s}),this.pending.drop(o-t),this.consumeSegments(),await this.lease.save(this.progress(),this.stored),await this.dropConsumed()}}export{o as R,y as a,m as r};
|