UNPKG

s3mini

Version:

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

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