UNPKG

s3mini

Version:

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

3 lines (2 loc) • 15.4 kB
const t="AWS4-HMAC-SHA256",e="aws4_request",r="s3",s="UNSIGNED-PAYLOAD",o="application/octet-stream",i="application/xml",n=["accessKeyId","secretAccessKey","sessionToken","password","token"],a="x-amz-content-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",$=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 i=s[t];void 0===i?s[t]=r:Array.isArray(i)?i.push(r):s[t]=[i,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 x extends _{}class C 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 i(o),o=[]);return o.length&&await i(o),s;async function i(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 I{accessKeyId;secretAccessKey;endpoint;region;requestSizeInBytes;requestAbortTimeout;logger;signingKeyDate;signingKey;constructor({accessKeyId:t,secretAccessKey:e,endpoint:r,region:s="auto",requestSizeInBytes:o=8388608,requestAbortTimeout:i,logger:n}){this.t(t,e,r),this.accessKeyId=t,this.secretAccessKey=e,this.endpoint=this.o(r),this.region=s,this.requestSizeInBytes=o,this.requestAbortTimeout=i,this.logger=n}i(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.i(t[r]):t[r],e)),Array.isArray(t)?[]:{})}u(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.u("error",e),new TypeError(e)}}h(t){if("GET"!==t&&"HEAD"!==t)throw this.u("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.u("error",w),new TypeError(w)}p(t){if("string"!=typeof t||0===t.trim().length)throw this.u("error",E),new TypeError(E)}m(t){if("string"!=typeof t)throw this.u("error",m),new TypeError(m)}$(t){if("object"!=typeof t)throw this.u("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,i]of Object.entries(t))s.includes(o.toLowerCase())?r[o]=i:e[o]=i;return{filteredOpts:e,conditionalHeaders:r}}j(t,e,r,s,o){if(this.l(t),!(r instanceof Buffer||"string"==typeof r))throw this.u("error",b),new TypeError(b);if("string"!=typeof e||0===e.trim().length)throw this.u("error",g),new TypeError(g);if(!Number.isInteger(s)||s<=0)throw this.u("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 i=new URL(this.endpoint);e&&e.length>0&&(i.pathname="/"===i.pathname?"/"+e.replace(/^\/+/,""):`${i.pathname}/${e.replace(/^\/+/,"")}`);const n=(new Date).toISOString().replace(/[:-]|\.\d{3}/g,""),c=n.slice(0,8),u=this.A(c);o[a]=s,o["x-amz-date"]=n,o.host=i.host;const h=this.q(o),d=Object.keys(o).map((t=>t.toLowerCase())).sort().join(";"),l=this.U(t,i,r,h,d),p=this.S(n,u,l),y=this.D(c,p),f=this._(u,d,y);return o.Authorization=f,{url:""+i,headers:o}}q(t){return Object.entries(t).map((([t,e])=>`${t.toLowerCase()}:${(e+"").trim()}`)).sort().join("\n")}U(t,e,r,o,i){return[t,e.pathname,this.C(r),o+"\n",i,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 I(t,e,{query:r={},body:o="",headers:i={},tolerated:n=[],withQuery:c=!1}={}){if(!["GET","HEAD","PUT","POST","DELETE"].includes(t))throw Error(`${d}Unsupported HTTP method ${t}`);e&&this.l(e);const{filteredOpts:u,conditionalHeaders:h}=["GET","HEAD"].includes(t)?this.T(r):{filteredOpts:r,conditionalHeaders:{}},l={[a]:s,...i,...h},p=e?D(e).replace(/%2F/g,"/"):"",{url:y,headers:f}=this.O(t,p,u,l);Object.keys(r).length>0&&(c=!0);const w=Object.fromEntries(Object.entries(u).map((([t,e])=>[t,e+""]))),g=c&&Object.keys(u).length?`${y}?${new URLSearchParams(w)}`:y,b=Object.fromEntries(Object.entries(f).map((([t,e])=>[t,e+""])));return this.H(g,t,b,o,n)}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]:i,[u]:""+Buffer.byteLength(t)};return 200===(await this.I("PUT","",{body:t,headers:e,tolerated:[200,404,403,409]})).status}async bucketExists(){return 200===(await this.I("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),i=!(r&&r>0);let n,a=i?1/0:r;const c=[];do{const t={"list-type":"2","max-keys":Math.min(a,1e3)+"",...e?{prefix:e}:{},...n?{"continuation-token":n}:{},...s},r=await this.I("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.u("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 u=U(await r.text());if("object"!=typeof u||!u||"error"in u)throw this.u("error",`${d}Unexpected listObjects response shape: ${JSON.stringify(u)}`),Error(d+"Unexpected listObjects response shape");const h=u.ListBucketResult||u.listBucketResult||u,l=h.Contents||h.contents;if(l){const t=Array.isArray(l)?l:[l];c.push(...t),i||(a-=t.length)}n="true"===h.IsTruncated||"true"===h.isTruncated?h.NextContinuationToken||h.nextContinuationToken||h.NextMarker||h.nextMarker:void 0}while(n&&a>0);return c}async listMultipartUploads(t="/",e="",r="GET",s={}){this.p(t),this.m(e),this.h(r),this.$(s);const o={uploads:"",...s},i="/"===t?t:D(t),n=await this.I(r,i,{query:o,withQuery:!0}),a=U(await n.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={}){const r=await this.I("GET",t,{query:e,tolerated:[200,404,412,304]});return[404,412,304].includes(r.status)?null:r.text()}async getObjectResponse(t,e={}){const r=await this.I("GET",t,{query:e,tolerated:[200,404,412,304]});return[404,412,304].includes(r.status)?null:r}async getObjectArrayBuffer(t,e={}){const r=await this.I("GET",t,{query:e,tolerated:[200,404,412,304]});return[404,412,304].includes(r.status)?null:r.arrayBuffer()}async getObjectJSON(t,e={}){const r=await this.I("GET",t,{query:e,tolerated:[200,404,412,304]});return[404,412,304].includes(r.status)?null:r.json()}async getObjectWithETag(t,e={}){try{const r=await this.I("GET",t,{query:e,tolerated:[200,404,412,304]});if([404,412,304].includes(r.status))return{etag:null,data:null};const s=r.headers.get(h);if(!s)throw Error(d+"ETag not found in response headers");return{etag:O(s),data:await r.arrayBuffer()}}catch(e){throw this.u("error",`Error getting object ${t} with ETag: ${e+""}`),e}}async getObjectRaw(t,e=!0,r=0,s=this.requestSizeInBytes,o={}){return this.I("GET",t,{query:{...o},headers:e?{}:{range:`bytes=${r}-${s-1}`},withQuery:!0})}async getContentLength(t){try{const e=(await this.I("HEAD",t)).headers.get(u);return e?+e:0}catch(e){throw this.u("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.I("HEAD",t,{query:e,tolerated:[200,404,412,304]});return 404!==r.status&&(412!==r.status&&304!==r.status||null)}async getEtag(t,e={}){const r=await this.I("HEAD",t,{query:e,tolerated:[200,404]});if(404===r.status)return null;const s=r.headers.get(h);if(!s)throw Error(d+"ETag not found in response headers");return O(s)}async putObject(t,e,r=o){if(!(e instanceof Buffer||"string"==typeof e))throw new TypeError(b);return this.I("PUT",t,{body:e,headers:{[u]:"string"==typeof e?Buffer.byteLength(e):e.length,[c]:r},tolerated:[200]})}async getMultipartUploadId(t,e=o){if(this.l(t),"string"!=typeof e)throw new TypeError(d+"fileType must be a string");const r={[c]:e},s=await this.I("POST",t,{query:{uploads:""},headers:r,withQuery:!0}),i=U(await s.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={}){this.j(t,e,r,s,o);const i={uploadId:e,partNumber:s,...o},n=await this.I("PUT",t,{query:i,body:r,headers:{[u]:"string"==typeof r?Buffer.byteLength(r):r.length}});return{partNumber:s,etag:O(n.headers.get("etag")||"")}}async completeMultipartUpload(t,e,r){const s={uploadId:e},o=this.N(r),n={[c]:i,[u]:""+Buffer.byteLength(o)},a=await this.I("POST",t,{query:s,body:o,headers:n,withQuery:!0}),h=U(await a.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:this.sanitizeETag(r)}:t}}throw Error(`${d}Failed to complete multipart upload: ${JSON.stringify(h)}`)}async abortMultipartUpload(t,e){if(this.l(t),!e)throw new TypeError(g);const r={uploadId:e},s={[c]:i},o=await this.I("DELETE",t,{query:r,headers:s,withQuery:!0}),n=U(await o.text());if(n&&"error"in n&&"object"==typeof n.error&&null!==n.error&&"message"in n.error)throw this.u("error",`${d}Failed to abort multipart upload: ${n.error.message+""}`),Error(`${d}Failed to abort multipart upload: ${n.error.message+""}`);return{status:"Aborted",key:t,uploadId:e,response:n}}N(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.I("DELETE",t,{tolerated:[200,204]});return 200===e.status||204===e.status}async R(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]:i,[u]:""+Buffer.byteLength(e),"Content-MD5":r},n=await this.I("POST","",{query:{delete:""},body:e,headers:o,withQuery:!0}),a=U(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,s=t.message||t.Message;e&&"string"==typeof e&&(l.set(e,!1),this.u("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.R(o))}return(await Promise.all(r)).flat()}return await this.R(t)}async H(t,e,r,s,o=[]){this.u("info",`Sending ${e} request to ${t}`,"headers: "+JSON.stringify(r));try{const i=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.u("info",`Response status: ${i.status}, tolerated: ${o.join(",")}`),i.ok||o.includes(i.status)||await this.k(i),i}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 x("S3 network error: "+e,e,t);throw t}}async k(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.u("error",`${d}Request failed with status ${t.status}: ${r} - ${s},err body: ${e}`),new C(`S3 returned ${t.status} – ${r}`,t.status,r,e)}C(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),i=j(o,r);return j(i,e)}}export{I as default,P as runInBatches,I as s3mini,O as sanitizeETag}; //# sourceMappingURL=s3mini.min.js.map