@lunora/storage 1.0.0-alpha.110 → 1.0.0-alpha.111
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-C0bIs07J.mjs +1 -0
- package/dist/packem_shared/createR2BindingUploadStorage-ByDWP_UR.mjs +1 -0
- package/dist/upload.d.mts +8 -4
- package/dist/upload.d.ts +8 -4
- package/dist/upload.mjs +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-BlLcQ6Uc.mjs +0 -1
- package/dist/packem_shared/createR2BindingUploadStorage-BQE2iEz6.mjs +0 -1
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
package/dist/upload.d.mts
CHANGED
|
@@ -188,11 +188,15 @@ interface R2BindingUploadStorageOptions extends Omit<BaseStorageOptions, "metaSt
|
|
|
188
188
|
* miniflare as in production.
|
|
189
189
|
*
|
|
190
190
|
* Speaks TUS (without the `checksum`, `concatenation` and
|
|
191
|
-
* `creation-defer-length` extensions), chunked REST
|
|
192
|
-
* (a chunk at any other offset is a `409`), and multipart forms.
|
|
191
|
+
* `creation-defer-length` extensions), chunked REST, and multipart forms.
|
|
193
192
|
*
|
|
194
|
-
* Chunked
|
|
195
|
-
*
|
|
193
|
+
* Chunked-REST chunks have to arrive one at a time and in order. A chunk at any
|
|
194
|
+
* offset other than the upload's current one, or one sent while another is
|
|
195
|
+
* still streaming, is a `409` and stores nothing, and it never counts towards
|
|
196
|
+
* completion (see the chunk list in `r2-upload-state-store.ts`). The bundled
|
|
197
|
+
* `@visulima/storage-client` chunked-REST client sends four chunks in parallel,
|
|
198
|
+
* so over this provider it can upload only as one chunk; use TUS for
|
|
199
|
+
* resumable uploads.
|
|
196
200
|
*/
|
|
197
201
|
declare const createR2BindingUploadStorage: (bucket: R2UploadBucket, options?: R2BindingUploadStorageOptions) => UploadStorage;
|
|
198
202
|
export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
|
package/dist/upload.d.ts
CHANGED
|
@@ -188,11 +188,15 @@ interface R2BindingUploadStorageOptions extends Omit<BaseStorageOptions, "metaSt
|
|
|
188
188
|
* miniflare as in production.
|
|
189
189
|
*
|
|
190
190
|
* Speaks TUS (without the `checksum`, `concatenation` and
|
|
191
|
-
* `creation-defer-length` extensions), chunked REST
|
|
192
|
-
* (a chunk at any other offset is a `409`), and multipart forms.
|
|
191
|
+
* `creation-defer-length` extensions), chunked REST, and multipart forms.
|
|
193
192
|
*
|
|
194
|
-
* Chunked
|
|
195
|
-
*
|
|
193
|
+
* Chunked-REST chunks have to arrive one at a time and in order. A chunk at any
|
|
194
|
+
* offset other than the upload's current one, or one sent while another is
|
|
195
|
+
* still streaming, is a `409` and stores nothing, and it never counts towards
|
|
196
|
+
* completion (see the chunk list in `r2-upload-state-store.ts`). The bundled
|
|
197
|
+
* `@visulima/storage-client` chunked-REST client sends four chunks in parallel,
|
|
198
|
+
* so over this provider it can upload only as one chunk; use TUS for
|
|
199
|
+
* resumable uploads.
|
|
196
200
|
*/
|
|
197
201
|
declare const createR2BindingUploadStorage: (bucket: R2UploadBucket, options?: R2BindingUploadStorageOptions) => UploadStorage;
|
|
198
202
|
export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
|
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-ByDWP_UR.mjs";import{DEFAULT_MAX_UPLOAD_BYTES as t,createR2UploadStorage as p,createUploadHandler as d}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-C0bIs07J.mjs";import{R as U}from"./packem_shared/r2-upload-part-writer-BE4HJRLY.mjs";export{t as DEFAULT_MAX_UPLOAD_BYTES,U as R2_PART_SIZE,o as createR2BindingUploadStorage,p as createR2UploadStorage,d 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.111",
|
|
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.25",
|
|
56
56
|
"aws4fetch": "1.0.20"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as T}from"@lunora/errors";import{File as S}from"@visulima/storage";import{Rest as g,Multipart as y,Tus as E}from"@visulima/storage/handler/http/fetch";import{AwsLightStorage as b}from"@visulima/storage/provider/aws-light";const w=100*1024*1024,N="1.0.0",m=(t,e,r)=>{const o={"content-type":"application/json"};return t==="tus"&&(o["Tus-Resumable"]=N),Response.json({error:r},{headers:o,status:e})},d=t=>m(t,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),f=t=>m(t,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),v=t=>{const e=m(t,405,{code:"METHOD_NOT_ALLOWED",message:"DELETE is not allowed on this upload route",name:"MethodNotAllowedError"});return e.headers.set("Allow",t==="multipart"?"GET, OPTIONS, POST":"GET, HEAD, OPTIONS, PATCH, POST, PUT"),e},O=/^\d+$/u,h=(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 s=O.test(n.trim())?Number(n):Number.NaN,i=Number.isSafeInteger(s)?s:Number.POSITIVE_INFINITY;(r===void 0||i>r)&&(r=i)}return r},R=t=>{const e={};for(const[r,o]of t.split(",").map(n=>n.split(" ")))r!==void 0&&r!==""&&(e[r]=o===void 0||o===""?"":Buffer.from(o,"base64").toString());return e},z=t=>{try{const e=JSON.parse(t??"{}");return e!==null&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}},A=(t,e)=>{const r=e==="tus"?R(t.headers.get("Upload-Metadata")??""):z(t.headers.get("X-File-Metadata")),o=t.headers.get("Content-Type"),s=e==="tus"?void 0:o===null||o===""?"application/octet-stream":o,i=new S({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}},F=(t,e)=>e!=="multipart"&&(t.method==="POST"||t.method==="PUT"),I=async(t,e,r)=>{const o=h(e.request,e.protocol);let n;try{n=await t({...e,...A(e.request,e.protocol),declaredSize:o})}catch{return d(e.protocol)}if(n!==void 0)return typeof n!="number"||!Number.isFinite(n)||n<0?d(e.protocol):o===void 0||o>Math.min(n,r)?f(e.protocol):void 0},L=(t,e)=>t==="chunked-rest"?new g(e):t==="multipart"?new y(e):new E(e),M=t=>{const e=t.protocol??"tus",r=t.maxFileSize??w;if(!Number.isFinite(r)||r<0)throw new T("VALIDATION_ERROR",`@lunora/storage: maxFileSize must be a finite, non-negative number (received ${String(r)})`);const o={disableTerminationForFinishedUploads:!0,maxFileSize:r,storage:t.storage},n=L(e,o),{authorize:s,maxFileSizeFor:i}=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 a=>{if(e!=="tus"&&a.method==="DELETE")return v(e);const c=h(a,e);if(c!==void 0&&c>r)return f(e);const p={method:a.method,protocol:e,request:a,url:new URL(a.url)};if(s!==void 0)try{if(await s(p)!==!0)return d(e)}catch{return d(e)}if(i!==void 0&&F(a,e)){const u=await I(i,p,r);if(u!==void 0)return u}return n.fetch(a)},protocol:e}},k=t=>new b({accessKeyId:t.accessKeyId,bucket:t.bucket,endpoint:t.endpoint??`https://${t.accountId}.r2.cloudflarestorage.com`,path:t.path??"/",region:"auto",secretAccessKey:t.secretAccessKey,...t.partSize===void 0?{}:{partSize:t.partSize}});export{w as DEFAULT_MAX_UPLOAD_BYTES,k as createR2UploadStorage,M as createUploadHandler};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Readable as v}from"node:stream";import{MetaStorage as O,throwErrorCode as c,ERRORS as o,AbstractBaseStorage as g,File as y,UploadError as _}from"@visulima/storage";import{r as E,a as N}from"./r2-upload-part-writer-BE4HJRLY.mjs";import{R as W}from"./r2-upload-part-writer-BE4HJRLY.mjs";const A=5,T=["bytesWritten","ETag","id","name","size","status"];class x extends O{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<A;a+=1){const s=await this.read(t);if(s===void 0)return c(o.FILE_NOT_FOUND);const r=e(s.state);if(await this.write(t,r,s.etag)!==void 0)return r}return c(o.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 e.state.file}async save(t,e){return(await this.swap(t,s=>{const i={...e};for(const n of T)i[n]=s.file[n];return{...s,file:i}})).file}async delete(t){await this.bucket.delete(this.getMetaName(t))}}const I="_lunora/uploads/",f=6e4,k=new Set(["checksum","concatenation","creation-defer-length"]),D=new Set([...T,"expiredAt"]),m=u=>c(o.FILE_CONFLICT,u),U=u=>u!==void 0&&u.expiresAt>Date.now(),b=32*1024*1024;class R extends g{meta;bucket;constructor(t,e={}){super(e),this.bucket=t,this.meta=new x(t,e.statePrefix??I)}get tusExtension(){return super.tusExtension.filter(t=>!k.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(o.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(o.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 c(o.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>b)return await a.body?.cancel(),c(o.REQUEST_ENTITY_TOO_LARGE,`get() buffers the whole file and refuses one over ${String(b)} 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=v.from(E(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 c(o.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])=>!D.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 i of r.objects)if(i.key.endsWith(e)){const n=new y({id:this.meta.getIdFromMetaName(i.key),metadata:{}});n.createdAt=(i.uploaded??new Date).toISOString(),n.modifiedAt=n.createdAt,a.push(n)}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?c(o.FILE_NOT_FOUND):{file:e,object:a}}unsupported(t){return Promise.reject(new _(o.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!==""&&c(o.UNSUPPORTED_CHECKSUM_ALGORITHM),e.contentLength!==void 0&&e.start+e.contentLength>(t.size??0)&&c(o.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length")}async acquireLease(t,e,a){if(U(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 i=E(e);let n=0,d=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(n+=h.value.byteLength,n>a)return c(o.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length");Date.now()-d>=f/3&&(d=Date.now(),await this.renewLease(s,r)),await t.push(h.value)}}finally{await i.return(void 0).catch(()=>{})}}async writeLeased(t,e,a,s){const r=t.size??0,i=new N(this.bucket,t,e,this.meta.segmentPrefix(t.id),{confirm:async()=>this.renewLease(t.id,s),save:async(p,L)=>{await this.changeLeased(t.id,s,w=>({file:{...w.file,bytesWritten:L,status:"part"},upload:{...p,lock:w.upload.lock}}))},token:s}),n=await this.pump(i,a,r-t.bytesWritten,t.id,s),d=i.offset===r,h=d?await i.finish():void 0;if(!d)try{await i.keepTail()}catch(p){if(n===void 0)throw p}const l={bytesWritten:d?r:i.stored,modifiedAt:new Date().toISOString(),status:d?"completed":"part",...h===void 0?{}:{ETag:h}},S=d?{parts:[],segments:[]}:i.progress();if(await this.changeLeased(t.id,s,p=>({file:{...p.file,...l},upload:S})),await(d?this.deleteSegments(t.id).catch(()=>{}):i.dropConsumed()),n!==void 0)throw n;return{...t,...l}}}const z=(u,t={})=>new R(u,t);export{W as R2_PART_SIZE,z as createR2BindingUploadStorage};
|