UNPKG

s3mini

Version:

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

3 lines (2 loc) • 15.9 kB
const t="AWS4-HMAC-SHA256",e="aws4_request",r="s3",s="UNSIGNED-PAYLOAD",o="application/octet-stream",n="application/xml",i=["accessKeyId","secretAccessKey","sessionToken","password","token"],a="x-amz-content-sha256",c="content-type",h="content-length",u="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]",g=d+"key must be a non-empty string",w=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",$=crypto.createHmac||(await import("node:crypto")).createHmac,T=crypto.createHash||(await import("node:crypto")).createHash,j=(t,e,r)=>{const s=$("sha256",t).update(e);return r?s.digest(r):s.digest()},O=t=>{const e={'"':"","&quot;":"","&#34;":"","&QUOT;":"","&#x00022":""};return t.replace(/^("|&quot;|&#34;)|("|&quot;|&#34;)$/g,t=>e[t])},A={"&quot;":'"',"&apos;":"'","&lt;":"<","&gt;":">","&amp;":"&"},q=t=>t.replace(/&(quot|apos|lt|gt|amp);/g,t=>A[t]??t),U=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?U(e):q(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:q(e.trim())},S=t=>"%"+t.charCodeAt(0).toString(16).toUpperCase(),D=t=>encodeURIComponent(t).replace(/[!'()*]/g,S);class _ extends Error{code;constructor(t,e,r){super(t),this.name=new.target.name,this.code=e,this.cause=r}}class v extends _{}class x extends _{status;serviceCode;body;constructor(t,e,r,s){super(t,r),this.status=e,this.serviceCode=r,this.body=s}}const P=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 C{accessKeyId;secretAccessKey;endpoint;region;requestSizeInBytes;requestAbortTimeout;logger;signingKeyDate;signingKey;constructor({accessKeyId:t,secretAccessKey:e,endpoint:r,region:s="auto",requestSizeInBytes:o=8388608,requestAbortTimeout:n,logger:i}){this.t(t,e,r),this.accessKeyId=t,this.secretAccessKey=e,this.endpoint=this.o(r),this.region=s,this.requestSizeInBytes=o,this.requestAbortTimeout=n,this.logger=i}i(t){return"object"!=typeof t||null===t?t:Object.keys(t).reduce((e,r)=>(e[r]=i.includes(r.toLowerCase())?"[REDACTED]":"object"==typeof t[r]&&null!==t[r]?this.i(t[r]):t[r],e),Array.isArray(t)?[]:{})}h(t,e,r={}){if(this.logger&&"function"==typeof this.logger[t]){const s=this.i(r),o={timestamp:(new Date).toISOString(),level:t,message:e,details:s,context:this.i({region:this.region,endpoint:this.endpoint,accessKeyId:this.accessKeyId?this.accessKeyId.substring(0,4)+"...":void 0})};this.logger[t](JSON.stringify(o))}}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)}}u(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")}l(t){if("string"!=typeof t||0===t.trim().length)throw this.h("error",g),new TypeError(g)}p(t){if("string"!=typeof t||0===t.trim().length)throw this.h("error",E),new TypeError(E)}m(t){if("string"!=typeof t)throw this.h("error",m),new TypeError(m)}$(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={},s=["if-match","if-none-match","if-modified-since","if-unmodified-since"];for(const[o,n]of Object.entries(t))s.includes(o.toLowerCase())?r[o]=n:e[o]=n;return{filteredOpts:e,conditionalHeaders:r}}j(t,e,r,s,o){if(this.l(t),!(r instanceof Buffer||"string"==typeof r))throw this.h("error",b),new TypeError(b);if("string"!=typeof e||0===e.trim().length)throw this.h("error",w),new TypeError(w);if(!Number.isInteger(s)||s<=0)throw this.h("error",d+"partNumber must be a positive integer"),new TypeError(d+"partNumber must be a positive integer");this.$(o)}O(t,e,r={},o={}){const n=new URL(this.endpoint);e&&e.length>0&&(n.pathname="/"===n.pathname?"/"+e.replace(/^\/+/,""):`${n.pathname}/${e.replace(/^\/+/,"")}`);const i=(new Date).toISOString().replace(/[:-]|\.\d{3}/g,""),c=i.slice(0,8),h=this.A(c);o[a]=s,o["x-amz-date"]=i,o.host=n.host;const u=["authorization","content-length","content-type","user-agent"];let d=Object.fromEntries(Object.entries(o).filter(([t])=>!u.includes(t.toLowerCase())));d=Object.fromEntries(Object.entries(d).sort(([t],[e])=>t.localeCompare(e)));const l=this.q(d),p=Object.keys(d).map(t=>t.toLowerCase()).sort().join(";"),y=this.U(t,n,r,l,p),f=this.S(i,h,y),g=this.D(c,f),w=this._(h,p,g);return o.authorization=w,{url:""+n,headers:o}}q(t){return Object.entries(t).map(([t,e])=>`${t.toLowerCase()}:${(e+"").trim()}`).join("\n")}U(t,e,r,o,n){return[t,e.pathname,this.v(r),o+"\n",n,s].join("\n")}A(t){return[t,this.region,r,e].join("/")}S(e,r,s){return[t,e,r,(o=s,T("sha256").update(o).digest("hex"))].join("\n");var o}D(t,e){return t!==this.signingKeyDate&&(this.signingKeyDate=t,this.signingKey=this.P(t)),j(this.signingKey,e,"hex")}_(e,r,s){return[`${t} Credential=${this.accessKeyId}/${e}`,"SignedHeaders="+r,"Signature="+s].join(", ")}async C(t,e,{query:r={},body:o="",headers:n={},tolerated:i=[],withQuery:c=!1}={}){if(!["GET","HEAD","PUT","POST","DELETE"].includes(t))throw Error(`${d}Unsupported HTTP method ${t}`);const{filteredOpts:h,conditionalHeaders:u}=["GET","HEAD"].includes(t)?this.T(r):{filteredOpts:r,conditionalHeaders:{}},l={[a]:s,...n,...u},p=e?D(e).replace(/%2F/g,"/"):"",{url:y,headers:f}=this.O(t,p,h,l);Object.keys(r).length>0&&(c=!0);const g=Object.fromEntries(Object.entries(h).map(([t,e])=>[t,e+""])),w=c&&Object.keys(h).length?`${y}?${new URLSearchParams(g)}`:y,b=Object.fromEntries(Object.entries(f).map(([t,e])=>[t,e+""]));return this.I(w,t,b,o,i)}getProps(){return{accessKeyId:this.accessKeyId,secretAccessKey:this.secretAccessKey,endpoint:this.endpoint,region:this.region,requestSizeInBytes:this.requestSizeInBytes,requestAbortTimeout:this.requestAbortTimeout,logger:this.logger}}setProps(t){this.t(t.accessKeyId,t.secretAccessKey,t.endpoint),this.accessKeyId=t.accessKeyId,this.secretAccessKey=t.secretAccessKey,this.region=t.region||"auto",this.endpoint=t.endpoint,this.requestSizeInBytes=t.requestSizeInBytes||8388608,this.requestAbortTimeout=t.requestAbortTimeout,this.logger=t.logger}sanitizeETag(t){return O(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]:n,[h]:""+Buffer.byteLength(t)};return 200===(await this.C("PUT","",{body:t,headers:e,tolerated:[200,404,403,409]})).status}async bucketExists(){return 200===(await this.C("HEAD","",{tolerated:[200,404,403]})).status}async listObjects(t="/",e="",r,s={}){this.p(t),this.m(e),this.$(s);const o="/"===t?t:D(t),n=!(r&&r>0);let i,a=n?1/0:r;const c=[];do{const t={"list-type":"2","max-keys":Math.min(a,1e3)+"",...e?{prefix:e}:{},...i?{"continuation-token":i}:{},...s},r=await this.C("GET",o,{query:t,withQuery:!0,tolerated:[200,404]});if(404===r.status)return null;if(200!==r.status){const t=await r.text(),e=r.headers.get("x-amz-error-code")||"Unknown",s=r.headers.get("x-amz-error-message")||r.statusText;throw this.h("error",`${d}Request failed with status ${r.status}: ${e} - ${s}, err body: ${t}`),Error(`${d}Request failed with status ${r.status}: ${e} - ${s}, err body: ${t}`)}const h=U(await r.text());if("object"!=typeof h||!h||"error"in h)throw this.h("error",`${d}Unexpected listObjects response shape: ${JSON.stringify(h)}`),Error(d+"Unexpected listObjects response shape");const u=h.ListBucketResult||h.listBucketResult||h,l=u.Contents||u.contents;if(l){const t=Array.isArray(l)?l:[l];c.push(...t),n||(a-=t.length)}i="true"===u.IsTruncated||"true"===u.isTruncated?u.NextContinuationToken||u.nextContinuationToken||u.NextMarker||u.nextMarker:void 0}while(i&&a>0);return c}async listMultipartUploads(t="/",e="",r="GET",s={}){this.p(t),this.m(e),this.u(r),this.$(s);const o={uploads:"",...s},n="/"===t?t:D(t),i=await this.C(r,n,{query:o,withQuery:!0}),a=U(await i.text());if("object"!=typeof a||null===a)throw Error(d+"Unexpected listMultipartUploads response shape");return"listMultipartUploadsResult"in a?a.listMultipartUploadsResult:a}async getObject(t,e={},r){const s=await this.C("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return[404,412,304].includes(s.status)?null:s.text()}async getObjectResponse(t,e={},r){const s=await this.C("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return[404,412,304].includes(s.status)?null:s}async getObjectArrayBuffer(t,e={},r){const s=await this.C("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return[404,412,304].includes(s.status)?null:s.arrayBuffer()}async getObjectJSON(t,e={},r){const s=await this.C("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});return[404,412,304].includes(s.status)?null:s.json()}async getObjectWithETag(t,e={},r){try{const s=await this.C("GET",t,{query:e,tolerated:[200,404,412,304],headers:r?{...r}:void 0});if([404,412,304].includes(s.status))return{etag:null,data:null};const o=s.headers.get(u);if(!o)throw Error(d+"ETag not found in response headers");return{etag:O(o),data:await s.arrayBuffer()}}catch(e){throw this.h("error",`Error getting object ${t} with ETag: ${e+""}`),e}}async getObjectRaw(t,e=!0,r=0,s=this.requestSizeInBytes,o={},n){return this.C("GET",t,{query:{...o},headers:{...e?{}:{range:`bytes=${r}-${s-1}`},...n},withQuery:!0})}async getContentLength(t,e){try{const r=(await this.C("HEAD",t,{headers:e?{...e}:void 0})).headers.get(h);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.C("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.C("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(u);if(!o)throw Error(d+"ETag not found in response headers");return O(o)}async putObject(t,e,r=o,s){if(!(e instanceof Buffer||"string"==typeof e))throw new TypeError(b);return this.C("PUT",t,{body:e,headers:{[h]:"string"==typeof e?Buffer.byteLength(e):e.length,[c]:r,...s},tolerated:[200]})}async getMultipartUploadId(t,e=o,r){if(this.l(t),"string"!=typeof e)throw new TypeError(d+"fileType must be a string");const s={[c]:e,...r},n=await this.C("POST",t,{query:{uploads:""},headers:s,withQuery:!0}),i=U(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(`${d}Failed to create multipart upload: ${JSON.stringify(i)}`)}async uploadPart(t,e,r,s,o={},n){this.j(t,e,r,s,o);const i={uploadId:e,partNumber:s,...o},a=await this.C("PUT",t,{query:i,body:r,headers:{[h]:"string"==typeof r?Buffer.byteLength(r):r.length,...n}});return{partNumber:s,etag:O(a.headers.get("etag")||"")}}async completeMultipartUpload(t,e,r){const s={uploadId:e},o=this.H(r),i={[c]:n,[h]:""+Buffer.byteLength(o)},a=await this.C("POST",t,{query:s,body:o,headers:i,withQuery:!0}),u=U(await a.text());if(u&&"object"==typeof u){const t=u.completeMultipartUploadResult||u.CompleteMultipartUploadResult||u;if(t&&"object"==typeof t){const e=t,r=e.ETag||e.eTag||e.etag;return r&&"string"==typeof r?{...e,etag:this.sanitizeETag(r)}:t}}throw Error(`${d}Failed to complete multipart upload: ${JSON.stringify(u)}`)}async abortMultipartUpload(t,e,r){if(this.l(t),!e)throw new TypeError(w);const s={uploadId:e},o={[c]:n,...r?{...r}:{}},i=await this.C("DELETE",t,{query:s,headers:o,withQuery:!0}),a=U(await i.text());if(a&&"error"in a&&"object"==typeof a.error&&null!==a.error&&"message"in a.error)throw this.h("error",`${d}Failed to abort multipart upload: ${a.error.message+""}`),Error(`${d}Failed to abort multipart upload: ${a.error.message+""}`);return{status:"Aborted",key:t,uploadId:e,response:a}}H(t){return`\n <CompleteMultipartUpload>\n ${t.map(t=>`\n <Part>\n <PartNumber>${t.partNumber}</PartNumber>\n <ETag>${t.etag}</ETag>\n </Part>\n `).join("")}\n </CompleteMultipartUpload>\n `}async deleteObject(t){const e=await this.C("DELETE",t,{tolerated:[200,204]});return 200===e.status||204===e.status}async N(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=(s=e,T("md5").update(s).digest("base64"));var s;const o={[c]:n,[h]:""+Buffer.byteLength(e),"Content-MD5":r},i=await this.C("POST","",{query:{delete:""},body:e,headers:o,withQuery:!0}),a=U(await i.text());if(!a||"object"!=typeof a)throw Error(`${d}Failed to delete objects: ${JSON.stringify(a)}`);const u=a.DeleteResult||a.deleteResult||a,l=new Map;t.forEach(t=>l.set(t,!1));const p=u.deleted||u.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=u.error||u.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,s=t.message||t.Message;e&&"string"==typeof e&&(l.set(e,!1),this.h("warn","Failed to delete object: "+e,{code:r||"Unknown",message:s||"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 s=0;s<t.length;s+=e){const o=t.slice(s,s+e);r.push(this.N(o))}return(await Promise.all(r)).flat()}return await this.N(t)}async I(t,e,r,s,o=[]){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:s,signal:void 0!==this.requestAbortTimeout?AbortSignal.timeout(this.requestAbortTimeout):void 0});return this.h("info",`Response status: ${n.status}, tolerated: ${o.join(",")}`),n.ok||o.includes(n.status)||await this.R(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 v("S3 network error: "+e,e,t);throw t}}async R(t){const e=await t.text(),r=t.headers.get("x-amz-error-code")??"Unknown",s=t.headers.get("x-amz-error-message")||t.statusText;throw this.h("error",`${d}Request failed with status ${t.status}: ${r} - ${s},err body: ${e}`),new x(`S3 returned ${t.status} – ${r}`,t.status,r,e)}v(t){return t&&0!==Object.keys(t).length?Object.keys(t).map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(t[e])}`).sort().join("&"):""}P(t){const s=j("AWS4"+this.secretAccessKey,t),o=j(s,this.region),n=j(o,r);return j(n,e)}}const I=C;export{C as S3mini,C as default,P as runInBatches,I as s3mini,O as sanitizeETag}; //# sourceMappingURL=s3mini.min.js.map