@lunora/storage 1.0.0-alpha.122 → 1.0.0-alpha.123
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.
|
@@ -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 x,r as E,a as I,N as T}from"./r2-upload-part-writer-SdQlWaAu.mjs";import{R as G}from"./r2-upload-part-writer-SdQlWaAu.mjs";const k=5,S=["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;scope;constructor(t,e,a=()=>""){super({prefix:e,suffix:".json"}),this.bucket=t,this.scope=a}currentPrefix(){const t=this.scope();if(t!==""&&!t.endsWith("/"))throw new TypeError(`@lunora/storage: stateScope must end in "/" (got "${t}")`);return`${this.prefix}${t}`}getMetaName(t){return`${this.currentPrefix()}${t}${this.suffix}`}segmentPrefix(t){return`${this.currentPrefix()}${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 S)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/",m=6e4,R=new Set(["checksum","concatenation","creation-defer-length"]),M=new Set([...S,"expiredAt"]),p=d=>c(n.FILE_CONFLICT,d),P=d=>d!==void 0&&d.expiresAt>Date.now(),b=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,e.stateScope)}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(T);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(T);if(e===0)try{await x(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>b)return await a.body?.cancel(),c(n.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 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()+m,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()+m,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>=m/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 I(this.bucket,t,e,this.meta.segmentPrefix(t.id),{confirm:async()=>this.renewLease(t.id,r),save:async(f,v)=>{await this.changeLeased(t.id,r,w=>({file:{...w.file,bytesWritten:v,status:"part"},upload:{...f,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(f){if(o===void 0)throw f}const l={bytesWritten:u?s:i.stored,modifiedAt:new Date().toISOString(),status:u?"completed":"part",...h===void 0?{}:{ETag:h}},O=u?{parts:[],segments:[]}:i.progress();if(await this.changeLeased(t.id,r,f=>({file:{...f.file,...l},upload:O})),await(u?this.deleteSegments(t.id).catch(()=>{}):i.dropConsumed()),o!==void 0)throw o;return{...t,...l}}}const $=(d,t={})=>new j(d,t);export{G as R2_PART_SIZE,$ as createR2BindingUploadStorage};
|
package/dist/upload.d.mts
CHANGED
|
@@ -203,6 +203,14 @@ type R2BindingUploadStorageOptions = Omit<DeclaredKeys<BaseStorageOptions>, "met
|
|
|
203
203
|
* any upload you expect to take.
|
|
204
204
|
*/
|
|
205
205
|
statePrefix?: string;
|
|
206
|
+
/**
|
|
207
|
+
* The scope a request's upload state is kept under, appended to `statePrefix`: the state of an upload
|
|
208
|
+
* is then readable only by requests in the same scope. Read it from the request's grant, e.g.
|
|
209
|
+
* `() => \`${caller.get().userId}/\``. Must return a string ending in `/`, or `""` for the unscoped root.
|
|
210
|
+
*
|
|
211
|
+
* `list()` reads only the unscoped root, so an upload kept under a scope is not listed.
|
|
212
|
+
*/
|
|
213
|
+
stateScope?: () => string;
|
|
206
214
|
};
|
|
207
215
|
/**
|
|
208
216
|
* Build a resumable-upload provider for `createUploadHandler` over a Worker's
|
package/dist/upload.d.ts
CHANGED
|
@@ -203,6 +203,14 @@ type R2BindingUploadStorageOptions = Omit<DeclaredKeys<BaseStorageOptions>, "met
|
|
|
203
203
|
* any upload you expect to take.
|
|
204
204
|
*/
|
|
205
205
|
statePrefix?: string;
|
|
206
|
+
/**
|
|
207
|
+
* The scope a request's upload state is kept under, appended to `statePrefix`: the state of an upload
|
|
208
|
+
* is then readable only by requests in the same scope. Read it from the request's grant, e.g.
|
|
209
|
+
* `() => \`${caller.get().userId}/\``. Must return a string ending in `/`, or `""` for the unscoped root.
|
|
210
|
+
*
|
|
211
|
+
* `list()` reads only the unscoped root, so an upload kept under a scope is not listed.
|
|
212
|
+
*/
|
|
213
|
+
stateScope?: () => string;
|
|
206
214
|
};
|
|
207
215
|
/**
|
|
208
216
|
* Build a resumable-upload provider for `createUploadHandler` over a Worker's
|
package/dist/upload.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createR2BindingUploadStorage as r}from"./packem_shared/createR2BindingUploadStorage-
|
|
1
|
+
import{createR2BindingUploadStorage as r}from"./packem_shared/createR2BindingUploadStorage-Vjs0vyJJ.mjs";import{createR2UploadStorage as t}from"./packem_shared/createR2UploadStorage-DGB1xJiD.mjs";import{DEFAULT_MAX_UPLOAD_BYTES as d,createUploadContext as U,createUploadHandler as l}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-BOaXqwP5.mjs";import{R}from"./packem_shared/r2-upload-part-writer-SdQlWaAu.mjs";export{d as DEFAULT_MAX_UPLOAD_BYTES,R as R2_PART_SIZE,r as createR2BindingUploadStorage,t as createR2UploadStorage,U as createUploadContext,l as createUploadHandler};
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
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};
|