@telestack/storage 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.global.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +10 -2
- package/debug-fetch.js +0 -28
- package/src/BatchBuilder.ts +0 -48
- package/src/CryptoHelper.ts +0 -72
- package/src/HttpClient.ts +0 -152
- package/src/ImageHelper.ts +0 -80
- package/src/QueryBuilder.ts +0 -45
- package/src/StorageRef.ts +0 -153
- package/src/TelestackStorage.ts +0 -93
- package/src/UploadTask.ts +0 -332
- package/src/index.ts +0 -28
- package/src/types.ts +0 -91
- package/test-e2e.js +0 -142
- package/test-e2e.ts +0 -182
- package/test-output.txt +0 -0
- package/tsconfig.json +0 -17
package/dist/index.d.mts
CHANGED
|
@@ -278,6 +278,11 @@ declare class TelestackStorage {
|
|
|
278
278
|
dir(path: string): DirRef;
|
|
279
279
|
/** Helper: List files for the entire bucket/tenant or specific prefix. */
|
|
280
280
|
list(options?: ListOptions): Promise<FileMetadata[]>;
|
|
281
|
+
/** Rename a single file or directory prefix. */
|
|
282
|
+
rename(oldPath: string, newName: string): Promise<{
|
|
283
|
+
success: boolean;
|
|
284
|
+
newPath: string;
|
|
285
|
+
}>;
|
|
281
286
|
/** Get a fluent batch operation builder for mass-mutations over the network. */
|
|
282
287
|
batch(): BatchBuilder;
|
|
283
288
|
/** Get a fluent query builder for metadata and full-text search. */
|
package/dist/index.d.ts
CHANGED
|
@@ -278,6 +278,11 @@ declare class TelestackStorage {
|
|
|
278
278
|
dir(path: string): DirRef;
|
|
279
279
|
/** Helper: List files for the entire bucket/tenant or specific prefix. */
|
|
280
280
|
list(options?: ListOptions): Promise<FileMetadata[]>;
|
|
281
|
+
/** Rename a single file or directory prefix. */
|
|
282
|
+
rename(oldPath: string, newName: string): Promise<{
|
|
283
|
+
success: boolean;
|
|
284
|
+
newPath: string;
|
|
285
|
+
}>;
|
|
281
286
|
/** Get a fluent batch operation builder for mass-mutations over the network. */
|
|
282
287
|
batch(): BatchBuilder;
|
|
283
288
|
/** Get a fluent query builder for metadata and full-text search. */
|
package/dist/index.global.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var TelestackStorageSetup=(()=>{var U=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var S=(l,t)=>{for(var e in t)U(l,e,{get:t[e],enumerable:!0})},M=(l,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of x(t))!C.call(l,r)&&r!==e&&U(l,r,{get:()=>t[r],enumerable:!(s=B(t,r))||s.enumerable});return l};var E=l=>M(U({},"__esModule",{value:!0}),l);var O={};S(O,{BatchBuilder:()=>b,CryptoHelper:()=>d,DirRef:()=>y,FileRef:()=>f,HttpClient:()=>_,ImageHelper:()=>g,QueryBuilder:()=>v,StorageRef:()=>w,TelestackError:()=>u,TelestackStorage:()=>T,UploadTask:()=>m});var _=class{baseUrl;tenantId;headers;constructor(t){this.baseUrl=t.baseUrl.replace(/\/$/,""),this.tenantId=t.tenantId,this.headers={"Content-Type":"application/json","X-Tenant-ID":t.tenantId},t.apiKey?this.headers["X-API-Key"]=t.apiKey:t.token&&(this.headers.Authorization=`Bearer ${t.token}`)}async _fetchWithRetry(t,e,s=3){for(let r=0;r<=s;r++)try{let i=await fetch(t,e);if((i.status===429||i.status>=500)&&r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network error ${i.status}. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}return i}catch(i){if(r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}throw i}throw new Error("Unreachable code in retry logic")}async get(t,e){let s=new URL(`${this.baseUrl}${t}`);e&&Object.entries(e).forEach(([i,a])=>{a!==void 0&&s.searchParams.set(i,a)});let r=await this._fetchWithRetry(s.toString(),{headers:this.headers});return this._handle(r)}async post(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"POST",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async patch(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PATCH",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async put(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PUT",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async delete(t){let e=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"DELETE",headers:this.headers});return this._handle(e)}async uploadToPresignedUrl(t,e,s,r){for(let a=0;a<=3;a++)try{await new Promise((n,h)=>{let o=new XMLHttpRequest;o.open("PUT",t),o.setRequestHeader("Content-Type",s),r&&(o.upload.onprogress=p=>{p.lengthComputable&&r(Math.round(p.loaded/p.total*100))}),o.onload=()=>{o.status>=200&&o.status<300?n():o.status===429||o.status>=500?h(new Error(`Retryable network error: ${o.status}`)):h(new u(`Upload failed: ${o.status}`,o.status))},o.onerror=()=>h(new Error("Retryable network error")),o.send(e)});return}catch(n){if(n instanceof u||a>=3)throw n;let h=Math.min(1e3*Math.pow(2,a),1e4);console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${h}ms...`),await new Promise(o=>setTimeout(o,h))}}async _handle(t){let e=await t.json();if(!t.ok)throw new u(e.error||t.statusText,t.status,e.code);return e}},u=class extends Error{constructor(e,s,r){super(e);this.status=s;this.code=r;this.name="TelestackError"}};var d=class{static async generateKey(){let t=await crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),e=await crypto.subtle.exportKey("raw",t);return btoa(String.fromCharCode(...new Uint8Array(e)))}static async encrypt(t,e){let s=await this._importKey(e),r=crypto.getRandomValues(new Uint8Array(12)),i=await t.arrayBuffer(),a=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},s,i);return new Blob([r,a],{type:"application/octet-stream"})}static async decrypt(t,e,s="application/octet-stream"){let r=await this._importKey(e),i=await t.arrayBuffer(),a=i.slice(0,12),n=i.slice(12),h=await crypto.subtle.decrypt({name:"AES-GCM",iv:new Uint8Array(a)},r,n);return new Blob([h],{type:s})}static async _importKey(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0));return crypto.subtle.importKey("raw",e,{name:"AES-GCM"},!1,["encrypt","decrypt"])}};var g=class{static async compress(t,e={}){if(!t.type.startsWith("image/"))return t;let{maxWidth:s=1920,maxHeight:r=1080,quality:i=.8}=e;return new Promise((a,n)=>{let h=new Image,o=URL.createObjectURL(t);h.onload=()=>{URL.revokeObjectURL(o);let p=h.width,c=h.height;p>s&&(c=Math.round(c*s/p),p=s),c>r&&(p=Math.round(p*r/c),c=r);let R=document.createElement("canvas");R.width=p,R.height=c;let P=R.getContext("2d");if(!P)return n(new Error("Failed to get canvas 2d context for image compression"));P.drawImage(h,0,0,p,c);let k=t.type==="image/png"?"image/png":"image/webp";R.toBlob(I=>{I?a(I):n(new Error("Canvas toBlob failed"))},k,i)},h.onerror=()=>{URL.revokeObjectURL(o),n(new Error("Failed to load image for compression"))},h.src=o})}};var m=class{constructor(t,e,s,r,i,a){this.client=t;this.path=e;this.name=r;this.contentType=i;this.options=a;this._data=s,this._totalBytes=s.size,this._promise=new Promise((n,h)=>{this._resolve=n,this._reject=h}),this._start()}_state="processing";_bytesTransferred=0;_totalBytes=0;_data;_observers=[];_promise;_resolve;_reject;_uploadId;_parts=[];_currentPartIndex=0;_activeXhr;_isResumable=!1;_simpleFileMetadata;on(t,e,s,r){let i=typeof e=="function"?{next:e,error:s,complete:r}:e||{error:s,complete:r};return this._observers.push(i),i.next&&i.next(this.snapshot),()=>{this._observers=this._observers.filter(a=>a!==i)}}pause(){return this._state!=="running"?!1:(this._state="paused",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._notifyObservers(),!0)}resume(){return this._state!=="paused"?!1:(this._state="running",this._notifyObservers(),this._isResumable?this._continueResumable().catch(this._handleError):(this._bytesTransferred=0,this._notifyObservers(),this._startSimple().catch(this._handleError)),!0)}cancel(){if(this._state==="success"||this._state==="error"||this._state==="canceled")return!1;this._state="canceled",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._isResumable&&this._uploadId?this.client.post("/files/resumable/abort",{path:this.path,uploadId:this._uploadId}).catch(()=>{}):!this._isResumable&&this._simpleFileMetadata&&this.client.delete(`/files/${encodeURIComponent(this.path)}`).catch(()=>{}),this._notifyObservers();let t=new Error("Upload canceled by user");return t.name="UploadCanceled",this._handleError(t),!0}get snapshot(){return{bytesTransferred:this._bytesTransferred,totalBytes:this._totalBytes,state:this._state,task:this}}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}[Symbol.toStringTag]="UploadTask";async _start(){try{if(this.options.compressImage&&this._data.type.startsWith("image/")&&(this._data=await g.compress(this._data,this.options.compressImage)),this.options.encryptionKey&&(this._data=await d.encrypt(this._data,this.options.encryptionKey)),this._totalBytes=this._data.size,this._state!=="processing")return;this._state="running",this._notifyObservers();let t=50*1024*1024;this._isResumable=this._totalBytes>=t,this._isResumable?await this._startResumable():await this._startSimple()}catch(t){this._handleError(t)}}async _startSimple(){let t=await this.client.post("/files/upload-url",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});if(this._simpleFileMetadata=t.file,this._state!=="running")return;let e=new XMLHttpRequest;this._activeXhr=e,await new Promise((s,r)=>{e.open("PUT",t.uploadUrl),e.setRequestHeader("Content-Type",this.contentType),e.upload.onprogress=i=>{i.lengthComputable&&this._state==="running"&&(this._bytesTransferred=i.loaded,this._notifyObservers())},e.onload=()=>{e.status>=200&&e.status<300?s():r(new Error(`Upload failed: ${e.status} ${e.responseText}`))},e.onerror=()=>r(new Error("Network error during upload")),e.onabort=()=>r(new Error("Upload aborted")),e.send(this._data)}),this._activeXhr=void 0,this._state==="running"&&(await this.client.post("/files/complete-upload",{path:this.path}),this._bytesTransferred=this._totalBytes,this._state="success",this._notifyObservers(),this._resolve({file:t.file,resumable:!1}))}async _startResumable(){let t=await this.client.post("/files/resumable/init",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});this._uploadId=t.uploadId,this._simpleFileMetadata=t.file,await this._continueResumable()}async _continueResumable(){if(!this._uploadId||!this._simpleFileMetadata)return;let t=this.options.chunkSize??10*1024*1024,e=Math.ceil(this._totalBytes/t);try{for(;this._currentPartIndex<e;){if(this._state!=="running")return;let s=this._currentPartIndex*t,r=Math.min(s+t,this._totalBytes),i=this._data.slice(s,r),a=await this.client.post("/files/resumable/part-url",{path:this.path,uploadId:this._uploadId,partNumber:this._currentPartIndex+1});if(this._state!=="running")return;let n=await this._uploadChunkWithProgress(a.uploadUrl,i,s);this._parts.push({PartNumber:this._currentPartIndex+1,ETag:n}),this._currentPartIndex++,this._bytesTransferred=Math.min(this._currentPartIndex*t,this._totalBytes),this._notifyObservers()}this._state==="running"&&(await this.client.post("/files/resumable/complete",{path:this.path,uploadId:this._uploadId,parts:this._parts}),this._state="success",this._notifyObservers(),this._resolve({file:this._simpleFileMetadata,resumable:!0}))}catch(s){if(s.message==="Upload aborted")return;this._handleError(s)}}_uploadChunkWithProgress(t,e,s){return new Promise((r,i)=>{let a=new XMLHttpRequest;this._activeXhr=a,a.open("PUT",t),a.setRequestHeader("Content-Type",this.contentType),a.upload.onprogress=n=>{n.lengthComputable&&this._state==="running"&&(this._bytesTransferred=s+n.loaded,this._notifyObservers())},a.onload=()=>{if(a.status>=200&&a.status<300){let n=a.getResponseHeader("ETag")||"";r(n.replace(/"/g,""))}else i(new Error(`Part upload failed: ${a.status}`))},a.onerror=()=>i(new Error("Network error during part upload")),a.onabort=()=>i(new Error("Upload aborted")),a.send(e)})}_notifyObservers(){let t=this.snapshot;this._observers.forEach(e=>{t.state==="success"&&e.complete?e.complete():e.next&&e.next(t)})}_handleError=t=>{this._state!=="canceled"&&(this._state="error",this._activeXhr=void 0,this._notifyObservers(),this._observers.forEach(e=>e.error?.(t)),this._reject(t))}};var w=class{constructor(t,e,s){this.client=t;this.path=e;this.tenantId=s}child(t){let s=(this.path?this.path.endsWith("/")?this.path:this.path+"/":"")+t.replace(/^\//,"");return s.endsWith("/")?new y(this.client,s,this.tenantId):new f(this.client,s,this.tenantId)}},y=class extends w{async listAll(t=100){return(await this.client.get("/files",{prefix:this.path,limit:String(t)})).files}},f=class extends w{put(t,e){let s=(t instanceof File,t.size),r=t instanceof File?t.name:this.path.split("/").pop()||"file",i=t.type||"application/octet-stream";return new m(this.client,this.path,t,r,i,e)}putBytes(t,e,s){let r=this.path.split("/").pop()||"file",i=new Blob([t],{type:e});return new m(this.client,this.path,i,r,e,s)}async getDownloadUrl(t){return t?.versionId?(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t.versionId})).downloadUrl:(await this.client.get(`/files/download-url/${encodeURIComponent(this.path)}`)).downloadUrl}async getDecryptedBlob(t){let e=await this.getDownloadUrl(),s=await fetch(e);if(!s.ok)throw new Error(`Failed to download file from S3: ${s.status}`);let r=await s.blob(),i=await this.getMetadata();return await d.decrypt(r,t,i.content_type)}async getMetadata(){return(await this.client.get(`/files/metadata/${encodeURIComponent(this.path)}`)).file}async updateMetadata(t){return(await this.client.patch(`/files/metadata/${encodeURIComponent(this.path)}`,{metadata:t})).file}async delete(){await this.client.delete(`/files/${encodeURIComponent(this.path)}`)}async listVersions(){return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`)}async getVersionUrl(t){return(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t})).downloadUrl}async getTags(){return(await this.client.get(`/files/tags/${encodeURIComponent(this.path)}`)).tags}async setTags(t){await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`,{tags:t})}async setLegalHold(t){await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`,{status:t})}};var b=class{constructor(t){this.client=t}_op=null;_paths=[];_dest="";delete(t){return this._op="delete",this._paths=t,this}copy(t,e){return this._op="copy",this._paths=t,this._dest=e,this}move(t,e){return this._op="move",this._paths=t,this._dest=e,this}async run(){if(!this._op)throw new Error("No batch operation specified");if(this._op==="delete")return this.client.post("/files/batch/delete",{paths:this._paths});let t={sourcePaths:this._paths,destinationPrefix:this._dest};return this.client.post(`/files/batch/${this._op}`,t)}};var v=class{constructor(t){this.client=t}_query="";_metadata={};_limit=20;nameContains(t){return this._query=t,this}where(t,e){return this._metadata[t]=e,this}limit(t){return this._limit=t,this}async get(){let t={q:this._query||void 0,limit:String(this._limit),metadata:Object.keys(this._metadata).length>0?JSON.stringify(this._metadata):void 0};return(await this.client.get("/files/search",t)).files}};var T=class{constructor(t){this.config=t;this.http=new _(t),this.tenantId=t.tenantId}http;tenantId;ref(t){if(t.endsWith("/"))throw new Error("Use .dir() for directory references");return new f(this.http,t,this.tenantId)}dir(t){let e=t.endsWith("/")?t:t+"/";return new y(this.http,e,this.tenantId)}async list(t){let e={prefix:t?.prefix,limit:t?.limit!==void 0?String(t.limit):void 0};return(await this.http.get("/files",e)).files}batch(){return new b(this.http)}query(){return new v(this.http)}async generateApiKey(t){return this.http.post("/internal/keys/generate",{name:t})}async revokeApiKey(t){await this.http.post("/internal/keys/revoke",{keyId:t})}async getBucketInfo(){return this.http.get("/internal/bucket-info")}};return E(O);})();
|
|
1
|
+
"use strict";var TelestackStorageSetup=(()=>{var U=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var S=(l,t)=>{for(var e in t)U(l,e,{get:t[e],enumerable:!0})},M=(l,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of x(t))!C.call(l,r)&&r!==e&&U(l,r,{get:()=>t[r],enumerable:!(s=B(t,r))||s.enumerable});return l};var E=l=>M(U({},"__esModule",{value:!0}),l);var O={};S(O,{BatchBuilder:()=>b,CryptoHelper:()=>d,DirRef:()=>y,FileRef:()=>f,HttpClient:()=>_,ImageHelper:()=>g,QueryBuilder:()=>v,StorageRef:()=>w,TelestackError:()=>u,TelestackStorage:()=>T,UploadTask:()=>m});var _=class{baseUrl;tenantId;headers;constructor(t){this.baseUrl=t.baseUrl.replace(/\/$/,""),this.tenantId=t.tenantId,this.headers={"Content-Type":"application/json","X-Tenant-ID":t.tenantId},t.apiKey?this.headers["X-API-Key"]=t.apiKey:t.token&&(this.headers.Authorization=`Bearer ${t.token}`)}async _fetchWithRetry(t,e,s=3){for(let r=0;r<=s;r++)try{let i=await fetch(t,e);if((i.status===429||i.status>=500)&&r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network error ${i.status}. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}return i}catch(i){if(r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}throw i}throw new Error("Unreachable code in retry logic")}async get(t,e){let s=new URL(`${this.baseUrl}${t}`);e&&Object.entries(e).forEach(([i,a])=>{a!==void 0&&s.searchParams.set(i,a)});let r=await this._fetchWithRetry(s.toString(),{headers:this.headers});return this._handle(r)}async post(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"POST",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async patch(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PATCH",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async put(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PUT",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async delete(t){let e=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"DELETE",headers:this.headers});return this._handle(e)}async uploadToPresignedUrl(t,e,s,r){for(let a=0;a<=3;a++)try{await new Promise((n,h)=>{let o=new XMLHttpRequest;o.open("PUT",t),o.setRequestHeader("Content-Type",s),r&&(o.upload.onprogress=p=>{p.lengthComputable&&r(Math.round(p.loaded/p.total*100))}),o.onload=()=>{o.status>=200&&o.status<300?n():o.status===429||o.status>=500?h(new Error(`Retryable network error: ${o.status}`)):h(new u(`Upload failed: ${o.status}`,o.status))},o.onerror=()=>h(new Error("Retryable network error")),o.send(e)});return}catch(n){if(n instanceof u||a>=3)throw n;let h=Math.min(1e3*Math.pow(2,a),1e4);console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${h}ms...`),await new Promise(o=>setTimeout(o,h))}}async _handle(t){let e=await t.json();if(!t.ok)throw new u(e.error||t.statusText,t.status,e.code);return e}},u=class extends Error{constructor(e,s,r){super(e);this.status=s;this.code=r;this.name="TelestackError"}};var d=class{static async generateKey(){let t=await crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),e=await crypto.subtle.exportKey("raw",t);return btoa(String.fromCharCode(...new Uint8Array(e)))}static async encrypt(t,e){let s=await this._importKey(e),r=crypto.getRandomValues(new Uint8Array(12)),i=await t.arrayBuffer(),a=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},s,i);return new Blob([r,a],{type:"application/octet-stream"})}static async decrypt(t,e,s="application/octet-stream"){let r=await this._importKey(e),i=await t.arrayBuffer(),a=i.slice(0,12),n=i.slice(12),h=await crypto.subtle.decrypt({name:"AES-GCM",iv:new Uint8Array(a)},r,n);return new Blob([h],{type:s})}static async _importKey(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0));return crypto.subtle.importKey("raw",e,{name:"AES-GCM"},!1,["encrypt","decrypt"])}};var g=class{static async compress(t,e={}){if(!t.type.startsWith("image/"))return t;let{maxWidth:s=1920,maxHeight:r=1080,quality:i=.8}=e;return new Promise((a,n)=>{let h=new Image,o=URL.createObjectURL(t);h.onload=()=>{URL.revokeObjectURL(o);let p=h.width,c=h.height;p>s&&(c=Math.round(c*s/p),p=s),c>r&&(p=Math.round(p*r/c),c=r);let R=document.createElement("canvas");R.width=p,R.height=c;let P=R.getContext("2d");if(!P)return n(new Error("Failed to get canvas 2d context for image compression"));P.drawImage(h,0,0,p,c);let k=t.type==="image/png"?"image/png":"image/webp";R.toBlob(I=>{I?a(I):n(new Error("Canvas toBlob failed"))},k,i)},h.onerror=()=>{URL.revokeObjectURL(o),n(new Error("Failed to load image for compression"))},h.src=o})}};var m=class{constructor(t,e,s,r,i,a){this.client=t;this.path=e;this.name=r;this.contentType=i;this.options=a;this._data=s,this._totalBytes=s.size,this._promise=new Promise((n,h)=>{this._resolve=n,this._reject=h}),this._start()}_state="processing";_bytesTransferred=0;_totalBytes=0;_data;_observers=[];_promise;_resolve;_reject;_uploadId;_parts=[];_currentPartIndex=0;_activeXhr;_isResumable=!1;_simpleFileMetadata;on(t,e,s,r){let i=typeof e=="function"?{next:e,error:s,complete:r}:e||{error:s,complete:r};return this._observers.push(i),i.next&&i.next(this.snapshot),()=>{this._observers=this._observers.filter(a=>a!==i)}}pause(){return this._state!=="running"?!1:(this._state="paused",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._notifyObservers(),!0)}resume(){return this._state!=="paused"?!1:(this._state="running",this._notifyObservers(),this._isResumable?this._continueResumable().catch(this._handleError):(this._bytesTransferred=0,this._notifyObservers(),this._startSimple().catch(this._handleError)),!0)}cancel(){if(this._state==="success"||this._state==="error"||this._state==="canceled")return!1;this._state="canceled",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._isResumable&&this._uploadId?this.client.post("/files/resumable/abort",{path:this.path,uploadId:this._uploadId}).catch(()=>{}):!this._isResumable&&this._simpleFileMetadata&&this.client.delete(`/files/${encodeURIComponent(this.path)}`).catch(()=>{}),this._notifyObservers();let t=new Error("Upload canceled by user");return t.name="UploadCanceled",this._handleError(t),!0}get snapshot(){return{bytesTransferred:this._bytesTransferred,totalBytes:this._totalBytes,state:this._state,task:this}}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}[Symbol.toStringTag]="UploadTask";async _start(){try{if(this.options.compressImage&&this._data.type.startsWith("image/")&&(this._data=await g.compress(this._data,this.options.compressImage)),this.options.encryptionKey&&(this._data=await d.encrypt(this._data,this.options.encryptionKey)),this._totalBytes=this._data.size,this._state!=="processing")return;this._state="running",this._notifyObservers();let t=50*1024*1024;this._isResumable=this._totalBytes>=t,this._isResumable?await this._startResumable():await this._startSimple()}catch(t){this._handleError(t)}}async _startSimple(){let t=await this.client.post("/files/upload-url",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});if(this._simpleFileMetadata=t.file,this._state!=="running")return;let e=new XMLHttpRequest;this._activeXhr=e,await new Promise((s,r)=>{e.open("PUT",t.uploadUrl),e.setRequestHeader("Content-Type",this.contentType),e.upload.onprogress=i=>{i.lengthComputable&&this._state==="running"&&(this._bytesTransferred=i.loaded,this._notifyObservers())},e.onload=()=>{e.status>=200&&e.status<300?s():r(new Error(`Upload failed: ${e.status} ${e.responseText}`))},e.onerror=()=>r(new Error("Network error during upload")),e.onabort=()=>r(new Error("Upload aborted")),e.send(this._data)}),this._activeXhr=void 0,this._state==="running"&&(await this.client.post("/files/complete-upload",{path:this.path}),this._bytesTransferred=this._totalBytes,this._state="success",this._notifyObservers(),this._resolve({file:t.file,resumable:!1}))}async _startResumable(){let t=await this.client.post("/files/resumable/init",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});this._uploadId=t.uploadId,this._simpleFileMetadata=t.file,await this._continueResumable()}async _continueResumable(){if(!this._uploadId||!this._simpleFileMetadata)return;let t=this.options.chunkSize??10*1024*1024,e=Math.ceil(this._totalBytes/t);try{for(;this._currentPartIndex<e;){if(this._state!=="running")return;let s=this._currentPartIndex*t,r=Math.min(s+t,this._totalBytes),i=this._data.slice(s,r),a=await this.client.post("/files/resumable/part-url",{path:this.path,uploadId:this._uploadId,partNumber:this._currentPartIndex+1});if(this._state!=="running")return;let n=await this._uploadChunkWithProgress(a.uploadUrl,i,s);this._parts.push({PartNumber:this._currentPartIndex+1,ETag:n}),this._currentPartIndex++,this._bytesTransferred=Math.min(this._currentPartIndex*t,this._totalBytes),this._notifyObservers()}this._state==="running"&&(await this.client.post("/files/resumable/complete",{path:this.path,uploadId:this._uploadId,parts:this._parts}),this._state="success",this._notifyObservers(),this._resolve({file:this._simpleFileMetadata,resumable:!0}))}catch(s){if(s.message==="Upload aborted")return;this._handleError(s)}}_uploadChunkWithProgress(t,e,s){return new Promise((r,i)=>{let a=new XMLHttpRequest;this._activeXhr=a,a.open("PUT",t),a.setRequestHeader("Content-Type",this.contentType),a.upload.onprogress=n=>{n.lengthComputable&&this._state==="running"&&(this._bytesTransferred=s+n.loaded,this._notifyObservers())},a.onload=()=>{if(a.status>=200&&a.status<300){let n=a.getResponseHeader("ETag")||"";r(n.replace(/"/g,""))}else i(new Error(`Part upload failed: ${a.status}`))},a.onerror=()=>i(new Error("Network error during part upload")),a.onabort=()=>i(new Error("Upload aborted")),a.send(e)})}_notifyObservers(){let t=this.snapshot;this._observers.forEach(e=>{t.state==="success"&&e.complete?e.complete():e.next&&e.next(t)})}_handleError=t=>{this._state!=="canceled"&&(this._state="error",this._activeXhr=void 0,this._notifyObservers(),this._observers.forEach(e=>e.error?.(t)),this._reject(t))}};var w=class{constructor(t,e,s){this.client=t;this.path=e;this.tenantId=s}child(t){let s=(this.path?this.path.endsWith("/")?this.path:this.path+"/":"")+t.replace(/^\//,"");return s.endsWith("/")?new y(this.client,s,this.tenantId):new f(this.client,s,this.tenantId)}},y=class extends w{async listAll(t=100){return(await this.client.get("/files",{prefix:this.path,limit:String(t)})).files}},f=class extends w{put(t,e){let s=(t instanceof File,t.size),r=t instanceof File?t.name:this.path.split("/").pop()||"file",i=t.type||"application/octet-stream";return new m(this.client,this.path,t,r,i,e)}putBytes(t,e,s){let r=this.path.split("/").pop()||"file",i=new Blob([t],{type:e});return new m(this.client,this.path,i,r,e,s)}async getDownloadUrl(t){return t?.versionId?(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t.versionId})).downloadUrl:(await this.client.get(`/files/download-url/${encodeURIComponent(this.path)}`)).downloadUrl}async getDecryptedBlob(t){let e=await this.getDownloadUrl(),s=await fetch(e);if(!s.ok)throw new Error(`Failed to download file from S3: ${s.status}`);let r=await s.blob(),i=await this.getMetadata();return await d.decrypt(r,t,i.content_type)}async getMetadata(){return(await this.client.get(`/files/metadata/${encodeURIComponent(this.path)}`)).file}async updateMetadata(t){return(await this.client.patch(`/files/metadata/${encodeURIComponent(this.path)}`,{metadata:t})).file}async delete(){await this.client.delete(`/files/${encodeURIComponent(this.path)}`)}async listVersions(){return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`)}async getVersionUrl(t){return(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t})).downloadUrl}async getTags(){return(await this.client.get(`/files/tags/${encodeURIComponent(this.path)}`)).tags}async setTags(t){await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`,{tags:t})}async setLegalHold(t){await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`,{status:t})}};var b=class{constructor(t){this.client=t}_op=null;_paths=[];_dest="";delete(t){return this._op="delete",this._paths=t,this}copy(t,e){return this._op="copy",this._paths=t,this._dest=e,this}move(t,e){return this._op="move",this._paths=t,this._dest=e,this}async run(){if(!this._op)throw new Error("No batch operation specified");if(this._op==="delete")return this.client.post("/files/batch/delete",{paths:this._paths});let t={sourcePaths:this._paths,destinationPrefix:this._dest};return this.client.post(`/files/batch/${this._op}`,t)}};var v=class{constructor(t){this.client=t}_query="";_metadata={};_limit=20;nameContains(t){return this._query=t,this}where(t,e){return this._metadata[t]=e,this}limit(t){return this._limit=t,this}async get(){let t={q:this._query||void 0,limit:String(this._limit),metadata:Object.keys(this._metadata).length>0?JSON.stringify(this._metadata):void 0};return(await this.client.get("/files/search",t)).files}};var T=class{constructor(t){this.config=t;this.http=new _(t),this.tenantId=t.tenantId}http;tenantId;ref(t){if(t.endsWith("/"))throw new Error("Use .dir() for directory references");return new f(this.http,t,this.tenantId)}dir(t){let e=t.endsWith("/")?t:t+"/";return new y(this.http,e,this.tenantId)}async list(t){let e={prefix:t?.prefix||"",limit:String(t?.limit||100)};return(await this.http.get("/files",e)).files}async rename(t,e){return this.http.post("/files/rename",{oldPath:t,newName:e})}batch(){return new b(this.http)}query(){return new v(this.http)}async generateApiKey(t){return this.http.post("/internal/keys/generate",{name:t})}async revokeApiKey(t){await this.http.post("/internal/keys/revoke",{keyId:t})}async getBucketInfo(){return this.http.get("/internal/bucket-info")}};return E(O);})();
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var U=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var S=(l,t)=>{for(var e in t)U(l,e,{get:t[e],enumerable:!0})},M=(l,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of x(t))!C.call(l,r)&&r!==e&&U(l,r,{get:()=>t[r],enumerable:!(s=B(t,r))||s.enumerable});return l};var E=l=>M(U({},"__esModule",{value:!0}),l);var O={};S(O,{BatchBuilder:()=>b,CryptoHelper:()=>d,DirRef:()=>y,FileRef:()=>f,HttpClient:()=>_,ImageHelper:()=>g,QueryBuilder:()=>v,StorageRef:()=>w,TelestackError:()=>u,TelestackStorage:()=>T,UploadTask:()=>m});module.exports=E(O);var _=class{baseUrl;tenantId;headers;constructor(t){this.baseUrl=t.baseUrl.replace(/\/$/,""),this.tenantId=t.tenantId,this.headers={"Content-Type":"application/json","X-Tenant-ID":t.tenantId},t.apiKey?this.headers["X-API-Key"]=t.apiKey:t.token&&(this.headers.Authorization=`Bearer ${t.token}`)}async _fetchWithRetry(t,e,s=3){for(let r=0;r<=s;r++)try{let i=await fetch(t,e);if((i.status===429||i.status>=500)&&r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network error ${i.status}. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}return i}catch(i){if(r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}throw i}throw new Error("Unreachable code in retry logic")}async get(t,e){let s=new URL(`${this.baseUrl}${t}`);e&&Object.entries(e).forEach(([i,a])=>{a!==void 0&&s.searchParams.set(i,a)});let r=await this._fetchWithRetry(s.toString(),{headers:this.headers});return this._handle(r)}async post(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"POST",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async patch(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PATCH",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async put(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PUT",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async delete(t){let e=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"DELETE",headers:this.headers});return this._handle(e)}async uploadToPresignedUrl(t,e,s,r){for(let a=0;a<=3;a++)try{await new Promise((n,h)=>{let o=new XMLHttpRequest;o.open("PUT",t),o.setRequestHeader("Content-Type",s),r&&(o.upload.onprogress=p=>{p.lengthComputable&&r(Math.round(p.loaded/p.total*100))}),o.onload=()=>{o.status>=200&&o.status<300?n():o.status===429||o.status>=500?h(new Error(`Retryable network error: ${o.status}`)):h(new u(`Upload failed: ${o.status}`,o.status))},o.onerror=()=>h(new Error("Retryable network error")),o.send(e)});return}catch(n){if(n instanceof u||a>=3)throw n;let h=Math.min(1e3*Math.pow(2,a),1e4);console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${h}ms...`),await new Promise(o=>setTimeout(o,h))}}async _handle(t){let e=await t.json();if(!t.ok)throw new u(e.error||t.statusText,t.status,e.code);return e}},u=class extends Error{constructor(e,s,r){super(e);this.status=s;this.code=r;this.name="TelestackError"}};var d=class{static async generateKey(){let t=await crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),e=await crypto.subtle.exportKey("raw",t);return btoa(String.fromCharCode(...new Uint8Array(e)))}static async encrypt(t,e){let s=await this._importKey(e),r=crypto.getRandomValues(new Uint8Array(12)),i=await t.arrayBuffer(),a=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},s,i);return new Blob([r,a],{type:"application/octet-stream"})}static async decrypt(t,e,s="application/octet-stream"){let r=await this._importKey(e),i=await t.arrayBuffer(),a=i.slice(0,12),n=i.slice(12),h=await crypto.subtle.decrypt({name:"AES-GCM",iv:new Uint8Array(a)},r,n);return new Blob([h],{type:s})}static async _importKey(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0));return crypto.subtle.importKey("raw",e,{name:"AES-GCM"},!1,["encrypt","decrypt"])}};var g=class{static async compress(t,e={}){if(!t.type.startsWith("image/"))return t;let{maxWidth:s=1920,maxHeight:r=1080,quality:i=.8}=e;return new Promise((a,n)=>{let h=new Image,o=URL.createObjectURL(t);h.onload=()=>{URL.revokeObjectURL(o);let p=h.width,c=h.height;p>s&&(c=Math.round(c*s/p),p=s),c>r&&(p=Math.round(p*r/c),c=r);let R=document.createElement("canvas");R.width=p,R.height=c;let P=R.getContext("2d");if(!P)return n(new Error("Failed to get canvas 2d context for image compression"));P.drawImage(h,0,0,p,c);let k=t.type==="image/png"?"image/png":"image/webp";R.toBlob(I=>{I?a(I):n(new Error("Canvas toBlob failed"))},k,i)},h.onerror=()=>{URL.revokeObjectURL(o),n(new Error("Failed to load image for compression"))},h.src=o})}};var m=class{constructor(t,e,s,r,i,a){this.client=t;this.path=e;this.name=r;this.contentType=i;this.options=a;this._data=s,this._totalBytes=s.size,this._promise=new Promise((n,h)=>{this._resolve=n,this._reject=h}),this._start()}_state="processing";_bytesTransferred=0;_totalBytes=0;_data;_observers=[];_promise;_resolve;_reject;_uploadId;_parts=[];_currentPartIndex=0;_activeXhr;_isResumable=!1;_simpleFileMetadata;on(t,e,s,r){let i=typeof e=="function"?{next:e,error:s,complete:r}:e||{error:s,complete:r};return this._observers.push(i),i.next&&i.next(this.snapshot),()=>{this._observers=this._observers.filter(a=>a!==i)}}pause(){return this._state!=="running"?!1:(this._state="paused",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._notifyObservers(),!0)}resume(){return this._state!=="paused"?!1:(this._state="running",this._notifyObservers(),this._isResumable?this._continueResumable().catch(this._handleError):(this._bytesTransferred=0,this._notifyObservers(),this._startSimple().catch(this._handleError)),!0)}cancel(){if(this._state==="success"||this._state==="error"||this._state==="canceled")return!1;this._state="canceled",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._isResumable&&this._uploadId?this.client.post("/files/resumable/abort",{path:this.path,uploadId:this._uploadId}).catch(()=>{}):!this._isResumable&&this._simpleFileMetadata&&this.client.delete(`/files/${encodeURIComponent(this.path)}`).catch(()=>{}),this._notifyObservers();let t=new Error("Upload canceled by user");return t.name="UploadCanceled",this._handleError(t),!0}get snapshot(){return{bytesTransferred:this._bytesTransferred,totalBytes:this._totalBytes,state:this._state,task:this}}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}[Symbol.toStringTag]="UploadTask";async _start(){try{if(this.options.compressImage&&this._data.type.startsWith("image/")&&(this._data=await g.compress(this._data,this.options.compressImage)),this.options.encryptionKey&&(this._data=await d.encrypt(this._data,this.options.encryptionKey)),this._totalBytes=this._data.size,this._state!=="processing")return;this._state="running",this._notifyObservers();let t=50*1024*1024;this._isResumable=this._totalBytes>=t,this._isResumable?await this._startResumable():await this._startSimple()}catch(t){this._handleError(t)}}async _startSimple(){let t=await this.client.post("/files/upload-url",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});if(this._simpleFileMetadata=t.file,this._state!=="running")return;let e=new XMLHttpRequest;this._activeXhr=e,await new Promise((s,r)=>{e.open("PUT",t.uploadUrl),e.setRequestHeader("Content-Type",this.contentType),e.upload.onprogress=i=>{i.lengthComputable&&this._state==="running"&&(this._bytesTransferred=i.loaded,this._notifyObservers())},e.onload=()=>{e.status>=200&&e.status<300?s():r(new Error(`Upload failed: ${e.status} ${e.responseText}`))},e.onerror=()=>r(new Error("Network error during upload")),e.onabort=()=>r(new Error("Upload aborted")),e.send(this._data)}),this._activeXhr=void 0,this._state==="running"&&(await this.client.post("/files/complete-upload",{path:this.path}),this._bytesTransferred=this._totalBytes,this._state="success",this._notifyObservers(),this._resolve({file:t.file,resumable:!1}))}async _startResumable(){let t=await this.client.post("/files/resumable/init",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});this._uploadId=t.uploadId,this._simpleFileMetadata=t.file,await this._continueResumable()}async _continueResumable(){if(!this._uploadId||!this._simpleFileMetadata)return;let t=this.options.chunkSize??10*1024*1024,e=Math.ceil(this._totalBytes/t);try{for(;this._currentPartIndex<e;){if(this._state!=="running")return;let s=this._currentPartIndex*t,r=Math.min(s+t,this._totalBytes),i=this._data.slice(s,r),a=await this.client.post("/files/resumable/part-url",{path:this.path,uploadId:this._uploadId,partNumber:this._currentPartIndex+1});if(this._state!=="running")return;let n=await this._uploadChunkWithProgress(a.uploadUrl,i,s);this._parts.push({PartNumber:this._currentPartIndex+1,ETag:n}),this._currentPartIndex++,this._bytesTransferred=Math.min(this._currentPartIndex*t,this._totalBytes),this._notifyObservers()}this._state==="running"&&(await this.client.post("/files/resumable/complete",{path:this.path,uploadId:this._uploadId,parts:this._parts}),this._state="success",this._notifyObservers(),this._resolve({file:this._simpleFileMetadata,resumable:!0}))}catch(s){if(s.message==="Upload aborted")return;this._handleError(s)}}_uploadChunkWithProgress(t,e,s){return new Promise((r,i)=>{let a=new XMLHttpRequest;this._activeXhr=a,a.open("PUT",t),a.setRequestHeader("Content-Type",this.contentType),a.upload.onprogress=n=>{n.lengthComputable&&this._state==="running"&&(this._bytesTransferred=s+n.loaded,this._notifyObservers())},a.onload=()=>{if(a.status>=200&&a.status<300){let n=a.getResponseHeader("ETag")||"";r(n.replace(/"/g,""))}else i(new Error(`Part upload failed: ${a.status}`))},a.onerror=()=>i(new Error("Network error during part upload")),a.onabort=()=>i(new Error("Upload aborted")),a.send(e)})}_notifyObservers(){let t=this.snapshot;this._observers.forEach(e=>{t.state==="success"&&e.complete?e.complete():e.next&&e.next(t)})}_handleError=t=>{this._state!=="canceled"&&(this._state="error",this._activeXhr=void 0,this._notifyObservers(),this._observers.forEach(e=>e.error?.(t)),this._reject(t))}};var w=class{constructor(t,e,s){this.client=t;this.path=e;this.tenantId=s}child(t){let s=(this.path?this.path.endsWith("/")?this.path:this.path+"/":"")+t.replace(/^\//,"");return s.endsWith("/")?new y(this.client,s,this.tenantId):new f(this.client,s,this.tenantId)}},y=class extends w{async listAll(t=100){return(await this.client.get("/files",{prefix:this.path,limit:String(t)})).files}},f=class extends w{put(t,e){let s=(t instanceof File,t.size),r=t instanceof File?t.name:this.path.split("/").pop()||"file",i=t.type||"application/octet-stream";return new m(this.client,this.path,t,r,i,e)}putBytes(t,e,s){let r=this.path.split("/").pop()||"file",i=new Blob([t],{type:e});return new m(this.client,this.path,i,r,e,s)}async getDownloadUrl(t){return t?.versionId?(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t.versionId})).downloadUrl:(await this.client.get(`/files/download-url/${encodeURIComponent(this.path)}`)).downloadUrl}async getDecryptedBlob(t){let e=await this.getDownloadUrl(),s=await fetch(e);if(!s.ok)throw new Error(`Failed to download file from S3: ${s.status}`);let r=await s.blob(),i=await this.getMetadata();return await d.decrypt(r,t,i.content_type)}async getMetadata(){return(await this.client.get(`/files/metadata/${encodeURIComponent(this.path)}`)).file}async updateMetadata(t){return(await this.client.patch(`/files/metadata/${encodeURIComponent(this.path)}`,{metadata:t})).file}async delete(){await this.client.delete(`/files/${encodeURIComponent(this.path)}`)}async listVersions(){return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`)}async getVersionUrl(t){return(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t})).downloadUrl}async getTags(){return(await this.client.get(`/files/tags/${encodeURIComponent(this.path)}`)).tags}async setTags(t){await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`,{tags:t})}async setLegalHold(t){await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`,{status:t})}};var b=class{constructor(t){this.client=t}_op=null;_paths=[];_dest="";delete(t){return this._op="delete",this._paths=t,this}copy(t,e){return this._op="copy",this._paths=t,this._dest=e,this}move(t,e){return this._op="move",this._paths=t,this._dest=e,this}async run(){if(!this._op)throw new Error("No batch operation specified");if(this._op==="delete")return this.client.post("/files/batch/delete",{paths:this._paths});let t={sourcePaths:this._paths,destinationPrefix:this._dest};return this.client.post(`/files/batch/${this._op}`,t)}};var v=class{constructor(t){this.client=t}_query="";_metadata={};_limit=20;nameContains(t){return this._query=t,this}where(t,e){return this._metadata[t]=e,this}limit(t){return this._limit=t,this}async get(){let t={q:this._query||void 0,limit:String(this._limit),metadata:Object.keys(this._metadata).length>0?JSON.stringify(this._metadata):void 0};return(await this.client.get("/files/search",t)).files}};var T=class{constructor(t){this.config=t;this.http=new _(t),this.tenantId=t.tenantId}http;tenantId;ref(t){if(t.endsWith("/"))throw new Error("Use .dir() for directory references");return new f(this.http,t,this.tenantId)}dir(t){let e=t.endsWith("/")?t:t+"/";return new y(this.http,e,this.tenantId)}async list(t){let e={prefix:t?.prefix,limit:t?.limit!==void 0?String(t.limit):void 0};return(await this.http.get("/files",e)).files}batch(){return new b(this.http)}query(){return new v(this.http)}async generateApiKey(t){return this.http.post("/internal/keys/generate",{name:t})}async revokeApiKey(t){await this.http.post("/internal/keys/revoke",{keyId:t})}async getBucketInfo(){return this.http.get("/internal/bucket-info")}};0&&(module.exports={BatchBuilder,CryptoHelper,DirRef,FileRef,HttpClient,ImageHelper,QueryBuilder,StorageRef,TelestackError,TelestackStorage,UploadTask});
|
|
1
|
+
"use strict";var U=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var S=(l,t)=>{for(var e in t)U(l,e,{get:t[e],enumerable:!0})},M=(l,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of x(t))!C.call(l,r)&&r!==e&&U(l,r,{get:()=>t[r],enumerable:!(s=B(t,r))||s.enumerable});return l};var E=l=>M(U({},"__esModule",{value:!0}),l);var O={};S(O,{BatchBuilder:()=>b,CryptoHelper:()=>d,DirRef:()=>y,FileRef:()=>f,HttpClient:()=>_,ImageHelper:()=>g,QueryBuilder:()=>v,StorageRef:()=>w,TelestackError:()=>u,TelestackStorage:()=>T,UploadTask:()=>m});module.exports=E(O);var _=class{baseUrl;tenantId;headers;constructor(t){this.baseUrl=t.baseUrl.replace(/\/$/,""),this.tenantId=t.tenantId,this.headers={"Content-Type":"application/json","X-Tenant-ID":t.tenantId},t.apiKey?this.headers["X-API-Key"]=t.apiKey:t.token&&(this.headers.Authorization=`Bearer ${t.token}`)}async _fetchWithRetry(t,e,s=3){for(let r=0;r<=s;r++)try{let i=await fetch(t,e);if((i.status===429||i.status>=500)&&r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network error ${i.status}. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}return i}catch(i){if(r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}throw i}throw new Error("Unreachable code in retry logic")}async get(t,e){let s=new URL(`${this.baseUrl}${t}`);e&&Object.entries(e).forEach(([i,a])=>{a!==void 0&&s.searchParams.set(i,a)});let r=await this._fetchWithRetry(s.toString(),{headers:this.headers});return this._handle(r)}async post(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"POST",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async patch(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PATCH",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async put(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PUT",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async delete(t){let e=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"DELETE",headers:this.headers});return this._handle(e)}async uploadToPresignedUrl(t,e,s,r){for(let a=0;a<=3;a++)try{await new Promise((n,h)=>{let o=new XMLHttpRequest;o.open("PUT",t),o.setRequestHeader("Content-Type",s),r&&(o.upload.onprogress=p=>{p.lengthComputable&&r(Math.round(p.loaded/p.total*100))}),o.onload=()=>{o.status>=200&&o.status<300?n():o.status===429||o.status>=500?h(new Error(`Retryable network error: ${o.status}`)):h(new u(`Upload failed: ${o.status}`,o.status))},o.onerror=()=>h(new Error("Retryable network error")),o.send(e)});return}catch(n){if(n instanceof u||a>=3)throw n;let h=Math.min(1e3*Math.pow(2,a),1e4);console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${h}ms...`),await new Promise(o=>setTimeout(o,h))}}async _handle(t){let e=await t.json();if(!t.ok)throw new u(e.error||t.statusText,t.status,e.code);return e}},u=class extends Error{constructor(e,s,r){super(e);this.status=s;this.code=r;this.name="TelestackError"}};var d=class{static async generateKey(){let t=await crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),e=await crypto.subtle.exportKey("raw",t);return btoa(String.fromCharCode(...new Uint8Array(e)))}static async encrypt(t,e){let s=await this._importKey(e),r=crypto.getRandomValues(new Uint8Array(12)),i=await t.arrayBuffer(),a=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},s,i);return new Blob([r,a],{type:"application/octet-stream"})}static async decrypt(t,e,s="application/octet-stream"){let r=await this._importKey(e),i=await t.arrayBuffer(),a=i.slice(0,12),n=i.slice(12),h=await crypto.subtle.decrypt({name:"AES-GCM",iv:new Uint8Array(a)},r,n);return new Blob([h],{type:s})}static async _importKey(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0));return crypto.subtle.importKey("raw",e,{name:"AES-GCM"},!1,["encrypt","decrypt"])}};var g=class{static async compress(t,e={}){if(!t.type.startsWith("image/"))return t;let{maxWidth:s=1920,maxHeight:r=1080,quality:i=.8}=e;return new Promise((a,n)=>{let h=new Image,o=URL.createObjectURL(t);h.onload=()=>{URL.revokeObjectURL(o);let p=h.width,c=h.height;p>s&&(c=Math.round(c*s/p),p=s),c>r&&(p=Math.round(p*r/c),c=r);let R=document.createElement("canvas");R.width=p,R.height=c;let P=R.getContext("2d");if(!P)return n(new Error("Failed to get canvas 2d context for image compression"));P.drawImage(h,0,0,p,c);let k=t.type==="image/png"?"image/png":"image/webp";R.toBlob(I=>{I?a(I):n(new Error("Canvas toBlob failed"))},k,i)},h.onerror=()=>{URL.revokeObjectURL(o),n(new Error("Failed to load image for compression"))},h.src=o})}};var m=class{constructor(t,e,s,r,i,a){this.client=t;this.path=e;this.name=r;this.contentType=i;this.options=a;this._data=s,this._totalBytes=s.size,this._promise=new Promise((n,h)=>{this._resolve=n,this._reject=h}),this._start()}_state="processing";_bytesTransferred=0;_totalBytes=0;_data;_observers=[];_promise;_resolve;_reject;_uploadId;_parts=[];_currentPartIndex=0;_activeXhr;_isResumable=!1;_simpleFileMetadata;on(t,e,s,r){let i=typeof e=="function"?{next:e,error:s,complete:r}:e||{error:s,complete:r};return this._observers.push(i),i.next&&i.next(this.snapshot),()=>{this._observers=this._observers.filter(a=>a!==i)}}pause(){return this._state!=="running"?!1:(this._state="paused",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._notifyObservers(),!0)}resume(){return this._state!=="paused"?!1:(this._state="running",this._notifyObservers(),this._isResumable?this._continueResumable().catch(this._handleError):(this._bytesTransferred=0,this._notifyObservers(),this._startSimple().catch(this._handleError)),!0)}cancel(){if(this._state==="success"||this._state==="error"||this._state==="canceled")return!1;this._state="canceled",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._isResumable&&this._uploadId?this.client.post("/files/resumable/abort",{path:this.path,uploadId:this._uploadId}).catch(()=>{}):!this._isResumable&&this._simpleFileMetadata&&this.client.delete(`/files/${encodeURIComponent(this.path)}`).catch(()=>{}),this._notifyObservers();let t=new Error("Upload canceled by user");return t.name="UploadCanceled",this._handleError(t),!0}get snapshot(){return{bytesTransferred:this._bytesTransferred,totalBytes:this._totalBytes,state:this._state,task:this}}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}[Symbol.toStringTag]="UploadTask";async _start(){try{if(this.options.compressImage&&this._data.type.startsWith("image/")&&(this._data=await g.compress(this._data,this.options.compressImage)),this.options.encryptionKey&&(this._data=await d.encrypt(this._data,this.options.encryptionKey)),this._totalBytes=this._data.size,this._state!=="processing")return;this._state="running",this._notifyObservers();let t=50*1024*1024;this._isResumable=this._totalBytes>=t,this._isResumable?await this._startResumable():await this._startSimple()}catch(t){this._handleError(t)}}async _startSimple(){let t=await this.client.post("/files/upload-url",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});if(this._simpleFileMetadata=t.file,this._state!=="running")return;let e=new XMLHttpRequest;this._activeXhr=e,await new Promise((s,r)=>{e.open("PUT",t.uploadUrl),e.setRequestHeader("Content-Type",this.contentType),e.upload.onprogress=i=>{i.lengthComputable&&this._state==="running"&&(this._bytesTransferred=i.loaded,this._notifyObservers())},e.onload=()=>{e.status>=200&&e.status<300?s():r(new Error(`Upload failed: ${e.status} ${e.responseText}`))},e.onerror=()=>r(new Error("Network error during upload")),e.onabort=()=>r(new Error("Upload aborted")),e.send(this._data)}),this._activeXhr=void 0,this._state==="running"&&(await this.client.post("/files/complete-upload",{path:this.path}),this._bytesTransferred=this._totalBytes,this._state="success",this._notifyObservers(),this._resolve({file:t.file,resumable:!1}))}async _startResumable(){let t=await this.client.post("/files/resumable/init",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});this._uploadId=t.uploadId,this._simpleFileMetadata=t.file,await this._continueResumable()}async _continueResumable(){if(!this._uploadId||!this._simpleFileMetadata)return;let t=this.options.chunkSize??10*1024*1024,e=Math.ceil(this._totalBytes/t);try{for(;this._currentPartIndex<e;){if(this._state!=="running")return;let s=this._currentPartIndex*t,r=Math.min(s+t,this._totalBytes),i=this._data.slice(s,r),a=await this.client.post("/files/resumable/part-url",{path:this.path,uploadId:this._uploadId,partNumber:this._currentPartIndex+1});if(this._state!=="running")return;let n=await this._uploadChunkWithProgress(a.uploadUrl,i,s);this._parts.push({PartNumber:this._currentPartIndex+1,ETag:n}),this._currentPartIndex++,this._bytesTransferred=Math.min(this._currentPartIndex*t,this._totalBytes),this._notifyObservers()}this._state==="running"&&(await this.client.post("/files/resumable/complete",{path:this.path,uploadId:this._uploadId,parts:this._parts}),this._state="success",this._notifyObservers(),this._resolve({file:this._simpleFileMetadata,resumable:!0}))}catch(s){if(s.message==="Upload aborted")return;this._handleError(s)}}_uploadChunkWithProgress(t,e,s){return new Promise((r,i)=>{let a=new XMLHttpRequest;this._activeXhr=a,a.open("PUT",t),a.setRequestHeader("Content-Type",this.contentType),a.upload.onprogress=n=>{n.lengthComputable&&this._state==="running"&&(this._bytesTransferred=s+n.loaded,this._notifyObservers())},a.onload=()=>{if(a.status>=200&&a.status<300){let n=a.getResponseHeader("ETag")||"";r(n.replace(/"/g,""))}else i(new Error(`Part upload failed: ${a.status}`))},a.onerror=()=>i(new Error("Network error during part upload")),a.onabort=()=>i(new Error("Upload aborted")),a.send(e)})}_notifyObservers(){let t=this.snapshot;this._observers.forEach(e=>{t.state==="success"&&e.complete?e.complete():e.next&&e.next(t)})}_handleError=t=>{this._state!=="canceled"&&(this._state="error",this._activeXhr=void 0,this._notifyObservers(),this._observers.forEach(e=>e.error?.(t)),this._reject(t))}};var w=class{constructor(t,e,s){this.client=t;this.path=e;this.tenantId=s}child(t){let s=(this.path?this.path.endsWith("/")?this.path:this.path+"/":"")+t.replace(/^\//,"");return s.endsWith("/")?new y(this.client,s,this.tenantId):new f(this.client,s,this.tenantId)}},y=class extends w{async listAll(t=100){return(await this.client.get("/files",{prefix:this.path,limit:String(t)})).files}},f=class extends w{put(t,e){let s=(t instanceof File,t.size),r=t instanceof File?t.name:this.path.split("/").pop()||"file",i=t.type||"application/octet-stream";return new m(this.client,this.path,t,r,i,e)}putBytes(t,e,s){let r=this.path.split("/").pop()||"file",i=new Blob([t],{type:e});return new m(this.client,this.path,i,r,e,s)}async getDownloadUrl(t){return t?.versionId?(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t.versionId})).downloadUrl:(await this.client.get(`/files/download-url/${encodeURIComponent(this.path)}`)).downloadUrl}async getDecryptedBlob(t){let e=await this.getDownloadUrl(),s=await fetch(e);if(!s.ok)throw new Error(`Failed to download file from S3: ${s.status}`);let r=await s.blob(),i=await this.getMetadata();return await d.decrypt(r,t,i.content_type)}async getMetadata(){return(await this.client.get(`/files/metadata/${encodeURIComponent(this.path)}`)).file}async updateMetadata(t){return(await this.client.patch(`/files/metadata/${encodeURIComponent(this.path)}`,{metadata:t})).file}async delete(){await this.client.delete(`/files/${encodeURIComponent(this.path)}`)}async listVersions(){return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`)}async getVersionUrl(t){return(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t})).downloadUrl}async getTags(){return(await this.client.get(`/files/tags/${encodeURIComponent(this.path)}`)).tags}async setTags(t){await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`,{tags:t})}async setLegalHold(t){await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`,{status:t})}};var b=class{constructor(t){this.client=t}_op=null;_paths=[];_dest="";delete(t){return this._op="delete",this._paths=t,this}copy(t,e){return this._op="copy",this._paths=t,this._dest=e,this}move(t,e){return this._op="move",this._paths=t,this._dest=e,this}async run(){if(!this._op)throw new Error("No batch operation specified");if(this._op==="delete")return this.client.post("/files/batch/delete",{paths:this._paths});let t={sourcePaths:this._paths,destinationPrefix:this._dest};return this.client.post(`/files/batch/${this._op}`,t)}};var v=class{constructor(t){this.client=t}_query="";_metadata={};_limit=20;nameContains(t){return this._query=t,this}where(t,e){return this._metadata[t]=e,this}limit(t){return this._limit=t,this}async get(){let t={q:this._query||void 0,limit:String(this._limit),metadata:Object.keys(this._metadata).length>0?JSON.stringify(this._metadata):void 0};return(await this.client.get("/files/search",t)).files}};var T=class{constructor(t){this.config=t;this.http=new _(t),this.tenantId=t.tenantId}http;tenantId;ref(t){if(t.endsWith("/"))throw new Error("Use .dir() for directory references");return new f(this.http,t,this.tenantId)}dir(t){let e=t.endsWith("/")?t:t+"/";return new y(this.http,e,this.tenantId)}async list(t){let e={prefix:t?.prefix||"",limit:String(t?.limit||100)};return(await this.http.get("/files",e)).files}async rename(t,e){return this.http.post("/files/rename",{oldPath:t,newName:e})}batch(){return new b(this.http)}query(){return new v(this.http)}async generateApiKey(t){return this.http.post("/internal/keys/generate",{name:t})}async revokeApiKey(t){await this.http.post("/internal/keys/revoke",{keyId:t})}async getBucketInfo(){return this.http.get("/internal/bucket-info")}};0&&(module.exports={BatchBuilder,CryptoHelper,DirRef,FileRef,HttpClient,ImageHelper,QueryBuilder,StorageRef,TelestackError,TelestackStorage,UploadTask});
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var _=class{baseUrl;tenantId;headers;constructor(t){this.baseUrl=t.baseUrl.replace(/\/$/,""),this.tenantId=t.tenantId,this.headers={"Content-Type":"application/json","X-Tenant-ID":t.tenantId},t.apiKey?this.headers["X-API-Key"]=t.apiKey:t.token&&(this.headers.Authorization=`Bearer ${t.token}`)}async _fetchWithRetry(t,e,s=3){for(let r=0;r<=s;r++)try{let i=await fetch(t,e);if((i.status===429||i.status>=500)&&r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network error ${i.status}. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}return i}catch(i){if(r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}throw i}throw new Error("Unreachable code in retry logic")}async get(t,e){let s=new URL(`${this.baseUrl}${t}`);e&&Object.entries(e).forEach(([i,a])=>{a!==void 0&&s.searchParams.set(i,a)});let r=await this._fetchWithRetry(s.toString(),{headers:this.headers});return this._handle(r)}async post(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"POST",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async patch(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PATCH",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async put(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PUT",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async delete(t){let e=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"DELETE",headers:this.headers});return this._handle(e)}async uploadToPresignedUrl(t,e,s,r){for(let a=0;a<=3;a++)try{await new Promise((n,l)=>{let o=new XMLHttpRequest;o.open("PUT",t),o.setRequestHeader("Content-Type",s),r&&(o.upload.onprogress=h=>{h.lengthComputable&&r(Math.round(h.loaded/h.total*100))}),o.onload=()=>{o.status>=200&&o.status<300?n():o.status===429||o.status>=500?l(new Error(`Retryable network error: ${o.status}`)):l(new u(`Upload failed: ${o.status}`,o.status))},o.onerror=()=>l(new Error("Retryable network error")),o.send(e)});return}catch(n){if(n instanceof u||a>=3)throw n;let l=Math.min(1e3*Math.pow(2,a),1e4);console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${l}ms...`),await new Promise(o=>setTimeout(o,l))}}async _handle(t){let e=await t.json();if(!t.ok)throw new u(e.error||t.statusText,t.status,e.code);return e}},u=class extends Error{constructor(e,s,r){super(e);this.status=s;this.code=r;this.name="TelestackError"}};var c=class{static async generateKey(){let t=await crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),e=await crypto.subtle.exportKey("raw",t);return btoa(String.fromCharCode(...new Uint8Array(e)))}static async encrypt(t,e){let s=await this._importKey(e),r=crypto.getRandomValues(new Uint8Array(12)),i=await t.arrayBuffer(),a=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},s,i);return new Blob([r,a],{type:"application/octet-stream"})}static async decrypt(t,e,s="application/octet-stream"){let r=await this._importKey(e),i=await t.arrayBuffer(),a=i.slice(0,12),n=i.slice(12),l=await crypto.subtle.decrypt({name:"AES-GCM",iv:new Uint8Array(a)},r,n);return new Blob([l],{type:s})}static async _importKey(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0));return crypto.subtle.importKey("raw",e,{name:"AES-GCM"},!1,["encrypt","decrypt"])}};var g=class{static async compress(t,e={}){if(!t.type.startsWith("image/"))return t;let{maxWidth:s=1920,maxHeight:r=1080,quality:i=.8}=e;return new Promise((a,n)=>{let l=new Image,o=URL.createObjectURL(t);l.onload=()=>{URL.revokeObjectURL(o);let h=l.width,d=l.height;h>s&&(d=Math.round(d*s/h),h=s),d>r&&(h=Math.round(h*r/d),d=r);let R=document.createElement("canvas");R.width=h,R.height=d;let U=R.getContext("2d");if(!U)return n(new Error("Failed to get canvas 2d context for image compression"));U.drawImage(l,0,0,h,d);let I=t.type==="image/png"?"image/png":"image/webp";R.toBlob(P=>{P?a(P):n(new Error("Canvas toBlob failed"))},I,i)},l.onerror=()=>{URL.revokeObjectURL(o),n(new Error("Failed to load image for compression"))},l.src=o})}};var m=class{constructor(t,e,s,r,i,a){this.client=t;this.path=e;this.name=r;this.contentType=i;this.options=a;this._data=s,this._totalBytes=s.size,this._promise=new Promise((n,l)=>{this._resolve=n,this._reject=l}),this._start()}_state="processing";_bytesTransferred=0;_totalBytes=0;_data;_observers=[];_promise;_resolve;_reject;_uploadId;_parts=[];_currentPartIndex=0;_activeXhr;_isResumable=!1;_simpleFileMetadata;on(t,e,s,r){let i=typeof e=="function"?{next:e,error:s,complete:r}:e||{error:s,complete:r};return this._observers.push(i),i.next&&i.next(this.snapshot),()=>{this._observers=this._observers.filter(a=>a!==i)}}pause(){return this._state!=="running"?!1:(this._state="paused",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._notifyObservers(),!0)}resume(){return this._state!=="paused"?!1:(this._state="running",this._notifyObservers(),this._isResumable?this._continueResumable().catch(this._handleError):(this._bytesTransferred=0,this._notifyObservers(),this._startSimple().catch(this._handleError)),!0)}cancel(){if(this._state==="success"||this._state==="error"||this._state==="canceled")return!1;this._state="canceled",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._isResumable&&this._uploadId?this.client.post("/files/resumable/abort",{path:this.path,uploadId:this._uploadId}).catch(()=>{}):!this._isResumable&&this._simpleFileMetadata&&this.client.delete(`/files/${encodeURIComponent(this.path)}`).catch(()=>{}),this._notifyObservers();let t=new Error("Upload canceled by user");return t.name="UploadCanceled",this._handleError(t),!0}get snapshot(){return{bytesTransferred:this._bytesTransferred,totalBytes:this._totalBytes,state:this._state,task:this}}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}[Symbol.toStringTag]="UploadTask";async _start(){try{if(this.options.compressImage&&this._data.type.startsWith("image/")&&(this._data=await g.compress(this._data,this.options.compressImage)),this.options.encryptionKey&&(this._data=await c.encrypt(this._data,this.options.encryptionKey)),this._totalBytes=this._data.size,this._state!=="processing")return;this._state="running",this._notifyObservers();let t=50*1024*1024;this._isResumable=this._totalBytes>=t,this._isResumable?await this._startResumable():await this._startSimple()}catch(t){this._handleError(t)}}async _startSimple(){let t=await this.client.post("/files/upload-url",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});if(this._simpleFileMetadata=t.file,this._state!=="running")return;let e=new XMLHttpRequest;this._activeXhr=e,await new Promise((s,r)=>{e.open("PUT",t.uploadUrl),e.setRequestHeader("Content-Type",this.contentType),e.upload.onprogress=i=>{i.lengthComputable&&this._state==="running"&&(this._bytesTransferred=i.loaded,this._notifyObservers())},e.onload=()=>{e.status>=200&&e.status<300?s():r(new Error(`Upload failed: ${e.status} ${e.responseText}`))},e.onerror=()=>r(new Error("Network error during upload")),e.onabort=()=>r(new Error("Upload aborted")),e.send(this._data)}),this._activeXhr=void 0,this._state==="running"&&(await this.client.post("/files/complete-upload",{path:this.path}),this._bytesTransferred=this._totalBytes,this._state="success",this._notifyObservers(),this._resolve({file:t.file,resumable:!1}))}async _startResumable(){let t=await this.client.post("/files/resumable/init",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});this._uploadId=t.uploadId,this._simpleFileMetadata=t.file,await this._continueResumable()}async _continueResumable(){if(!this._uploadId||!this._simpleFileMetadata)return;let t=this.options.chunkSize??10*1024*1024,e=Math.ceil(this._totalBytes/t);try{for(;this._currentPartIndex<e;){if(this._state!=="running")return;let s=this._currentPartIndex*t,r=Math.min(s+t,this._totalBytes),i=this._data.slice(s,r),a=await this.client.post("/files/resumable/part-url",{path:this.path,uploadId:this._uploadId,partNumber:this._currentPartIndex+1});if(this._state!=="running")return;let n=await this._uploadChunkWithProgress(a.uploadUrl,i,s);this._parts.push({PartNumber:this._currentPartIndex+1,ETag:n}),this._currentPartIndex++,this._bytesTransferred=Math.min(this._currentPartIndex*t,this._totalBytes),this._notifyObservers()}this._state==="running"&&(await this.client.post("/files/resumable/complete",{path:this.path,uploadId:this._uploadId,parts:this._parts}),this._state="success",this._notifyObservers(),this._resolve({file:this._simpleFileMetadata,resumable:!0}))}catch(s){if(s.message==="Upload aborted")return;this._handleError(s)}}_uploadChunkWithProgress(t,e,s){return new Promise((r,i)=>{let a=new XMLHttpRequest;this._activeXhr=a,a.open("PUT",t),a.setRequestHeader("Content-Type",this.contentType),a.upload.onprogress=n=>{n.lengthComputable&&this._state==="running"&&(this._bytesTransferred=s+n.loaded,this._notifyObservers())},a.onload=()=>{if(a.status>=200&&a.status<300){let n=a.getResponseHeader("ETag")||"";r(n.replace(/"/g,""))}else i(new Error(`Part upload failed: ${a.status}`))},a.onerror=()=>i(new Error("Network error during part upload")),a.onabort=()=>i(new Error("Upload aborted")),a.send(e)})}_notifyObservers(){let t=this.snapshot;this._observers.forEach(e=>{t.state==="success"&&e.complete?e.complete():e.next&&e.next(t)})}_handleError=t=>{this._state!=="canceled"&&(this._state="error",this._activeXhr=void 0,this._notifyObservers(),this._observers.forEach(e=>e.error?.(t)),this._reject(t))}};var w=class{constructor(t,e,s){this.client=t;this.path=e;this.tenantId=s}child(t){let s=(this.path?this.path.endsWith("/")?this.path:this.path+"/":"")+t.replace(/^\//,"");return s.endsWith("/")?new y(this.client,s,this.tenantId):new f(this.client,s,this.tenantId)}},y=class extends w{async listAll(t=100){return(await this.client.get("/files",{prefix:this.path,limit:String(t)})).files}},f=class extends w{put(t,e){let s=(t instanceof File,t.size),r=t instanceof File?t.name:this.path.split("/").pop()||"file",i=t.type||"application/octet-stream";return new m(this.client,this.path,t,r,i,e)}putBytes(t,e,s){let r=this.path.split("/").pop()||"file",i=new Blob([t],{type:e});return new m(this.client,this.path,i,r,e,s)}async getDownloadUrl(t){return t?.versionId?(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t.versionId})).downloadUrl:(await this.client.get(`/files/download-url/${encodeURIComponent(this.path)}`)).downloadUrl}async getDecryptedBlob(t){let e=await this.getDownloadUrl(),s=await fetch(e);if(!s.ok)throw new Error(`Failed to download file from S3: ${s.status}`);let r=await s.blob(),i=await this.getMetadata();return await c.decrypt(r,t,i.content_type)}async getMetadata(){return(await this.client.get(`/files/metadata/${encodeURIComponent(this.path)}`)).file}async updateMetadata(t){return(await this.client.patch(`/files/metadata/${encodeURIComponent(this.path)}`,{metadata:t})).file}async delete(){await this.client.delete(`/files/${encodeURIComponent(this.path)}`)}async listVersions(){return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`)}async getVersionUrl(t){return(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t})).downloadUrl}async getTags(){return(await this.client.get(`/files/tags/${encodeURIComponent(this.path)}`)).tags}async setTags(t){await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`,{tags:t})}async setLegalHold(t){await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`,{status:t})}};var b=class{constructor(t){this.client=t}_op=null;_paths=[];_dest="";delete(t){return this._op="delete",this._paths=t,this}copy(t,e){return this._op="copy",this._paths=t,this._dest=e,this}move(t,e){return this._op="move",this._paths=t,this._dest=e,this}async run(){if(!this._op)throw new Error("No batch operation specified");if(this._op==="delete")return this.client.post("/files/batch/delete",{paths:this._paths});let t={sourcePaths:this._paths,destinationPrefix:this._dest};return this.client.post(`/files/batch/${this._op}`,t)}};var v=class{constructor(t){this.client=t}_query="";_metadata={};_limit=20;nameContains(t){return this._query=t,this}where(t,e){return this._metadata[t]=e,this}limit(t){return this._limit=t,this}async get(){let t={q:this._query||void 0,limit:String(this._limit),metadata:Object.keys(this._metadata).length>0?JSON.stringify(this._metadata):void 0};return(await this.client.get("/files/search",t)).files}};var T=class{constructor(t){this.config=t;this.http=new _(t),this.tenantId=t.tenantId}http;tenantId;ref(t){if(t.endsWith("/"))throw new Error("Use .dir() for directory references");return new f(this.http,t,this.tenantId)}dir(t){let e=t.endsWith("/")?t:t+"/";return new y(this.http,e,this.tenantId)}async list(t){let e={prefix:t?.prefix,limit:t?.limit!==void 0?String(t.limit):void 0};return(await this.http.get("/files",e)).files}batch(){return new b(this.http)}query(){return new v(this.http)}async generateApiKey(t){return this.http.post("/internal/keys/generate",{name:t})}async revokeApiKey(t){await this.http.post("/internal/keys/revoke",{keyId:t})}async getBucketInfo(){return this.http.get("/internal/bucket-info")}};export{b as BatchBuilder,c as CryptoHelper,y as DirRef,f as FileRef,_ as HttpClient,g as ImageHelper,v as QueryBuilder,w as StorageRef,u as TelestackError,T as TelestackStorage,m as UploadTask};
|
|
1
|
+
var _=class{baseUrl;tenantId;headers;constructor(t){this.baseUrl=t.baseUrl.replace(/\/$/,""),this.tenantId=t.tenantId,this.headers={"Content-Type":"application/json","X-Tenant-ID":t.tenantId},t.apiKey?this.headers["X-API-Key"]=t.apiKey:t.token&&(this.headers.Authorization=`Bearer ${t.token}`)}async _fetchWithRetry(t,e,s=3){for(let r=0;r<=s;r++)try{let i=await fetch(t,e);if((i.status===429||i.status>=500)&&r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network error ${i.status}. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}return i}catch(i){if(r<s){let a=Math.min(1e3*Math.pow(2,r),1e4);console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${a}ms...`),await new Promise(n=>setTimeout(n,a));continue}throw i}throw new Error("Unreachable code in retry logic")}async get(t,e){let s=new URL(`${this.baseUrl}${t}`);e&&Object.entries(e).forEach(([i,a])=>{a!==void 0&&s.searchParams.set(i,a)});let r=await this._fetchWithRetry(s.toString(),{headers:this.headers});return this._handle(r)}async post(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"POST",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async patch(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PATCH",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async put(t,e){let s=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"PUT",headers:this.headers,body:JSON.stringify(e)});return this._handle(s)}async delete(t){let e=await this._fetchWithRetry(`${this.baseUrl}${t}`,{method:"DELETE",headers:this.headers});return this._handle(e)}async uploadToPresignedUrl(t,e,s,r){for(let a=0;a<=3;a++)try{await new Promise((n,l)=>{let o=new XMLHttpRequest;o.open("PUT",t),o.setRequestHeader("Content-Type",s),r&&(o.upload.onprogress=h=>{h.lengthComputable&&r(Math.round(h.loaded/h.total*100))}),o.onload=()=>{o.status>=200&&o.status<300?n():o.status===429||o.status>=500?l(new Error(`Retryable network error: ${o.status}`)):l(new u(`Upload failed: ${o.status}`,o.status))},o.onerror=()=>l(new Error("Retryable network error")),o.send(e)});return}catch(n){if(n instanceof u||a>=3)throw n;let l=Math.min(1e3*Math.pow(2,a),1e4);console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${l}ms...`),await new Promise(o=>setTimeout(o,l))}}async _handle(t){let e=await t.json();if(!t.ok)throw new u(e.error||t.statusText,t.status,e.code);return e}},u=class extends Error{constructor(e,s,r){super(e);this.status=s;this.code=r;this.name="TelestackError"}};var c=class{static async generateKey(){let t=await crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),e=await crypto.subtle.exportKey("raw",t);return btoa(String.fromCharCode(...new Uint8Array(e)))}static async encrypt(t,e){let s=await this._importKey(e),r=crypto.getRandomValues(new Uint8Array(12)),i=await t.arrayBuffer(),a=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},s,i);return new Blob([r,a],{type:"application/octet-stream"})}static async decrypt(t,e,s="application/octet-stream"){let r=await this._importKey(e),i=await t.arrayBuffer(),a=i.slice(0,12),n=i.slice(12),l=await crypto.subtle.decrypt({name:"AES-GCM",iv:new Uint8Array(a)},r,n);return new Blob([l],{type:s})}static async _importKey(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0));return crypto.subtle.importKey("raw",e,{name:"AES-GCM"},!1,["encrypt","decrypt"])}};var g=class{static async compress(t,e={}){if(!t.type.startsWith("image/"))return t;let{maxWidth:s=1920,maxHeight:r=1080,quality:i=.8}=e;return new Promise((a,n)=>{let l=new Image,o=URL.createObjectURL(t);l.onload=()=>{URL.revokeObjectURL(o);let h=l.width,d=l.height;h>s&&(d=Math.round(d*s/h),h=s),d>r&&(h=Math.round(h*r/d),d=r);let R=document.createElement("canvas");R.width=h,R.height=d;let U=R.getContext("2d");if(!U)return n(new Error("Failed to get canvas 2d context for image compression"));U.drawImage(l,0,0,h,d);let I=t.type==="image/png"?"image/png":"image/webp";R.toBlob(P=>{P?a(P):n(new Error("Canvas toBlob failed"))},I,i)},l.onerror=()=>{URL.revokeObjectURL(o),n(new Error("Failed to load image for compression"))},l.src=o})}};var m=class{constructor(t,e,s,r,i,a){this.client=t;this.path=e;this.name=r;this.contentType=i;this.options=a;this._data=s,this._totalBytes=s.size,this._promise=new Promise((n,l)=>{this._resolve=n,this._reject=l}),this._start()}_state="processing";_bytesTransferred=0;_totalBytes=0;_data;_observers=[];_promise;_resolve;_reject;_uploadId;_parts=[];_currentPartIndex=0;_activeXhr;_isResumable=!1;_simpleFileMetadata;on(t,e,s,r){let i=typeof e=="function"?{next:e,error:s,complete:r}:e||{error:s,complete:r};return this._observers.push(i),i.next&&i.next(this.snapshot),()=>{this._observers=this._observers.filter(a=>a!==i)}}pause(){return this._state!=="running"?!1:(this._state="paused",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._notifyObservers(),!0)}resume(){return this._state!=="paused"?!1:(this._state="running",this._notifyObservers(),this._isResumable?this._continueResumable().catch(this._handleError):(this._bytesTransferred=0,this._notifyObservers(),this._startSimple().catch(this._handleError)),!0)}cancel(){if(this._state==="success"||this._state==="error"||this._state==="canceled")return!1;this._state="canceled",this._activeXhr&&(this._activeXhr.abort(),this._activeXhr=void 0),this._isResumable&&this._uploadId?this.client.post("/files/resumable/abort",{path:this.path,uploadId:this._uploadId}).catch(()=>{}):!this._isResumable&&this._simpleFileMetadata&&this.client.delete(`/files/${encodeURIComponent(this.path)}`).catch(()=>{}),this._notifyObservers();let t=new Error("Upload canceled by user");return t.name="UploadCanceled",this._handleError(t),!0}get snapshot(){return{bytesTransferred:this._bytesTransferred,totalBytes:this._totalBytes,state:this._state,task:this}}then(t,e){return this._promise.then(t,e)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}[Symbol.toStringTag]="UploadTask";async _start(){try{if(this.options.compressImage&&this._data.type.startsWith("image/")&&(this._data=await g.compress(this._data,this.options.compressImage)),this.options.encryptionKey&&(this._data=await c.encrypt(this._data,this.options.encryptionKey)),this._totalBytes=this._data.size,this._state!=="processing")return;this._state="running",this._notifyObservers();let t=50*1024*1024;this._isResumable=this._totalBytes>=t,this._isResumable?await this._startResumable():await this._startSimple()}catch(t){this._handleError(t)}}async _startSimple(){let t=await this.client.post("/files/upload-url",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});if(this._simpleFileMetadata=t.file,this._state!=="running")return;let e=new XMLHttpRequest;this._activeXhr=e,await new Promise((s,r)=>{e.open("PUT",t.uploadUrl),e.setRequestHeader("Content-Type",this.contentType),e.upload.onprogress=i=>{i.lengthComputable&&this._state==="running"&&(this._bytesTransferred=i.loaded,this._notifyObservers())},e.onload=()=>{e.status>=200&&e.status<300?s():r(new Error(`Upload failed: ${e.status} ${e.responseText}`))},e.onerror=()=>r(new Error("Network error during upload")),e.onabort=()=>r(new Error("Upload aborted")),e.send(this._data)}),this._activeXhr=void 0,this._state==="running"&&(await this.client.post("/files/complete-upload",{path:this.path}),this._bytesTransferred=this._totalBytes,this._state="success",this._notifyObservers(),this._resolve({file:t.file,resumable:!1}))}async _startResumable(){let t=await this.client.post("/files/resumable/init",{path:this.path,name:this.name,size:this._totalBytes,contentType:this.contentType,userId:this.options.userId,metadata:this.options.metadata});this._uploadId=t.uploadId,this._simpleFileMetadata=t.file,await this._continueResumable()}async _continueResumable(){if(!this._uploadId||!this._simpleFileMetadata)return;let t=this.options.chunkSize??10*1024*1024,e=Math.ceil(this._totalBytes/t);try{for(;this._currentPartIndex<e;){if(this._state!=="running")return;let s=this._currentPartIndex*t,r=Math.min(s+t,this._totalBytes),i=this._data.slice(s,r),a=await this.client.post("/files/resumable/part-url",{path:this.path,uploadId:this._uploadId,partNumber:this._currentPartIndex+1});if(this._state!=="running")return;let n=await this._uploadChunkWithProgress(a.uploadUrl,i,s);this._parts.push({PartNumber:this._currentPartIndex+1,ETag:n}),this._currentPartIndex++,this._bytesTransferred=Math.min(this._currentPartIndex*t,this._totalBytes),this._notifyObservers()}this._state==="running"&&(await this.client.post("/files/resumable/complete",{path:this.path,uploadId:this._uploadId,parts:this._parts}),this._state="success",this._notifyObservers(),this._resolve({file:this._simpleFileMetadata,resumable:!0}))}catch(s){if(s.message==="Upload aborted")return;this._handleError(s)}}_uploadChunkWithProgress(t,e,s){return new Promise((r,i)=>{let a=new XMLHttpRequest;this._activeXhr=a,a.open("PUT",t),a.setRequestHeader("Content-Type",this.contentType),a.upload.onprogress=n=>{n.lengthComputable&&this._state==="running"&&(this._bytesTransferred=s+n.loaded,this._notifyObservers())},a.onload=()=>{if(a.status>=200&&a.status<300){let n=a.getResponseHeader("ETag")||"";r(n.replace(/"/g,""))}else i(new Error(`Part upload failed: ${a.status}`))},a.onerror=()=>i(new Error("Network error during part upload")),a.onabort=()=>i(new Error("Upload aborted")),a.send(e)})}_notifyObservers(){let t=this.snapshot;this._observers.forEach(e=>{t.state==="success"&&e.complete?e.complete():e.next&&e.next(t)})}_handleError=t=>{this._state!=="canceled"&&(this._state="error",this._activeXhr=void 0,this._notifyObservers(),this._observers.forEach(e=>e.error?.(t)),this._reject(t))}};var w=class{constructor(t,e,s){this.client=t;this.path=e;this.tenantId=s}child(t){let s=(this.path?this.path.endsWith("/")?this.path:this.path+"/":"")+t.replace(/^\//,"");return s.endsWith("/")?new y(this.client,s,this.tenantId):new f(this.client,s,this.tenantId)}},y=class extends w{async listAll(t=100){return(await this.client.get("/files",{prefix:this.path,limit:String(t)})).files}},f=class extends w{put(t,e){let s=(t instanceof File,t.size),r=t instanceof File?t.name:this.path.split("/").pop()||"file",i=t.type||"application/octet-stream";return new m(this.client,this.path,t,r,i,e)}putBytes(t,e,s){let r=this.path.split("/").pop()||"file",i=new Blob([t],{type:e});return new m(this.client,this.path,i,r,e,s)}async getDownloadUrl(t){return t?.versionId?(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t.versionId})).downloadUrl:(await this.client.get(`/files/download-url/${encodeURIComponent(this.path)}`)).downloadUrl}async getDecryptedBlob(t){let e=await this.getDownloadUrl(),s=await fetch(e);if(!s.ok)throw new Error(`Failed to download file from S3: ${s.status}`);let r=await s.blob(),i=await this.getMetadata();return await c.decrypt(r,t,i.content_type)}async getMetadata(){return(await this.client.get(`/files/metadata/${encodeURIComponent(this.path)}`)).file}async updateMetadata(t){return(await this.client.patch(`/files/metadata/${encodeURIComponent(this.path)}`,{metadata:t})).file}async delete(){await this.client.delete(`/files/${encodeURIComponent(this.path)}`)}async listVersions(){return this.client.get(`/files/versions/${encodeURIComponent(this.path)}`)}async getVersionUrl(t){return(await this.client.get(`/files/version-url/${encodeURIComponent(this.path)}`,{versionId:t})).downloadUrl}async getTags(){return(await this.client.get(`/files/tags/${encodeURIComponent(this.path)}`)).tags}async setTags(t){await this.client.put(`/files/tags/${encodeURIComponent(this.path)}`,{tags:t})}async setLegalHold(t){await this.client.post(`/files/legal-hold/${encodeURIComponent(this.path)}`,{status:t})}};var b=class{constructor(t){this.client=t}_op=null;_paths=[];_dest="";delete(t){return this._op="delete",this._paths=t,this}copy(t,e){return this._op="copy",this._paths=t,this._dest=e,this}move(t,e){return this._op="move",this._paths=t,this._dest=e,this}async run(){if(!this._op)throw new Error("No batch operation specified");if(this._op==="delete")return this.client.post("/files/batch/delete",{paths:this._paths});let t={sourcePaths:this._paths,destinationPrefix:this._dest};return this.client.post(`/files/batch/${this._op}`,t)}};var v=class{constructor(t){this.client=t}_query="";_metadata={};_limit=20;nameContains(t){return this._query=t,this}where(t,e){return this._metadata[t]=e,this}limit(t){return this._limit=t,this}async get(){let t={q:this._query||void 0,limit:String(this._limit),metadata:Object.keys(this._metadata).length>0?JSON.stringify(this._metadata):void 0};return(await this.client.get("/files/search",t)).files}};var T=class{constructor(t){this.config=t;this.http=new _(t),this.tenantId=t.tenantId}http;tenantId;ref(t){if(t.endsWith("/"))throw new Error("Use .dir() for directory references");return new f(this.http,t,this.tenantId)}dir(t){let e=t.endsWith("/")?t:t+"/";return new y(this.http,e,this.tenantId)}async list(t){let e={prefix:t?.prefix||"",limit:String(t?.limit||100)};return(await this.http.get("/files",e)).files}async rename(t,e){return this.http.post("/files/rename",{oldPath:t,newName:e})}batch(){return new b(this.http)}query(){return new v(this.http)}async generateApiKey(t){return this.http.post("/internal/keys/generate",{name:t})}async revokeApiKey(t){await this.http.post("/internal/keys/revoke",{keyId:t})}async getBucketInfo(){return this.http.get("/internal/bucket-info")}};export{b as BatchBuilder,c as CryptoHelper,y as DirRef,f as FileRef,_ as HttpClient,g as ImageHelper,v as QueryBuilder,w as StorageRef,u as TelestackError,T as TelestackStorage,m as UploadTask};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telestack/storage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Official Telestack Storage Web SDK — fluent, Firebase-killer, real-time cloud storage for web apps.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
15
20
|
"scripts": {
|
|
16
21
|
"build": "tsup src/index.ts --format cjs,esm,iife --global-name TelestackStorageSetup --minify --dts --clean"
|
|
17
22
|
},
|
|
@@ -23,6 +28,9 @@
|
|
|
23
28
|
"file-upload"
|
|
24
29
|
],
|
|
25
30
|
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
26
34
|
"devDependencies": {
|
|
27
35
|
"@types/jsdom": "^27.0.0",
|
|
28
36
|
"dotenv": "^17.3.1",
|
|
@@ -34,4 +42,4 @@
|
|
|
34
42
|
"typescript": "^5.0.0",
|
|
35
43
|
"xhr2": "^0.2.1"
|
|
36
44
|
}
|
|
37
|
-
}
|
|
45
|
+
}
|
package/debug-fetch.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
|
-
|
|
3
|
-
async function testUploadUrl() {
|
|
4
|
-
try {
|
|
5
|
-
const res = await fetch('http://localhost:8787/files/upload-url', {
|
|
6
|
-
method: 'POST',
|
|
7
|
-
headers: {
|
|
8
|
-
'Content-Type': 'application/json',
|
|
9
|
-
'X-Tenant-ID': 'test-tenant',
|
|
10
|
-
'Authorization': 'Bearer DEV_TEST_TOKEN'
|
|
11
|
-
},
|
|
12
|
-
body: JSON.stringify({
|
|
13
|
-
path: 'test.txt',
|
|
14
|
-
name: 'test.txt',
|
|
15
|
-
size: 1024,
|
|
16
|
-
contentType: 'text/plain',
|
|
17
|
-
userId: 'user-sdk-test'
|
|
18
|
-
})
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const text = await res.text();
|
|
22
|
-
console.log(`Status: ${res.status}`);
|
|
23
|
-
console.log(`Body: ${text}`);
|
|
24
|
-
} catch (err) {
|
|
25
|
-
console.error('Fetch error:', err);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
testUploadUrl();
|
package/src/BatchBuilder.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { HttpClient } from './HttpClient';
|
|
2
|
-
import { BatchDeleteResult, BatchCopyResult } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fluent builder for batch file operations.
|
|
6
|
-
* Operations execute identically as single network requests over the backend API.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* await storage.batch().delete(['a.pdf', 'b.pdf']).run();
|
|
10
|
-
* await storage.batch().copy(['a.pdf'], 'archive/').run();
|
|
11
|
-
* await storage.batch().move(['a.pdf'], 'archive/').run();
|
|
12
|
-
*/
|
|
13
|
-
export class BatchBuilder {
|
|
14
|
-
private _op: 'delete' | 'copy' | 'move' | null = null;
|
|
15
|
-
private _paths: string[] = [];
|
|
16
|
-
private _dest = '';
|
|
17
|
-
|
|
18
|
-
constructor(private readonly client: HttpClient) { }
|
|
19
|
-
|
|
20
|
-
delete(paths: string[]): this {
|
|
21
|
-
this._op = 'delete';
|
|
22
|
-
this._paths = paths;
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
copy(paths: string[], destinationPrefix: string): this {
|
|
27
|
-
this._op = 'copy';
|
|
28
|
-
this._paths = paths;
|
|
29
|
-
this._dest = destinationPrefix;
|
|
30
|
-
return this;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
move(paths: string[], destinationPrefix: string): this {
|
|
34
|
-
this._op = 'move';
|
|
35
|
-
this._paths = paths;
|
|
36
|
-
this._dest = destinationPrefix;
|
|
37
|
-
return this;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async run(): Promise<BatchDeleteResult | BatchCopyResult> {
|
|
41
|
-
if (!this._op) throw new Error('No batch operation specified');
|
|
42
|
-
if (this._op === 'delete') {
|
|
43
|
-
return this.client.post<BatchDeleteResult>('/files/batch/delete', { paths: this._paths });
|
|
44
|
-
}
|
|
45
|
-
const body = { sourcePaths: this._paths, destinationPrefix: this._dest };
|
|
46
|
-
return this.client.post<BatchCopyResult>(`/files/batch/${this._op}`, body);
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/CryptoHelper.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// TelestackStorage Web SDK — CryptoHelper (E2EE)
|
|
3
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export class CryptoHelper {
|
|
6
|
-
/**
|
|
7
|
-
* Generates a new random AES-GCM 256-bit encryption key.
|
|
8
|
-
* Returns the key as a base64 encoded string.
|
|
9
|
-
*/
|
|
10
|
-
static async generateKey(): Promise<string> {
|
|
11
|
-
const key = await crypto.subtle.generateKey(
|
|
12
|
-
{ name: 'AES-GCM', length: 256 },
|
|
13
|
-
true, // extractable
|
|
14
|
-
['encrypt', 'decrypt']
|
|
15
|
-
);
|
|
16
|
-
const exported = await crypto.subtle.exportKey('raw', key);
|
|
17
|
-
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Encrypts a Blob using AES-GCM.
|
|
22
|
-
* Prepends a random 12-byte IV to the resulting ciphertext Blob.
|
|
23
|
-
*/
|
|
24
|
-
static async encrypt(blob: Blob, keyBase64: string): Promise<Blob> {
|
|
25
|
-
const key = await this._importKey(keyBase64);
|
|
26
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
27
|
-
|
|
28
|
-
const buffer = await blob.arrayBuffer();
|
|
29
|
-
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
30
|
-
{ name: 'AES-GCM', iv },
|
|
31
|
-
key,
|
|
32
|
-
buffer
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
// Prepend the 12-byte IV so we can decrypt it later
|
|
36
|
-
return new Blob([iv, encryptedBuffer], { type: 'application/octet-stream' });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Decrypts a Blob that was encrypted with `encrypt()`.
|
|
41
|
-
* Extracts the 12-byte IV from the front and decrypts the rest.
|
|
42
|
-
* Restores the original MIME type.
|
|
43
|
-
*/
|
|
44
|
-
static async decrypt(blob: Blob, keyBase64: string, originalMimeType: string = 'application/octet-stream'): Promise<Blob> {
|
|
45
|
-
const key = await this._importKey(keyBase64);
|
|
46
|
-
|
|
47
|
-
const buffer = await blob.arrayBuffer();
|
|
48
|
-
|
|
49
|
-
// Extract the 12-byte IV from the prefix
|
|
50
|
-
const iv = buffer.slice(0, 12);
|
|
51
|
-
const ciphertext = buffer.slice(12);
|
|
52
|
-
|
|
53
|
-
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
54
|
-
{ name: 'AES-GCM', iv: new Uint8Array(iv) },
|
|
55
|
-
key,
|
|
56
|
-
ciphertext
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
return new Blob([decryptedBuffer], { type: originalMimeType });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private static async _importKey(keyBase64: string): Promise<CryptoKey> {
|
|
63
|
-
const keyBuffer = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
|
|
64
|
-
return crypto.subtle.importKey(
|
|
65
|
-
'raw',
|
|
66
|
-
keyBuffer,
|
|
67
|
-
{ name: 'AES-GCM' },
|
|
68
|
-
false, // no need to extract it again
|
|
69
|
-
['encrypt', 'decrypt']
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/HttpClient.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// TelestackStorage Web SDK — HTTP Client
|
|
3
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
import type { TelestackConfig } from './types';
|
|
6
|
-
|
|
7
|
-
export class HttpClient {
|
|
8
|
-
private baseUrl: string;
|
|
9
|
-
private tenantId: string;
|
|
10
|
-
private headers: Record<string, string>;
|
|
11
|
-
|
|
12
|
-
constructor(config: TelestackConfig) {
|
|
13
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
14
|
-
this.tenantId = config.tenantId;
|
|
15
|
-
this.headers = {
|
|
16
|
-
'Content-Type': 'application/json',
|
|
17
|
-
'X-Tenant-ID': config.tenantId,
|
|
18
|
-
};
|
|
19
|
-
if (config.apiKey) {
|
|
20
|
-
this.headers['X-API-Key'] = config.apiKey;
|
|
21
|
-
} else if (config.token) {
|
|
22
|
-
this.headers['Authorization'] = `Bearer ${config.token}`;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private async _fetchWithRetry(url: string | URL, options: RequestInit, retries: number = 3): Promise<Response> {
|
|
27
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(url, options);
|
|
30
|
-
// Retry on rate limits (429) or server errors (5xx)
|
|
31
|
-
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
|
|
32
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
33
|
-
console.warn(`[TelestackStorage] Network error ${res.status}. Retrying in ${delay}ms...`);
|
|
34
|
-
await new Promise(r => setTimeout(r, delay));
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
return res;
|
|
38
|
-
} catch (err: any) {
|
|
39
|
-
// Network failures (e.g. CORS, offline)
|
|
40
|
-
if (attempt < retries) {
|
|
41
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
42
|
-
console.warn(`[TelestackStorage] Network un-reachable. Retrying in ${delay}ms...`);
|
|
43
|
-
await new Promise(r => setTimeout(r, delay));
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
throw new Error('Unreachable code in retry logic');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async get<T>(path: string, params?: Record<string, string | undefined>): Promise<T> {
|
|
53
|
-
const url = new URL(`${this.baseUrl}${path}`);
|
|
54
|
-
if (params) {
|
|
55
|
-
Object.entries(params).forEach(([k, v]) => {
|
|
56
|
-
if (v !== undefined) url.searchParams.set(k, v);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const res = await this._fetchWithRetry(url.toString(), { headers: this.headers });
|
|
60
|
-
return this._handle<T>(res);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
64
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
headers: this.headers,
|
|
67
|
-
body: JSON.stringify(body),
|
|
68
|
-
});
|
|
69
|
-
return this._handle<T>(res);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async patch<T>(path: string, body?: unknown): Promise<T> {
|
|
73
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
74
|
-
method: 'PATCH',
|
|
75
|
-
headers: this.headers,
|
|
76
|
-
body: JSON.stringify(body),
|
|
77
|
-
});
|
|
78
|
-
return this._handle<T>(res);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
82
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
83
|
-
method: 'PUT',
|
|
84
|
-
headers: this.headers,
|
|
85
|
-
body: JSON.stringify(body),
|
|
86
|
-
});
|
|
87
|
-
return this._handle<T>(res);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async delete<T>(path: string): Promise<T> {
|
|
91
|
-
const res = await this._fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
92
|
-
method: 'DELETE',
|
|
93
|
-
headers: this.headers,
|
|
94
|
-
});
|
|
95
|
-
return this._handle<T>(res);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Upload a file binary buffer directly to a presigned S3 URL. */
|
|
99
|
-
async uploadToPresignedUrl(url: string, data: Blob | ArrayBuffer, contentType: string, onProgress?: (p: number) => void): Promise<void> {
|
|
100
|
-
const maxRetries = 3;
|
|
101
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
102
|
-
try {
|
|
103
|
-
await new Promise<void>((resolve, reject) => {
|
|
104
|
-
const xhr = new XMLHttpRequest();
|
|
105
|
-
xhr.open('PUT', url);
|
|
106
|
-
xhr.setRequestHeader('Content-Type', contentType);
|
|
107
|
-
if (onProgress) {
|
|
108
|
-
xhr.upload.onprogress = (e) => {
|
|
109
|
-
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
xhr.onload = () => {
|
|
113
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
114
|
-
resolve();
|
|
115
|
-
} else if (xhr.status === 429 || xhr.status >= 500) {
|
|
116
|
-
reject(new Error(`Retryable network error: ${xhr.status}`));
|
|
117
|
-
} else {
|
|
118
|
-
reject(new TelestackError(`Upload failed: ${xhr.status}`, xhr.status));
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
xhr.onerror = () => reject(new Error('Retryable network error'));
|
|
122
|
-
xhr.send(data);
|
|
123
|
-
});
|
|
124
|
-
return; // Success, exit retry loop
|
|
125
|
-
} catch (err: any) {
|
|
126
|
-
if (err instanceof TelestackError || attempt >= maxRetries) {
|
|
127
|
-
throw err; // Fatal error or out of retries
|
|
128
|
-
}
|
|
129
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
130
|
-
console.warn(`[TelestackStorage] Upload chunk failed. Retrying in ${delay}ms...`);
|
|
131
|
-
await new Promise(r => setTimeout(r, delay));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private async _handle<T>(res: Response): Promise<T> {
|
|
137
|
-
const json = await res.json() as any;
|
|
138
|
-
if (!res.ok) throw new TelestackError(json.error || res.statusText, res.status, json.code);
|
|
139
|
-
return json as T;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export class TelestackError extends Error {
|
|
144
|
-
constructor(
|
|
145
|
-
message: string,
|
|
146
|
-
public readonly status: number,
|
|
147
|
-
public readonly code?: string
|
|
148
|
-
) {
|
|
149
|
-
super(message);
|
|
150
|
-
this.name = 'TelestackError';
|
|
151
|
-
}
|
|
152
|
-
}
|