@stacksjs/storage 0.70.351 → 0.70.353

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.
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{basename}from"node:path";import{normalizeExpiryToMilliseconds}from"../types";import{sanitizePresignedDir,sanitizePresignedFilename}from"../path-sanitize";import{signS3PresignedPost}from"../s3-presigned-post";import process from"node:process";const S3_MIN_PART_SIZE=5242880,S3_MAX_PART_SIZE=5368709120;function clampPartSize(requested){if(!Number.isFinite(requested))return S3_MIN_PART_SIZE;return Math.max(S3_MIN_PART_SIZE,Math.min(Math.floor(requested),S3_MAX_PART_SIZE))}class ChunkBuffer{chunks=[];total=0;constructor(_partSize){}get length(){return this.total}push(c){this.chunks.push(c);this.total+=c.length}take(n){const out=new Uint8Array(n);let written=0;while(written<n&&this.chunks.length>0){const head=this.chunks[0],need=n-written;if(head.length<=need){out.set(head,written);written+=head.length;this.chunks.shift()}else{out.set(head.subarray(0,need),written);this.chunks[0]=head.subarray(need);written+=need}}this.total-=n;return out}flush(){const out=new Uint8Array(this.total);let off=0;for(const c of this.chunks){out.set(c,off);off+=c.length}this.chunks=[];this.total=0;return out}}async function isSettled(p){const sentinel=Symbol("pending");return await Promise.race([p.then(()=>"settled",()=>"settled"),Promise.resolve(sentinel)])!==sentinel}export function resolveS3ClientOptions(config){const options={};if(config.endpoint)options.endpoint=config.endpoint.replace(/^https?:\/\//i,"").replace(/\/+$/,"");if(config.usePathStyleEndpoint)options.forcePathStyle=!0;if(config.credentials)options.credentials=config.credentials;return Object.keys(options).length>0?options:void 0}export class S3StorageAdapter{_client;_clientPromise=null;bucket;prefix;region;endpoint;usePathStyleEndpoint;credentials;constructor(client,config){this._client=client;this.bucket=config.bucket||"";this.prefix=config.prefix||"";this.region=config.region||"us-east-1";this.credentials=config.credentials;this.endpoint=config.endpoint;this.usePathStyleEndpoint=config.usePathStyleEndpoint;if(!this.bucket)throw Error("S3 bucket name is required")}async getClient(){if(this._client)return this._client;if(!this._clientPromise)this._clientPromise=import("@stacksjs/ts-cloud").then((cloud)=>{this._client=new cloud.S3Client(this.region,void 0,resolveS3ClientOptions({endpoint:this.endpoint,usePathStyleEndpoint:this.usePathStyleEndpoint,credentials:this.credentials}));return this._client});return this._clientPromise}resolveCredentials(){if(this.credentials?.accessKeyId&&this.credentials.secretAccessKey)return this.credentials;const accessKeyId=process.env.AWS_ACCESS_KEY_ID,secretAccessKey=process.env.AWS_SECRET_ACCESS_KEY,sessionToken=process.env.AWS_SESSION_TOKEN;if(!accessKeyId||!secretAccessKey)throw Error("[storage/s3] presignedUploadPolicy requires AWS credentials \u2014 "+"pass them via S3DiskConfig.credentials or set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY.");return{accessKeyId,secretAccessKey,sessionToken}}prefixPath(path){if(!this.prefix)return path;return`${this.prefix}/${path}`.replace(/\/+/g,"/")}stripPrefix(path){if(!this.prefix)return path;const prefixWithSlash=`${this.prefix}/`;return path.startsWith(prefixWithSlash)?path.slice(prefixWithSlash.length):path}async contentsToBuffer(contents){if(typeof contents==="string")return Buffer.from(contents,"utf8");else if(contents instanceof Buffer)return contents;else if(contents instanceof Uint8Array)return Buffer.from(contents);else{if(typeof contents.getReader!=="function")throw TypeError("[storage/s3] contents must be a web-standard ReadableStream (with .getReader()), not a Node stream.Readable. Convert via Readable.toWeb(nodeStream) before passing.");const reader=contents.getReader(),chunks=[];while(!0){const{done,value}=await reader.read();if(done)break;if(value)chunks.push(value)}return Buffer.concat(chunks.map((c)=>Buffer.from(c)))}}async write(path,contents){const key=this.prefixPath(path),body=await this.contentsToBuffer(contents),contentType=this.detectMimeType(path);await(await this.getClient()).putObject({bucket:this.bucket,key,body,contentType});return{path,size:body.length,contentType,lastModified:Date.now()}}async read(path){const key=this.prefixPath(path),response=await(await this.getClient()).getObject(this.bucket,key);if(!response)throw Error(`Failed to read file: ${path}`);return Buffer.from(response)}async getStream(path,_options){const key=this.prefixPath(path),buf=await(await this.getClient()).getObjectBuffer(this.bucket,key);if(!buf)throw Error(`Failed to read file: ${path}`);const bytes=new Uint8Array(buf);return new ReadableStream({start(controller){controller.enqueue(bytes);controller.close()}})}async putStream(path,stream,options){const key=this.prefixPath(path),contentType=options?.contentType??this.detectMimeType(path),partSize=clampPartSize(options?.partSize??5242880),concurrency=Math.max(1,Math.min(options?.concurrency??4,100)),maxRetries=Math.max(0,options?.maxRetries??3),signal=options?.signal,reader=stream.getReader();let firstChunk=null,firstDone=!1;{const buf=new ChunkBuffer(partSize);while(!firstDone&&buf.length<partSize){if(signal?.aborted){try{reader.releaseLock()}catch{}throw Error("aborted")}const{value,done}=await reader.read();if(done){firstDone=!0;break}if(value)buf.push(value)}firstChunk=buf.flush()}if(firstDone){try{reader.releaseLock()}catch{}await(await this.getClient()).putObject({bucket:this.bucket,key,body:Buffer.from(firstChunk),contentType});return{path,size:firstChunk.length,contentType,lastModified:Date.now()}}const{UploadId:uploadId}=await(await this.getClient()).createMultipartUpload(this.bucket,key,{contentType}),completedParts=[];let totalBytes=0,partNumber=1;const inflight=[],uploadOne=async(body,n)=>{let attempt=0;while(!0){if(signal?.aborted)throw Error("aborted");try{const{ETag}=await(await this.getClient()).uploadPart(this.bucket,key,uploadId,n,Buffer.from(body));completedParts.push({PartNumber:n,ETag});totalBytes+=body.length;return}catch(err){if(attempt>=maxRetries)throw err;attempt+=1}}};try{inflight.push(uploadOne(firstChunk,partNumber++));firstChunk=null;const buf=new ChunkBuffer(partSize);while(!0){if(signal?.aborted)throw Error("aborted");const{value,done}=await reader.read();if(done)break;if(value)buf.push(value);while(buf.length>=partSize){const part=buf.take(partSize);if(inflight.length>=concurrency){await Promise.race(inflight.map((p,i)=>p.then(()=>i)));for(let i=inflight.length-1;i>=0;i--)if(await isSettled(inflight[i]))inflight.splice(i,1)}inflight.push(uploadOne(part,partNumber++))}}try{reader.releaseLock()}catch{}const tail=buf.flush();if(tail.length>0)inflight.push(uploadOne(tail,partNumber++));await Promise.all(inflight);completedParts.sort((a,b)=>a.PartNumber-b.PartNumber);await(await this.getClient()).completeMultipartUpload(this.bucket,key,uploadId,completedParts);return{path,size:totalBytes,contentType,lastModified:Date.now()}}catch(err){try{await(await this.getClient()).abortMultipartUpload(this.bucket,key,uploadId)}catch{}throw err}}async readToString(path){const key=this.prefixPath(path),response=await(await this.getClient()).getObject(this.bucket,key);if(!response)throw Error(`Failed to read file: ${path}`);return response}async readToBuffer(path){return await this.read(path)}async readToUint8Array(path){const buffer=await this.readToBuffer(path);return new Uint8Array(buffer)}async deleteFile(path){const key=this.prefixPath(path);await(await this.getClient()).deleteObject(this.bucket,key)}async deleteDirectory(path){const prefix=this.prefixPath(path),normalizedPrefix=prefix.endsWith("/")?prefix:`${prefix}/`,keys=(await(await this.getClient()).listAllObjects({bucket:this.bucket,prefix:normalizedPrefix})).map((obj)=>obj.Key).filter((k)=>typeof k==="string");if(keys.length===0)return;await(await this.getClient()).deleteObjects(this.bucket,keys)}async createDirectory(_path){}async moveFile(from,to){await this.copyFile(from,to);await this.deleteFile(from)}async copyFile(from,to){const fromKey=this.prefixPath(from),toKey=this.prefixPath(to);await(await this.getClient()).copyObject({sourceBucket:this.bucket,sourceKey:fromKey,destinationBucket:this.bucket,destinationKey:toKey})}async stat(path){const key=this.prefixPath(path),result=await(await this.getClient()).headObject(this.bucket,key);if(!result)throw Error(`File not found: ${path}`);return{path,type:"file",visibility:"private",size:result.ContentLength||0,lastModified:result.LastModified?new Date(result.LastModified).getTime():Date.now(),mimeType:result.ContentType}}list(path,options={}){return this.createAsyncIterator(path,options.deep||!1)}async*createAsyncIterator(path,deep){const prefix=this.prefixPath(path),normalizedPrefix=prefix?`${prefix}/`:void 0;if(deep){const objects=await(await this.getClient()).listAllObjects({bucket:this.bucket,prefix:normalizedPrefix});for(const obj of objects)yield{path:this.stripPrefix(obj.Key),type:"file"}}else{let continuationToken;do{const result=await(await this.getClient()).listObjects({bucket:this.bucket,prefix:normalizedPrefix,continuationToken});for(const obj of result.objects||[])yield{path:this.stripPrefix(obj.Key),type:"file"};continuationToken=result.nextContinuationToken}while(continuationToken)}}async changeVisibility(path,vis){const key=this.prefixPath(path),acl=vis==="public"?"public-read":"private";await(await this.getClient()).putObjectAcl(this.bucket,key,acl)}async visibility(path){const key=this.prefixPath(path);return((await(await this.getClient()).getObjectAcl(this.bucket,key))?.Grants??[]).some((g)=>g.Grantee?.URI==="http://acs.amazonaws.com/groups/global/AllUsers"&&(g.Permission==="READ"||g.Permission==="FULL_CONTROL"))?"public":"private"}async fileExists(path){const key=this.prefixPath(path);try{return!!await(await this.getClient()).headObject(this.bucket,key)}catch(error){if(!error.message?.includes("404")&&!error.message?.includes("NoSuchKey")&&!error.message?.includes("NotFound"))console.debug(`[s3] Unexpected error checking file existence for ${path}: ${error.message}`);return!1}}async directoryExists(path){const prefix=this.prefixPath(path);return((await(await this.getClient()).listObjects({bucket:this.bucket,prefix:`${prefix}/`,maxKeys:1})).objects||[]).length>0}async publicUrl(path,options={}){const key=this.prefixPath(path);return`${options.domain||`https://${this.bucket}.s3.${this.region}.amazonaws.com`}/${key}`}async temporaryUrl(path,options){const key=this.prefixPath(path),expiresIn=Math.floor(normalizeExpiryToMilliseconds(options.expiresIn)/1000),MIN_EXPIRY=60,MAX_EXPIRY=604800;if(!Number.isFinite(expiresIn)||expiresIn<MIN_EXPIRY||expiresIn>MAX_EXPIRY)throw RangeError(`[storage/s3] temporaryUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);return await(await this.getClient()).getSignedUrl({bucket:this.bucket,key,expiresIn,operation:"getObject"})}async signedUrl(path,options){return this.temporaryUrl(path,{expiresIn:options.expiresIn})}async presignedUploadUrl(options){if(!options.contentType)throw Error("[storage/s3] presignedUploadUrl requires `contentType` \u2014 S3 signs against the exact header.");const expiresIn=Math.floor(options.expiresIn),MIN_EXPIRY=60,MAX_EXPIRY=604800;if(!Number.isFinite(expiresIn)||expiresIn<MIN_EXPIRY||expiresIn>MAX_EXPIRY)throw RangeError(`[storage/s3] presignedUploadUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);const safeDir=sanitizePresignedDir(options.dir),safeFilename=options.filename!==void 0?sanitizePresignedFilename(options.filename):`${crypto.randomUUID().replace(/-/g,"")}${this.extensionForContentType(options.contentType)}`,path=safeDir?`${safeDir}/${safeFilename}`:safeFilename,key=this.prefixPath(path);return{url:await(await this.getClient()).getSignedUrl({bucket:this.bucket,key,expiresIn,operation:"putObject"}),path,key,contentType:options.contentType,maxBytes:options.maxBytes}}async presignedUploadPolicy(options){const credentials=this.resolveCredentials(),scopedKey=typeof options.key==="string"?this.prefixPath(options.key):{startsWith:this.prefixPath(options.key.startsWith)};return signS3PresignedPost({bucket:this.bucket,region:this.region,credentials,key:scopedKey,contentType:options.contentType,contentLengthRange:options.contentLengthRange,acl:options.acl,expiresIn:options.expiresIn,fields:options.fields})}extensionForContentType(contentType){const mime=contentType.toLowerCase().split(";")[0]?.trim()??"";return{"image/jpeg":".jpg","image/jpg":".jpg","image/png":".png","image/webp":".webp","image/gif":".gif","image/avif":".avif","image/svg+xml":".svg","application/pdf":".pdf","application/json":".json","application/zip":".zip","text/plain":".txt","text/csv":".csv","video/mp4":".mp4","video/webm":".webm","audio/mpeg":".mp3","audio/wav":".wav"}[mime]??""}async checksum(path,options={}){const algorithm=options.algorithm||"sha256",content=await this.readToUint8Array(path),hasher=new Bun.CryptoHasher(algorithm);hasher.update(content);return hasher.digest("hex")}async mimeType(path,_options={}){return(await this.stat(path)).mimeType||this.detectMimeType(path)}detectMimeType(path){const ext=basename(path).split(".").pop()?.toLowerCase();return{txt:"text/plain",html:"text/html",css:"text/css",js:"application/javascript",json:"application/json",xml:"application/xml",pdf:"application/pdf",zip:"application/zip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",svg:"image/svg+xml",mp4:"video/mp4",mp3:"audio/mpeg",wav:"audio/wav"}[ext||""]||"application/octet-stream"}async lastModified(path){return(await this.stat(path)).lastModified}async fileSize(path){return(await this.stat(path)).size}}export function createS3Storage(client,config){return new S3StorageAdapter(client,config)}
1
+ import{Buffer}from"node:buffer";import{basename}from"node:path";import{normalizeExpiryToMilliseconds}from"../types";import{sanitizePresignedDir,sanitizePresignedFilename}from"../path-sanitize";import{signS3PresignedPost}from"../s3-presigned-post";import process from"node:process";const S3_MIN_PART_SIZE=5242880,S3_MAX_PART_SIZE=5368709120;function clampPartSize(requested){if(!Number.isFinite(requested))return S3_MIN_PART_SIZE;return Math.max(S3_MIN_PART_SIZE,Math.min(Math.floor(requested),S3_MAX_PART_SIZE))}class ChunkBuffer{chunks=[];total=0;constructor(_partSize){}get length(){return this.total}push(c){this.chunks.push(c);this.total+=c.length}take(n){const out=new Uint8Array(n);let written=0;while(written<n&&this.chunks.length>0){const head=this.chunks[0],need=n-written;if(head.length<=need){out.set(head,written);written+=head.length;this.chunks.shift()}else{out.set(head.subarray(0,need),written);this.chunks[0]=head.subarray(need);written+=need}}this.total-=n;return out}flush(){const out=new Uint8Array(this.total);let off=0;for(const c of this.chunks){out.set(c,off);off+=c.length}this.chunks=[];this.total=0;return out}}async function isSettled(p){const sentinel=Symbol("pending");return await Promise.race([p.then(()=>"settled",()=>"settled"),Promise.resolve(sentinel)])!==sentinel}export function resolveS3ClientOptions(config){const options={};if(config.endpoint)options.endpoint=config.endpoint.replace(/^https?:\/\//i,"").replace(/\/+$/,"");if(config.usePathStyleEndpoint)options.forcePathStyle=!0;if(config.credentials)options.credentials=config.credentials;return Object.keys(options).length>0?options:void 0}export class S3StorageAdapter{_client;_clientPromise=null;bucket;prefix;region;endpoint;usePathStyleEndpoint;credentials;constructor(client,config){this._client=client;this.bucket=config.bucket||"";this.prefix=config.prefix||"";this.region=config.region||"us-east-1";this.credentials=config.credentials;this.endpoint=config.endpoint;this.usePathStyleEndpoint=config.usePathStyleEndpoint;if(!this.bucket)throw Error("S3 bucket name is required")}async getClient(){if(this._client)return this._client;if(!this._clientPromise)this._clientPromise=import("@stacksjs/ts-cloud").then((cloud)=>{this._client=new cloud.S3Client(this.region,void 0,resolveS3ClientOptions({endpoint:this.endpoint,usePathStyleEndpoint:this.usePathStyleEndpoint,credentials:this.credentials}));return this._client});return this._clientPromise}resolveCredentials(){if(this.credentials?.accessKeyId&&this.credentials.secretAccessKey)return this.credentials;const accessKeyId=process.env.AWS_ACCESS_KEY_ID,secretAccessKey=process.env.AWS_SECRET_ACCESS_KEY,sessionToken=process.env.AWS_SESSION_TOKEN;if(!accessKeyId||!secretAccessKey)throw Error("[storage/s3] presignedUploadPolicy requires AWS credentials \u2014 "+"pass them via S3DiskConfig.credentials or set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY.");return{accessKeyId,secretAccessKey,sessionToken}}prefixPath(path){if(!this.prefix)return path;return`${this.prefix}/${path}`.replace(/\/+/g,"/")}stripPrefix(path){if(!this.prefix)return path;const prefixWithSlash=`${this.prefix}/`;return path.startsWith(prefixWithSlash)?path.slice(prefixWithSlash.length):path}async contentsToBuffer(contents){if(typeof contents==="string")return Buffer.from(contents,"utf8");else if(contents instanceof Buffer)return contents;else if(contents instanceof Uint8Array)return Buffer.from(contents);else{if(typeof contents.getReader!=="function")throw TypeError("[storage/s3] contents must be a web-standard ReadableStream (with .getReader()), not a Node stream.Readable. Convert via Readable.toWeb(nodeStream) before passing.");const reader=contents.getReader(),chunks=[];while(!0){const{done,value}=await reader.read();if(done)break;if(value)chunks.push(value)}return Buffer.concat(chunks.map((c)=>Buffer.from(c)))}}async write(path,contents){const key=this.prefixPath(path),body=await this.contentsToBuffer(contents),contentType=this.detectMimeType(path);await(await this.getClient()).putObject({bucket:this.bucket,key,body,contentType});return{path,size:body.length,contentType,lastModified:Date.now()}}async read(path){const key=this.prefixPath(path),response=await(await this.getClient()).getObjectBytes(this.bucket,key);if(!response?.body)throw Error(`Failed to read file: ${path}`);return Buffer.from(response.body)}async getStream(path,_options){const key=this.prefixPath(path),buf=await(await this.getClient()).getObjectBuffer(this.bucket,key);if(!buf)throw Error(`Failed to read file: ${path}`);const bytes=new Uint8Array(buf);return new ReadableStream({start(controller){controller.enqueue(bytes);controller.close()}})}async putStream(path,stream,options){const key=this.prefixPath(path),contentType=options?.contentType??this.detectMimeType(path),partSize=clampPartSize(options?.partSize??5242880),concurrency=Math.max(1,Math.min(options?.concurrency??4,100)),maxRetries=Math.max(0,options?.maxRetries??3),signal=options?.signal,reader=stream.getReader();let firstChunk=null,firstDone=!1;{const buf=new ChunkBuffer(partSize);while(!firstDone&&buf.length<partSize){if(signal?.aborted){try{reader.releaseLock()}catch{}throw Error("aborted")}const{value,done}=await reader.read();if(done){firstDone=!0;break}if(value)buf.push(value)}firstChunk=buf.flush()}if(firstDone){try{reader.releaseLock()}catch{}await(await this.getClient()).putObject({bucket:this.bucket,key,body:Buffer.from(firstChunk),contentType});return{path,size:firstChunk.length,contentType,lastModified:Date.now()}}const{UploadId:uploadId}=await(await this.getClient()).createMultipartUpload(this.bucket,key,{contentType}),completedParts=[];let totalBytes=0,partNumber=1;const inflight=[],uploadOne=async(body,n)=>{let attempt=0;while(!0){if(signal?.aborted)throw Error("aborted");try{const{ETag}=await(await this.getClient()).uploadPart(this.bucket,key,uploadId,n,Buffer.from(body));completedParts.push({PartNumber:n,ETag});totalBytes+=body.length;return}catch(err){if(attempt>=maxRetries)throw err;attempt+=1}}};try{inflight.push(uploadOne(firstChunk,partNumber++));firstChunk=null;const buf=new ChunkBuffer(partSize);while(!0){if(signal?.aborted)throw Error("aborted");const{value,done}=await reader.read();if(done)break;if(value)buf.push(value);while(buf.length>=partSize){const part=buf.take(partSize);if(inflight.length>=concurrency){await Promise.race(inflight.map((p,i)=>p.then(()=>i)));for(let i=inflight.length-1;i>=0;i--)if(await isSettled(inflight[i]))inflight.splice(i,1)}inflight.push(uploadOne(part,partNumber++))}}try{reader.releaseLock()}catch{}const tail=buf.flush();if(tail.length>0)inflight.push(uploadOne(tail,partNumber++));await Promise.all(inflight);completedParts.sort((a,b)=>a.PartNumber-b.PartNumber);await(await this.getClient()).completeMultipartUpload(this.bucket,key,uploadId,completedParts);return{path,size:totalBytes,contentType,lastModified:Date.now()}}catch(err){try{await(await this.getClient()).abortMultipartUpload(this.bucket,key,uploadId)}catch{}throw err}}async readToString(path){const key=this.prefixPath(path),response=await(await this.getClient()).getObject(this.bucket,key);if(!response)throw Error(`Failed to read file: ${path}`);return response}async readToBuffer(path){return await this.read(path)}async readToUint8Array(path){const buffer=await this.readToBuffer(path);return new Uint8Array(buffer)}async deleteFile(path){const key=this.prefixPath(path);await(await this.getClient()).deleteObject(this.bucket,key)}async deleteDirectory(path){const prefix=this.prefixPath(path),normalizedPrefix=prefix.endsWith("/")?prefix:`${prefix}/`,keys=(await(await this.getClient()).listAllObjects({bucket:this.bucket,prefix:normalizedPrefix})).map((obj)=>obj.Key).filter((k)=>typeof k==="string");if(keys.length===0)return;await(await this.getClient()).deleteObjects(this.bucket,keys)}async createDirectory(_path){}async moveFile(from,to){await this.copyFile(from,to);await this.deleteFile(from)}async copyFile(from,to){const fromKey=this.prefixPath(from),toKey=this.prefixPath(to);await(await this.getClient()).copyObject({sourceBucket:this.bucket,sourceKey:fromKey,destinationBucket:this.bucket,destinationKey:toKey})}async stat(path){const key=this.prefixPath(path),result=await(await this.getClient()).headObject(this.bucket,key);if(!result)throw Error(`File not found: ${path}`);return{path,type:"file",visibility:"private",size:result.ContentLength||0,lastModified:result.LastModified?new Date(result.LastModified).getTime():Date.now(),mimeType:result.ContentType}}list(path,options={}){return this.createAsyncIterator(path,options.deep||!1)}async*createAsyncIterator(path,deep){const prefix=this.prefixPath(path),normalizedPrefix=prefix?`${prefix}/`:void 0;if(deep){const objects=await(await this.getClient()).listAllObjects({bucket:this.bucket,prefix:normalizedPrefix});for(const obj of objects)yield{path:this.stripPrefix(obj.Key),type:"file"}}else{let continuationToken;do{const result=await(await this.getClient()).listObjects({bucket:this.bucket,prefix:normalizedPrefix,continuationToken});for(const obj of result.objects||[])yield{path:this.stripPrefix(obj.Key),type:"file"};continuationToken=result.nextContinuationToken}while(continuationToken)}}async changeVisibility(path,vis){const key=this.prefixPath(path),acl=vis==="public"?"public-read":"private";await(await this.getClient()).putObjectAcl(this.bucket,key,acl)}async visibility(path){const key=this.prefixPath(path);return((await(await this.getClient()).getObjectAcl(this.bucket,key))?.Grants??[]).some((g)=>g.Grantee?.URI==="http://acs.amazonaws.com/groups/global/AllUsers"&&(g.Permission==="READ"||g.Permission==="FULL_CONTROL"))?"public":"private"}async fileExists(path){const key=this.prefixPath(path);try{return!!await(await this.getClient()).headObject(this.bucket,key)}catch(error){if(!error.message?.includes("404")&&!error.message?.includes("NoSuchKey")&&!error.message?.includes("NotFound"))console.debug(`[s3] Unexpected error checking file existence for ${path}: ${error.message}`);return!1}}async directoryExists(path){const prefix=this.prefixPath(path);return((await(await this.getClient()).listObjects({bucket:this.bucket,prefix:`${prefix}/`,maxKeys:1})).objects||[]).length>0}async publicUrl(path,options={}){const key=this.prefixPath(path);return`${options.domain||`https://${this.bucket}.s3.${this.region}.amazonaws.com`}/${key}`}async temporaryUrl(path,options){const key=this.prefixPath(path),expiresIn=Math.floor(normalizeExpiryToMilliseconds(options.expiresIn)/1000),MIN_EXPIRY=60,MAX_EXPIRY=604800;if(!Number.isFinite(expiresIn)||expiresIn<MIN_EXPIRY||expiresIn>MAX_EXPIRY)throw RangeError(`[storage/s3] temporaryUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);return await(await this.getClient()).getSignedUrl({bucket:this.bucket,key,expiresIn,operation:"getObject"})}async signedUrl(path,options){return this.temporaryUrl(path,{expiresIn:options.expiresIn})}async presignedUploadUrl(options){if(!options.contentType)throw Error("[storage/s3] presignedUploadUrl requires `contentType` \u2014 S3 signs against the exact header.");const expiresIn=Math.floor(options.expiresIn),MIN_EXPIRY=60,MAX_EXPIRY=604800;if(!Number.isFinite(expiresIn)||expiresIn<MIN_EXPIRY||expiresIn>MAX_EXPIRY)throw RangeError(`[storage/s3] presignedUploadUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);const safeDir=sanitizePresignedDir(options.dir),safeFilename=options.filename!==void 0?sanitizePresignedFilename(options.filename):`${crypto.randomUUID().replace(/-/g,"")}${this.extensionForContentType(options.contentType)}`,path=safeDir?`${safeDir}/${safeFilename}`:safeFilename,key=this.prefixPath(path);return{url:await(await this.getClient()).getSignedUrl({bucket:this.bucket,key,expiresIn,operation:"putObject"}),path,key,contentType:options.contentType,maxBytes:options.maxBytes}}async presignedUploadPolicy(options){const credentials=this.resolveCredentials(),scopedKey=typeof options.key==="string"?this.prefixPath(options.key):{startsWith:this.prefixPath(options.key.startsWith)};return signS3PresignedPost({bucket:this.bucket,region:this.region,credentials,key:scopedKey,contentType:options.contentType,contentLengthRange:options.contentLengthRange,acl:options.acl,expiresIn:options.expiresIn,fields:options.fields})}extensionForContentType(contentType){const mime=contentType.toLowerCase().split(";")[0]?.trim()??"";return{"image/jpeg":".jpg","image/jpg":".jpg","image/png":".png","image/webp":".webp","image/gif":".gif","image/avif":".avif","image/svg+xml":".svg","application/pdf":".pdf","application/json":".json","application/zip":".zip","text/plain":".txt","text/csv":".csv","video/mp4":".mp4","video/webm":".webm","audio/mpeg":".mp3","audio/wav":".wav"}[mime]??""}async checksum(path,options={}){const algorithm=options.algorithm||"sha256",content=await this.readToUint8Array(path),hasher=new Bun.CryptoHasher(algorithm);hasher.update(content);return hasher.digest("hex")}async mimeType(path,_options={}){return(await this.stat(path)).mimeType||this.detectMimeType(path)}detectMimeType(path){const ext=basename(path).split(".").pop()?.toLowerCase();return{txt:"text/plain",html:"text/html",css:"text/css",js:"application/javascript",json:"application/json",xml:"application/xml",pdf:"application/pdf",zip:"application/zip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",svg:"image/svg+xml",mp4:"video/mp4",mp3:"audio/mpeg",wav:"audio/wav"}[ext||""]||"application/octet-stream"}async lastModified(path){return(await this.stat(path)).lastModified}async fileSize(path){return(await this.stat(path)).size}}export function createS3Storage(client,config){return new S3StorageAdapter(client,config)}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/storage",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.351",
5
+ "version": "0.70.353",
6
6
  "description": "The Stacks file system.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -65,11 +65,11 @@
65
65
  "ts-images": "^0.2.8"
66
66
  },
67
67
  "devDependencies": {
68
- "@stacksjs/arrays": "0.70.351",
68
+ "@stacksjs/arrays": "0.70.353",
69
69
  "better-dx": "^0.2.17",
70
- "@stacksjs/error-handling": "0.70.351",
71
- "@stacksjs/path": "0.70.351",
72
- "@stacksjs/strings": "0.70.351",
73
- "@stacksjs/types": "0.70.351"
70
+ "@stacksjs/error-handling": "0.70.353",
71
+ "@stacksjs/path": "0.70.353",
72
+ "@stacksjs/strings": "0.70.353",
73
+ "@stacksjs/types": "0.70.353"
74
74
  }
75
75
  }