UNPKG

s3mini

Version:

👶 Tiny & fast S3 client for node and edge computing platforms

3 lines (2 loc) • 18.7 kB
const t="AWS4-HMAC-SHA256",e="aws4_request",r="UNSIGNED-PAYLOAD",s="application/octet-stream",o="application/xml",n=new Set(["accesskeyid","secretaccesskey","sessiontoken","password","token"]),i=new Set(["if-match","if-none-match","if-modified-since","if-unmodified-since"]),a="x-amz-content-sha256",c="x-amz-checksum-sha256",h="host",u="content-type",d="content-length",l="etag",p="[s3mini] ",y=p+"accessKeyId must be a non-empty string",f=p+"secretAccessKey must be a non-empty string",w=p+"endpoint must be a non-empty string",g=p+"endpoint must be a valid URL. Expected format: https://<host>[:port][/base-path]",b=p+"key must be a non-empty string",m=p+"uploadId must be a non-empty string",E=p+"data must be a Buffer or string",$=p+"prefix must be a string",j=p+"delimiter must be a string",T=new TextEncoder,O=new Uint8Array([48,49,50,51,52,53,54,55,56,57,97,98,99,100,101,102]),A=t=>{if("string"==typeof t)return T.encode(t).byteLength;if(t instanceof ArrayBuffer||t instanceof Uint8Array)return t.byteLength;if(t instanceof Blob)return t.size;throw Error("Unsupported data type")},S=t=>{const e=new Uint8Array(t),r=new Uint8Array(2*e.length);for(let t=0,s=0;t<e.length;t++)r[s++]=O[e[t]>>4],r[s++]=O[15&e[t]];return String.fromCodePoint(...r)},U=async t=>{const e=T.encode(t);return await globalThis.crypto.subtle.digest("SHA-256",e)},x=async(t,e)=>{const r=await globalThis.crypto.subtle.importKey("raw","string"==typeof t?T.encode(t):t,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),s=T.encode(e);return await globalThis.crypto.subtle.sign("HMAC",r,s)},_=t=>{const e={'"':"","&quot;":"","&#34;":""};return t.replaceAll(/(^("|&quot;|&#34;))|(("|&quot;|&#34;)$)/g,t=>e[t]||"")},C={"&quot;":'"',"&apos;":"'","&lt;":"<","&gt;":">","&amp;":"&"},v=t=>t.replaceAll(/&(quot|apos|lt|gt|amp);/g,t=>C[t]??t),q=t=>{const e=t.replace(/<\?xml[^?]*\?>\s*/,""),r=/<([A-Za-z_][\w\-.]*)[^>]*>([\s\S]*?)<\/\1>/gm,s={};let o;for(;null!==(o=r.exec(e));){const t=o[1],e=o[2],r=e?q(e):v(e?.trim()||"");if(!t)continue;const n=s[t];void 0===n?s[t]=r:Array.isArray(n)?n.push(r):s[t]=[n,r]}return Object.keys(s).length>0?s:v(e.trim())},D=t=>"%"+(t.codePointAt(0)??0).toString(16).toUpperCase(),k=t=>encodeURIComponent(t).replaceAll(/[!'()*]/g,D);class H extends Error{code;constructor(t,e,r){super(t),this.name=new.target.name,this.code=e,this.cause=r}}class P extends H{}class R extends H{status;serviceCode;body;constructor(t,e,r,s){super(t,r),this.status=e,this.serviceCode=r,this.body=s}}const N=async(t,e=30,r=0)=>{const s=[];let o=[];for(const r of t)o.push(r),o.length===e&&(await n(o),o=[]);return o.length&&await n(o),s;async function n(t){const e=Date.now(),o=await Promise.allSettled(t.map(t=>t()));if(s.push(...o),r>0){const t=r-(Date.now()-e);t>0&&await new Promise(e=>setTimeout(e,t))}}};class z{#t;#e;endpoint;region;bucketName;requestSizeInBytes;requestAbortTimeout;logger;t;signingKeyDate;signingKey;constructor({accessKeyId:t,secretAccessKey:e,endpoint:r,region:s="auto",requestSizeInBytes:o=8388608,requestAbortTimeout:n,logger:i,fetch:a=globalThis.fetch}){this.o(t,e,r),this.#t=t,this.#e=e,this.endpoint=new URL(this.i(r)),this.region=s,this.bucketName=this.h(),this.requestSizeInBytes=o,this.requestAbortTimeout=n,this.logger=i,this.t=(t,e)=>a(t,e)}u(t){return"object"!=typeof t||null===t?t:Object.keys(t).reduce((e,r)=>(e[r]=n.has(r.toLowerCase())?"[REDACTED]":"object"==typeof t[r]&&null!==t[r]?this.u(t[r]):t[r],e),Array.isArray(t)?[]:{})}l(t,e,r={}){if(this.logger&&"function"==typeof this.logger[t]){const s=this.u(r),o={timestamp:(new Date).toISOString(),level:t,message:e,details:s,context:this.u({region:this.region,endpoint:""+this.endpoint,accessKeyId:this.#t?this.#t.substring(0,4)+"...":void 0})};this.logger[t](JSON.stringify(o))}}o(t,e,r){if("string"!=typeof t)throw new TypeError(y);if("string"!=typeof e)throw new TypeError(f);if("string"!=typeof r||0===r.trim().length)throw new TypeError(w)}p(){return this.#t.trim().length>0&&this.#e.trim().length>0}i(t){const e=/^(https?:)?\/\//i.test(t)?t:"https://"+t;try{new URL(e);let t=e.length;for(;t>0&&"/"===e[t-1];)t--;return t===e.length?e:e.substring(0,t)}catch{const e=`${g} But provided: "${t}"`;throw this.l("error",e),new TypeError(e)}}m(t){if("GET"!==t&&"HEAD"!==t)throw this.l("error",p+"method must be either GET or HEAD"),Error(p+"method must be either GET or HEAD")}$(t){if("string"!=typeof t||0===t.trim().length)throw this.l("error",b),new TypeError(b)}j(t){if("string"!=typeof t||0===t.trim().length)throw this.l("error",j),new TypeError(j)}T(t){if("string"!=typeof t)throw this.l("error",$),new TypeError($)}O(t){if("object"!=typeof t)throw this.l("error",p+"opts must be an object"),new TypeError(p+"opts must be an object")}A(t){const e={},r={};for(const[s,o]of Object.entries(t))i.has(s.toLowerCase())?r[s]=o:e[s]=o;return{filteredOpts:e,conditionalHeaders:r}}S(t){if(!(globalThis.Buffer&&t instanceof globalThis.Buffer||"string"==typeof t))throw this.l("error",E),new TypeError(E);return t}U(t,e,r,s,o){if(this.$(t),"string"!=typeof e||0===e.trim().length)throw this.l("error",m),new TypeError(m);if(!Number.isInteger(s)||s<=0)throw this.l("error",p+"partNumber must be a positive integer"),new TypeError(p+"partNumber must be a positive integer");return this.O(o),this.S(r)}async _(s,o,n={},i={}){const c=new URL(this.endpoint);if(o&&o.length>0&&(c.pathname="/"===c.pathname?"/"+o.replace(/^\/+/,""):`${c.pathname}/${o.replace(/^\/+/,"")}`),!this.p())return i[h]=c.host,{url:""+c,headers:i};const u=new Date,d=`${u.getUTCFullYear()}${(u.getUTCMonth()+1+"").padStart(2,"0")}${(u.getUTCDate()+"").padStart(2,"0")}`,l=`${d}T${(u.getUTCHours()+"").padStart(2,"0")}${(u.getUTCMinutes()+"").padStart(2,"0")}${(u.getUTCSeconds()+"").padStart(2,"0")}Z`,p=`${d}/${this.region}/s3/${e}`;i[a]=r,i["x-amz-date"]=l,i[h]=c.host;const y=new Set(["authorization","content-length","content-type","user-agent"]);let f="",w="";for(const[t,e]of Object.entries(i).sort(([t],[e])=>t.localeCompare(e))){const r=t.toLowerCase();y.has(r)||(f&&(f+="\n",w+=";"),f+=`${r}:${(e+"").trim()}`,w+=r)}const g=`${s}\n${c.pathname}\n${this.C(n)}\n${f}\n\n${w}\n${r}`,b=`${t}\n${l}\n${p}\n${S(await U(g))}`;d===this.signingKeyDate&&this.signingKey||(this.signingKeyDate=d,this.signingKey=await this.v(d));const m=S(await x(this.signingKey,b));return i.authorization=`${t} Credential=${this.#t}/${p}, SignedHeaders=${w}, Signature=${m}`,{url:""+c,headers:i}}async q(t,e,{query:s={},body:o="",headers:n={},tolerated:i=[],withQuery:c=!1}={}){const{filteredOpts:h,conditionalHeaders:u}=["GET","HEAD"].includes(t)?this.A(s):{filteredOpts:s,conditionalHeaders:{}},d={[a]:r,...n,...u},l=e?k(e).replaceAll("%2F","/"):"",{url:p,headers:y}=await this._(t,l,h,d);Object.keys(s).length>0&&(c=!0);const f=c&&Object.keys(h).length?`${p}?${this.C(h)}`:p,w=Object.fromEntries(Object.entries(y).map(([t,e])=>[t,e+""]));return this.D(f,t,w,o,i)}sanitizeETag(t){return _(t)}async createBucket(){const t=`\n <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">\n <LocationConstraint>${this.region}</LocationConstraint>\n </CreateBucketConfiguration>\n `,e={[u]:o,[d]:A(t)};return 200===(await this.q("PUT","",{body:t,headers:e,tolerated:[200,404,403,409]})).status}h(){const t=this.endpoint,e=t.pathname.split("/").filter(Boolean);if(e.length>0&&"string"==typeof e[0])return e[0];const r=t.hostname.split(".");if(r.length>=3){const t=r.slice(-2).join(".");if(["amazonaws.com","digitaloceanspaces.com","cloudflare.com"].some(e=>t.includes(e))&&"string"==typeof r[0])return r[0]}return r[0]||""}async bucketExists(){return 200===(await this.q("HEAD","",{tolerated:[200,404,403]})).status}async listObjects(t="/",e="",r,s={}){this.j(t),this.T(e),this.O(s);const o="/"===t?t:k(t),n=!(r&&r>0);let i,a=n?1/0:r;const c=[];do{const t=await this.k(o,e,a,i,s);if(null===t)return null;c.push(...t.objects),n||(a-=t.objects.length),i=t.continuationToken}while(i&&a>0);return c}async listObjectsPaged(t="/",e="",r=100,s,o={}){this.j(t),this.T(e),this.O(o);const n="/"===t?t:k(t);let i=s;const a=[],c=await this.k(n,e,r,i,o);return null===c?null:(a.push(...c.objects),i=c.continuationToken,{objects:a,nextContinuationToken:i})}async k(t,e,r,s,o){const n=this.H(e,r,s,o),i=await this.q("GET",t,{query:n,withQuery:!0,tolerated:[200,404]});if(404===i.status)return null;200!==i.status&&await this.P(i);const a=await i.text();return this.R(a)}H(t,e,r,s){return{"list-type":"2","max-keys":Math.min(e,1e3)+"",...t?{prefix:t}:{},...r?{"continuation-token":r}:{},...s}}async P(t){const e=await t.text(),r=this.N(t.headers,e),s=t.headers.get("x-amz-error-code")??r.svcCode??"Unknown",o=t.headers.get("x-amz-error-message")??r.errorMessage??t.statusText;throw this.l("error",`${p}Request failed with status ${t.status}: ${s} - ${o}, err body: ${e}`),Error(`${p}Request failed with status ${t.status}: ${s} - ${o}, err body: ${e}`)}R(t){const e=q(t);if("object"!=typeof e||!e||"error"in e)throw this.l("error",`${p}Unexpected listObjects response shape: ${JSON.stringify(e)}`),Error(p+"Unexpected listObjects response shape");const r=e.ListBucketResult||e.listBucketResult||e;return{objects:this.I(r),continuationToken:this.M(r)}}I(t){const e=t.Contents||t.contents,r=t.CommonPrefixes||t.commonPrefixes,s=[];if(e&&(Array.isArray(e)?s.push(...e):s.push(e)),r){const t=Array.isArray(r)?r:[r];for(const e of t){const t=e.Prefix||e.prefix;"string"==typeof t&&s.push({Key:t,Size:0,LastModified:new Date(0),ETag:"",StorageClass:""})}}return s}M(t){if("true"===t.IsTruncated||"true"===t.isTruncated)return t.NextContinuationToken||t.nextContinuationToken||t.NextMarker||t.nextMarker}async listMultipartUploads(t="/",e="",r="GET",s={}){this.j(t),this.T(e),this.m(r),this.O(s);const o={uploads:"",...s},n="/"===t?t:k(t),i=await this.q(r,n,{query:o,withQuery:!0}),a=q(await i.text());if("object"!=typeof a||null===a)throw Error(p+"Unexpected listMultipartUploads response shape");return"listMultipartUploadsResult"in a?a.listMultipartUploadsResult:a}async getObject(t,e={},r){const s=await this.q("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return 200===s.status?s.text():null}async getObjectResponse(t,e={},r){const s=await this.q("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return 200===s.status?s:null}async getObjectArrayBuffer(t,e={},r){const s=await this.q("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return 200===s.status?s.arrayBuffer():null}async getObjectJSON(t,e={},r){const s=await this.q("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return 200===s.status?s.json():null}async getObjectWithETag(t,e={},r){try{const s=await this.q("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0}),o=s.status;if(404===o||412===o||304===o)return{etag:null,data:null};const n=s.headers.get(l);if(!n)throw Error(p+"ETag not found in response headers");return{etag:_(n),data:await s.arrayBuffer()}}catch(e){throw this.l("error",`Error getting object ${t} with ETag: ${e+""}`),e}}async getObjectRaw(t,e=!0,r=0,s,o={},n){let i={};return e||(i=void 0===s?{range:`bytes=${r}-`}:{range:`bytes=${r}-${s-1}`}),this.q("GET",t,{query:{...o},headers:{...i,...n},withQuery:!0})}async getContentLength(t,e){try{const r=(await this.q("HEAD",t,{headers:e?{...e}:void 0})).headers.get(d);return r?+r:0}catch(e){throw this.l("error",`Error getting content length for object ${t}: ${e+""}`),Error(`${p}Error getting content length for object ${t}: ${e+""}`)}}async objectExists(t,e={}){const r=await this.q("HEAD",t,{query:e,tolerated:[200,404,412,304]});return 404!==r.status&&(412!==r.status&&304!==r.status||null)}async getEtag(t,e={},r){const s=await this.q("HEAD",t,{query:e,tolerated:[200,304,404,412],headers:r?{...r}:void 0});if(404===s.status)return null;if(412===s.status||304===s.status)return null;const o=s.headers.get(l);if(!o)throw Error(p+"ETag not found in response headers");return _(o)}async putObject(t,e,r=s,o,n){return this.q("PUT",t,{body:this.S(e),headers:{[d]:A(e),[u]:r,...n,...o},tolerated:[200]})}async getMultipartUploadId(t,e=s,r){if(this.$(t),"string"!=typeof e)throw new TypeError(p+"fileType must be a string");const o={[u]:e,...r},n=await this.q("POST",t,{query:{uploads:""},headers:o,withQuery:!0}),i=q(await n.text());if(i&&"object"==typeof i){const t=i.initiateMultipartUploadResult||i.InitiateMultipartUploadResult;if(t&&"object"==typeof t){const e=t.uploadId||t.UploadId;if(e&&"string"==typeof e)return e}}throw Error(`${p}Failed to create multipart upload: ${JSON.stringify(i)}`)}async uploadPart(t,e,r,s,o={},n){const i=this.U(t,e,r,s,o),a={uploadId:e,partNumber:s,...o},c=await this.q("PUT",t,{query:a,body:i,headers:{[d]:A(r),...n}});return{partNumber:s,etag:_(c.headers.get("etag")||"")}}async completeMultipartUpload(t,e,r){const s={uploadId:e},n=this.G(r),i={[u]:o,[d]:A(n)},a=await this.q("POST",t,{query:s,body:n,headers:i,withQuery:!0}),c=q(await a.text());if(c&&"object"==typeof c){const t=c.completeMultipartUploadResult||c.CompleteMultipartUploadResult||c;if(t&&"object"==typeof t){const e=t,r=e.ETag||e.eTag||e.etag;return r&&"string"==typeof r?{...e,etag:_(r)}:t}}throw Error(`${p}Failed to complete multipart upload: ${JSON.stringify(c)}`)}async abortMultipartUpload(t,e,r){if(this.$(t),!e)throw new TypeError(m);const s={uploadId:e},n={[u]:o,...r?{...r}:{}},i=await this.q("DELETE",t,{query:s,headers:n,withQuery:!0}),a=q(await i.text());if(a&&"error"in a&&"object"==typeof a.error&&null!==a.error&&"message"in a.error)throw this.l("error",`${p}Failed to abort multipart upload: ${a.error.message+""}`),Error(`${p}Failed to abort multipart upload: ${a.error.message+""}`);return{status:"Aborted",key:t,uploadId:e,response:a}}G(t){let e="<CompleteMultipartUpload>";for(const r of t)e+=`<Part><PartNumber>${r.partNumber}</PartNumber><ETag>${r.etag}</ETag></Part>`;return e+="</CompleteMultipartUpload>",e}async L(t,e,r){const{metadataDirective:s="COPY",metadata:o={},contentType:n,storageClass:i,taggingDirective:a,websiteRedirectLocation:c,sourceSSECHeaders:h={},destinationSSECHeaders:d={},additionalHeaders:l={}}=r,p={"x-amz-copy-source":e,"x-amz-metadata-directive":s,...l,...n&&{[u]:n},...i&&{"x-amz-storage-class":i},...a&&{"x-amz-tagging-directive":a},...c&&{"x-amz-website-redirect-location":c},...this.K(h,d),..."REPLACE"===s?this.B(o):{}};try{const e=await this.q("PUT",t,{headers:p,tolerated:[200]});return this.F(await e.text())}catch(e){throw this.l("error","Error in copy operation to "+t,{error:e+""}),e}}copyObject(t,e,r={}){this.$(t),this.$(e);const s=`/${this.bucketName}/${k(t)}`;return this.L(e,s,r)}K(t,e){const r={};for(const[s,o]of Object.entries({...t,...e}))void 0!==o&&(r[s]=o);return r}async moveObject(t,e,r={}){try{const s=await this.copyObject(t,e,r);if(!await this.deleteObject(t))throw Error(p+"Failed to delete source object after successful copy");return s}catch(r){throw this.l("error",`Error moving object from ${t} to ${e}`,{error:r+""}),r}}B(t){const e={};for(const[r,s]of Object.entries(t))e[r.startsWith("x-amz-meta-")?r:"x-amz-meta-"+r]=s;return e}F(t){const e=q(t);if(!e||"object"!=typeof e)throw Error(p+"Unexpected copyObject response format");const r=e.CopyObjectResult||e.copyObjectResult||e,s=r.ETag||r.eTag||r.etag,o=r.LastModified||r.lastModified;if(!s||"string"!=typeof s)throw Error(p+"ETag not found in copyObject response");return{etag:_(s),lastModified:o?new Date(o):void 0}}async deleteObject(t){const e=await this.q("DELETE",t,{tolerated:[200,204]});return 200===e.status||204===e.status}async J(t){const e="<Delete>"+t.map(t=>{return`<Object><Key>${e=t,e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}</Key></Object>`;var e}).join("")+"</Delete>",r=(t=>{const e=new Uint8Array(t);let r="";for(let t=0;t<e.length;t+=32768){const s=e.subarray(t,t+32768);r+=btoa(String.fromCodePoint(...s))}return r})(await U(e)),s={[u]:o,[d]:A(e),[c]:r},n=await this.q("POST","",{query:{delete:""},body:e,headers:s,withQuery:!0}),i=q(await n.text());if(!i||"object"!=typeof i)throw Error(`${p}Failed to delete objects: ${JSON.stringify(i)}`);const a=i.DeleteResult||i.deleteResult||i,h=new Map;for(const e of t)h.set(e,!1);const l=a.deleted||a.Deleted;if(l){const t=Array.isArray(l)?l:[l];for(const e of t)if(e&&"object"==typeof e){const t=e.key||e.Key;t&&"string"==typeof t&&h.set(t,!0)}}const y=a.error||a.Error;if(y){const t=Array.isArray(y)?y:[y];for(const e of t)if(e&&"object"==typeof e){const t=e.key||e.Key,r=e.code||e.Code,s=e.message||e.Message;t&&"string"==typeof t&&(h.set(t,!1),this.l("warn","Failed to delete object: "+t,{code:r||"Unknown",message:s||"Unknown error"}))}}return t.map(t=>h.get(t)||!1)}async deleteObjects(t){if(!Array.isArray(t)||0===t.length)return[];const e=1e3;if(t.length>e){const r=[];for(let s=0;s<t.length;s+=e){const o=t.slice(s,s+e);r.push(this.J(o))}return(await Promise.all(r)).flat()}return await this.J(t)}async D(t,e,r,s,o=[]){this.l("info",`Sending ${e} request to ${t}`,"headers: "+JSON.stringify(r));try{const n=await this.t(t,{method:e,headers:r,body:"GET"===e||"HEAD"===e?void 0:s,signal:this.requestAbortTimeout?AbortSignal.timeout(this.requestAbortTimeout):void 0});return this.l("info",`Response status: ${n.status}, tolerated: ${o.join(",")}`),n.ok||o.includes(n.status)||await this.W(n),n}catch(t){const e=(t=>{if("object"!=typeof t||null===t)return;const e=t;return"string"==typeof e.code?e.code:"string"==typeof e.cause?.code?e.cause.code:void 0})(t);if(e&&["ENOTFOUND","EAI_AGAIN","ETIMEDOUT","ECONNREFUSED"].includes(e))throw new P("S3 network error: "+e,e,t);throw t}}N(t,e){if("application/xml"!==t.get("content-type"))return{};const r=q(e);if(!r||"object"!=typeof r||!("Error"in r)||!r.Error||"object"!=typeof r.Error)return{};const s=r.Error;return{svcCode:"Code"in s&&"string"==typeof s.Code?s.Code:void 0,errorMessage:"Message"in s&&"string"==typeof s.Message?s.Message:void 0}}async W(t){const e=await t.text(),r=this.N(t.headers,e),s=t.headers.get("x-amz-error-code")??r.svcCode??"Unknown",o=t.headers.get("x-amz-error-message")??r.errorMessage??t.statusText;throw this.l("error",`${p}Request failed with status ${t.status}: ${s} - ${o},err body: ${e}`),new R(`S3 returned ${t.status} – ${s}`,t.status,s,e)}C(t){return t&&0!==Object.keys(t).length?Object.keys(t).map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(t[e])}`).sort((t,e)=>t.localeCompare(e)).join("&"):""}async v(t){const r=await x("AWS4"+this.#e,t),s=await x(r,this.region),o=await x(s,"s3");return await x(o,e)}}export{z as S3mini,z as default,N as runInBatches,_ as sanitizeETag}; //# sourceMappingURL=s3mini.min.js.map