@stacksjs/storage 0.74.40 → 0.74.42

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.
package/dist/facade.js CHANGED
@@ -1 +1 @@
1
- import{resolve}from"node:path";import process from"node:process";import{filesystems,app as appConfig}from"@stacksjs/config";import{AzureBlobStorageAdapter}from"./adapters/azure";import{createLocalStorage}from"./adapters/local";import{S3StorageAdapter}from"./adapters/s3";import{parseDiskPath}from"./path-sanitize";import{putUploadedFile}from"./put-file";function buildConfig(){const cwd=process.cwd(),rootDir=filesystems.root||cwd,s3Config=filesystems.s3,appUrl=(appConfig?.url||"").replace(/\/$/,""),config={default:filesystems.driver||"local",disks:{local:{driver:"local",root:resolve(rootDir,"storage/app"),visibility:filesystems.defaultVisibility||"private"},public:{driver:"local",root:resolve(rootDir,"public"),url:appUrl||"/",visibility:"public"}}};if(s3Config?.bucket)config.disks.s3={driver:"s3",bucket:s3Config.bucket,region:s3Config.region||"us-east-1",prefix:s3Config.prefix,endpoint:s3Config.endpoint,url:filesystems.publicUrl?.domain,usePathStyleEndpoint:!!s3Config.endpoint,visibility:filesystems.defaultVisibility||"private",credentials:s3Config.credentials?{key:s3Config.credentials.accessKeyId,secret:s3Config.credentials.secretAccessKey}:void 0};return config}class StorageManager{_config=null;disks=new Map;customConfig=null;get config(){if(!this._config){const builtConfig=buildConfig();this._config=this.customConfig?{default:this.customConfig.default||builtConfig.default,disks:{...builtConfig.disks,...this.customConfig.disks}}:builtConfig}return this._config}init(config){this.customConfig=config;this._config=null;this.disks.clear();return this}disk(name){const diskName=name||this.config.default;if(this.disks.has(diskName))return this.disks.get(diskName);const diskConfig=this.config.disks[diskName];if(!diskConfig){const available=Object.keys(this.config.disks).join(", ");throw Error(`Disk [${diskName}] is not configured. Available: ${available}.${diskName==="s3"?" Set `filesystems.s3.bucket` in config/filesystems.ts - the s3 disk is only built once a bucket is configured.":name===void 0?" This is the DEFAULT disk, from `filesystems.driver`; it has to name one of the disks above.":""}`)}const adapter=this.createAdapter(diskName,diskConfig);this.disks.set(diskName,adapter);return adapter}createAdapter(name,config){switch(config.driver){case"local":return this.createLocalAdapter(config);case"s3":return this.createS3Adapter(name,config);case"azure":return this.createAzureAdapter(config);default:throw Error(`Unsupported driver: ${config.driver}`)}}createLocalAdapter(config){return createLocalStorage({root:config.root,url:config.url})}createS3Adapter(_name,config){return new S3StorageAdapter(null,{bucket:config.bucket,region:config.region,prefix:config.prefix,endpoint:config.endpoint,usePathStyleEndpoint:config.usePathStyleEndpoint,url:config.url,credentials:config.credentials?{accessKeyId:config.credentials.key,secretAccessKey:config.credentials.secret}:void 0})}createAzureAdapter(config){return new AzureBlobStorageAdapter({account:config.account,accountKey:config.accountKey,sasToken:config.sasToken,container:config.container,prefix:config.prefix,url:config.url,endpoint:config.endpoint})}async put(pathOrFile,contentsOrOpts){if(typeof pathOrFile==="string")return this.disk().write(pathOrFile,contentsOrOpts);return putUploadedFile(this,pathOrFile,contentsOrOpts??{})}async stat(path){return this.disk().stat(path)}async getStream(path,options){const adapter=this.disk();if(typeof adapter.getStream!=="function")throw Error(`[storage] disk '${this.config.default}' does not support getStream - adapter is missing the optional method`);return adapter.getStream(path,options)}async putStream(path,stream,options){const adapter=this.disk();if(typeof adapter.putStream!=="function")throw Error(`[storage] disk '${this.config.default}' does not support putStream - adapter is missing the optional method`);return adapter.putStream(path,stream,options)}async copyAcross(source,dest){const src=parseDiskPath(source),dst=parseDiskPath(dest);if(src.disk===dst.disk){const adapter=this.disk(src.disk);await adapter.copyFile(src.path,dst.path);return adapter.stat(dst.path).then((entry)=>({path:dst.path,size:entry.size,contentType:entry.mimeType,lastModified:entry.lastModified}))}const contents=await this.disk(src.disk).read(src.path);return this.disk(dst.disk).write(dst.path,contents)}async moveAcross(source,dest){const src=parseDiskPath(source),result=await this.copyAcross(source,dest);await this.disk(src.disk).deleteFile(src.path);return result}async get(path){return this.disk().readToString(path)}async exists(path){return this.disk().fileExists(path)}async missing(path){return!await this.exists(path)}async delete(path){return this.disk().deleteFile(path)}async copy(from,to){return this.disk().copyFile(from,to)}async move(from,to){return this.disk().moveFile(from,to)}async url(path){return this.disk().publicUrl(path)}async signedUrl(path,options){const adapter=this.disk();if(typeof adapter.signedUrl!=="function")throw Error(`[storage] disk '${this.config.default}' does not support signedUrl`);return adapter.signedUrl(path,options)}async presignedUploadUrl(options){const adapter=this.disk();if(typeof adapter.presignedUploadUrl!=="function")throw Error(`[storage] disk '${this.config.default}' does not support presignedUploadUrl - only S3-style adapters do. Use \`Storage.put(file, opts)\` for local/proxied uploads.`);return adapter.presignedUploadUrl(options)}async presignedUploadPolicy(options){const adapter=this.disk();if(typeof adapter.presignedUploadPolicy!=="function")throw Error(`[storage] disk '${this.config.default}' does not support presignedUploadPolicy - S3-only. Use \`presignedUploadUrl\` for the PUT-form, or \`Storage.put(file, opts)\` for server-proxied uploads.`);return adapter.presignedUploadPolicy(options)}async size(path){return this.disk().fileSize(path)}async lastModified(path){return this.disk().lastModified(path)}async mimeType(path){return this.disk().mimeType(path)}async checksum(path,algorithm){return this.disk().checksum(path,{algorithm})}async makeDirectory(path){return this.disk().createDirectory(path)}async deleteDirectory(path){return this.disk().deleteDirectory(path)}files(path=""){return this.disk().list(path)}allFiles(path=""){return this.disk().list(path,{deep:!0})}configure(name,config){const currentConfig=this.config;currentConfig.disks[name]=config;this.disks.delete(name);return this}setDefaultDisk(name){if(!this.config.disks[name])throw Error(`Disk [${name}] is not configured`);this.config.default=name;return this}getDiskConfig(name){return this.config.disks[name||this.config.default]}getConfiguredDisks(){return Object.keys(this.config.disks)}getDefaultDisk(){return this.config.default}reset(){this._config=null;this.customConfig=null;this.disks.clear();return this}}export const Storage=new StorageManager;export{StorageManager};
1
+ import{resolve}from"node:path";import process from"node:process";import{filesystems,app as appConfig}from"@stacksjs/config";import{AzureBlobStorageAdapter}from"./adapters/azure";import{createLocalStorage}from"./adapters/local";import{S3StorageAdapter}from"./adapters/s3";import{parseDiskPath}from"./path-sanitize";import{putUploadedFile}from"./put-file";function buildConfig(){const cwd=process.cwd(),rootDir=filesystems.root||cwd,s3Config=filesystems.s3,appUrl=(appConfig?.url||"").replace(/\/$/,""),config={default:filesystems.driver||"local",disks:{local:{driver:"local",root:resolve(rootDir,"storage/app"),visibility:filesystems.defaultVisibility||"private"},public:{driver:"local",root:resolve(rootDir,"public"),url:appUrl||"/",visibility:"public"}}};if(s3Config?.bucket)config.disks.s3={driver:"s3",bucket:s3Config.bucket,region:s3Config.region||"us-east-1",prefix:s3Config.prefix,endpoint:s3Config.endpoint,url:filesystems.publicUrl?.domain,usePathStyleEndpoint:s3Config.usePathStyleEndpoint??!!s3Config.endpoint,visibility:filesystems.defaultVisibility||"private",credentials:s3Config.credentials?{key:s3Config.credentials.accessKeyId,secret:s3Config.credentials.secretAccessKey}:void 0};return config}class StorageManager{_config=null;disks=new Map;customConfig=null;get config(){if(!this._config){const builtConfig=buildConfig();this._config=this.customConfig?{default:this.customConfig.default||builtConfig.default,disks:{...builtConfig.disks,...this.customConfig.disks}}:builtConfig}return this._config}init(config){this.customConfig=config;this._config=null;this.disks.clear();return this}disk(name){const diskName=name||this.config.default;if(this.disks.has(diskName))return this.disks.get(diskName);const diskConfig=this.config.disks[diskName];if(!diskConfig){const available=Object.keys(this.config.disks).join(", ");throw Error(`Disk [${diskName}] is not configured. Available: ${available}.${diskName==="s3"?" Set `filesystems.s3.bucket` in config/filesystems.ts - the s3 disk is only built once a bucket is configured.":name===void 0?" This is the DEFAULT disk, from `filesystems.driver`; it has to name one of the disks above.":""}`)}const adapter=this.createAdapter(diskName,diskConfig);this.disks.set(diskName,adapter);return adapter}createAdapter(name,config){switch(config.driver){case"local":return this.createLocalAdapter(config);case"s3":return this.createS3Adapter(name,config);case"azure":return this.createAzureAdapter(config);default:throw Error(`Unsupported driver: ${config.driver}`)}}createLocalAdapter(config){return createLocalStorage({root:config.root,url:config.url})}createS3Adapter(_name,config){return new S3StorageAdapter(null,{bucket:config.bucket,region:config.region,prefix:config.prefix,endpoint:config.endpoint,usePathStyleEndpoint:config.usePathStyleEndpoint,url:config.url,credentials:config.credentials?{accessKeyId:config.credentials.key,secretAccessKey:config.credentials.secret}:void 0})}createAzureAdapter(config){return new AzureBlobStorageAdapter({account:config.account,accountKey:config.accountKey,sasToken:config.sasToken,container:config.container,prefix:config.prefix,url:config.url,endpoint:config.endpoint})}async put(pathOrFile,contentsOrOpts){if(typeof pathOrFile==="string")return this.disk().write(pathOrFile,contentsOrOpts);return putUploadedFile(this,pathOrFile,contentsOrOpts??{})}async stat(path){return this.disk().stat(path)}async getStream(path,options){const adapter=this.disk();if(typeof adapter.getStream!=="function")throw Error(`[storage] disk '${this.config.default}' does not support getStream - adapter is missing the optional method`);return adapter.getStream(path,options)}async putStream(path,stream,options){const adapter=this.disk();if(typeof adapter.putStream!=="function")throw Error(`[storage] disk '${this.config.default}' does not support putStream - adapter is missing the optional method`);return adapter.putStream(path,stream,options)}async copyAcross(source,dest){const src=parseDiskPath(source),dst=parseDiskPath(dest);if(src.disk===dst.disk){const adapter=this.disk(src.disk);await adapter.copyFile(src.path,dst.path);return adapter.stat(dst.path).then((entry)=>({path:dst.path,size:entry.size,contentType:entry.mimeType,lastModified:entry.lastModified}))}const contents=await this.disk(src.disk).read(src.path);return this.disk(dst.disk).write(dst.path,contents)}async moveAcross(source,dest){const src=parseDiskPath(source),result=await this.copyAcross(source,dest);await this.disk(src.disk).deleteFile(src.path);return result}async get(path){return this.disk().readToString(path)}async exists(path){return this.disk().fileExists(path)}async missing(path){return!await this.exists(path)}async delete(path){return this.disk().deleteFile(path)}async copy(from,to){return this.disk().copyFile(from,to)}async move(from,to){return this.disk().moveFile(from,to)}async url(path){return this.disk().publicUrl(path)}async signedUrl(path,options){const adapter=this.disk();if(typeof adapter.signedUrl!=="function")throw Error(`[storage] disk '${this.config.default}' does not support signedUrl`);return adapter.signedUrl(path,options)}async presignedUploadUrl(options){const adapter=this.disk();if(typeof adapter.presignedUploadUrl!=="function")throw Error(`[storage] disk '${this.config.default}' does not support presignedUploadUrl - only S3-style adapters do. Use \`Storage.put(file, opts)\` for local/proxied uploads.`);return adapter.presignedUploadUrl(options)}async presignedUploadPolicy(options){const adapter=this.disk();if(typeof adapter.presignedUploadPolicy!=="function")throw Error(`[storage] disk '${this.config.default}' does not support presignedUploadPolicy - S3-only. Use \`presignedUploadUrl\` for the PUT-form, or \`Storage.put(file, opts)\` for server-proxied uploads.`);return adapter.presignedUploadPolicy(options)}async size(path){return this.disk().fileSize(path)}async lastModified(path){return this.disk().lastModified(path)}async mimeType(path){return this.disk().mimeType(path)}async checksum(path,algorithm){return this.disk().checksum(path,{algorithm})}async makeDirectory(path){return this.disk().createDirectory(path)}async deleteDirectory(path){return this.disk().deleteDirectory(path)}files(path=""){return this.disk().list(path)}allFiles(path=""){return this.disk().list(path,{deep:!0})}configure(name,config){const currentConfig=this.config;currentConfig.disks[name]=config;this.disks.delete(name);return this}setDefaultDisk(name){if(!this.config.disks[name])throw Error(`Disk [${name}] is not configured`);this.config.default=name;return this}getDiskConfig(name){return this.config.disks[name||this.config.default]}getConfiguredDisks(){return Object.keys(this.config.disks)}getDefaultDisk(){return this.config.default}reset(){this._config=null;this.customConfig=null;this.disks.clear();return this}}export const Storage=new StorageManager;export{StorageManager};
@@ -0,0 +1,52 @@
1
+ /** The presentation category for a mime type. */
2
+ export declare function fileKind(mime: string): FileKind;
3
+ /**
4
+ * Whether a preview can be rendered from the bytes alone.
5
+ *
6
+ * `false` does not mean "no preview is possible" - it means one cannot be made
7
+ * here, without a transcoder, a renderer or a headless browser. A caller that
8
+ * has those can still produce one; the point is that this module does not
9
+ * pretend it did.
10
+ *
11
+ * SVG is excluded from previewable despite being an image: rendering
12
+ * attacker-supplied SVG is script execution, and the decision to do it anyway
13
+ * belongs to the caller who knows where the file came from.
14
+ */
15
+ export declare function isPreviewable(mime: string): boolean;
16
+ /** Resolve the mime type from bytes first, then the name. */
17
+ export declare function resolveMime(bytes: Uint8Array, name?: string): { mime: string, source: FileInfo['mimeSource'] };
18
+ /**
19
+ * Describe a file from its bytes.
20
+ *
21
+ * `name` is optional and used only for the extension fallback, so this works
22
+ * for an in-memory upload that has no path yet.
23
+ *
24
+ * Image metadata is read through `ts-images`, imported lazily: a caller asking
25
+ * about a PDF should not pay to load an image decoder. A decode failure is not
26
+ * an error - a truncated or malformed image is still a file with a size and a
27
+ * type, and returning that beats throwing on the one case a file manager most
28
+ * needs to display.
29
+ */
30
+ export declare function fileInfoFromBytes(bytes: Uint8Array, name?: string): Promise<FileInfo>;
31
+ /** Describe a file on disk. */
32
+ export declare function fileInfo(path: string): Promise<FileInfo>;
33
+ export declare interface FileInfo {
34
+ mime: string
35
+ mimeSource: 'magic-bytes' | 'extension' | 'fallback'
36
+ size: number
37
+ kind: FileKind
38
+ previewable: boolean
39
+ image?: ImageInfo
40
+ }
41
+ /** What `ts-images` reports about a decoded image. */
42
+ export declare interface ImageInfo {
43
+ width: number
44
+ height: number
45
+ format: string
46
+ channels?: number
47
+ bitDepth?: number
48
+ hasAlpha?: boolean
49
+ colorSpace?: string
50
+ }
51
+ /** What a UI branches on: how to present the file, not what encodes it. */
52
+ export type FileKind = 'image' | 'video' | 'audio' | 'pdf' | 'text' | 'archive' | 'font' | 'other';
@@ -0,0 +1 @@
1
+ import{extname}from"node:path";import{detectMimeFromMagicBytes}from"./mime-verify";const EXTENSION_MIME={".txt":"text/plain",".md":"text/markdown",".csv":"text/csv",".json":"application/json",".xml":"application/xml",".svg":"image/svg+xml",".html":"text/html",".css":"text/css",".js":"text/javascript",".ts":"text/typescript",".yaml":"application/yaml",".yml":"application/yaml"},KIND_BY_TYPE=[[/^image\//,"image"],[/^video\//,"video"],[/^audio\//,"audio"],[/^font\/|^application\/(?:x-)?font/,"font"],[/^application\/pdf$/,"pdf"],[/^application\/(?:zip|x-tar|gzip|x-7z|x-rar|x-bzip)/,"archive"],[/^text\/|^application\/(?:json|xml|yaml|javascript|typescript)/,"text"]];export function fileKind(mime){for(const[pattern,kind]of KIND_BY_TYPE)if(pattern.test(mime))return kind;return"other"}export function isPreviewable(mime){if(mime==="image/svg+xml")return!1;return fileKind(mime)==="image"||fileKind(mime)==="text"}export function resolveMime(bytes,name){const sniffed=detectMimeFromMagicBytes(bytes);if(sniffed)return{mime:sniffed,source:"magic-bytes"};if(name){const byExtension=EXTENSION_MIME[extname(name).toLowerCase()];if(byExtension)return{mime:byExtension,source:"extension"}}return{mime:"application/octet-stream",source:"fallback"}}export async function fileInfoFromBytes(bytes,name){const{mime,source}=resolveMime(bytes,name),kind=fileKind(mime),info={mime,mimeSource:source,size:bytes.byteLength,kind,previewable:isPreviewable(mime)};if(kind==="image"&&mime!=="image/svg+xml")try{const{getMetadata}=await import("ts-images");info.image=await getMetadata(bytes)}catch{}return info}export async function fileInfo(path){const file=Bun.file(path),bytes=new Uint8Array(await file.arrayBuffer());return fileInfoFromBytes(bytes,path)}
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export type { DiskName, KnownDisks } from './types/filesystem';
8
8
  export type { SignedTokenVerification } from './signed-url';
9
9
  export type { SignedUrlOptions } from './types';
10
10
  export type { ParsedDiskPath } from './path-sanitize';
11
+ export type { FileInfo, FileKind, ImageInfo } from './file-info';
11
12
  export type { MimeVerifyResult } from './mime-verify';
12
13
  export type { S3PresignedPostInput, S3PresignedPostResult } from './s3-presigned-post';
13
14
  export type {
@@ -54,6 +55,7 @@ export { parseDiskPath, PathSanitizeError, sanitizePresignedDir, sanitizePresign
54
55
  // MIME re-verification — server-side check that uploaded bytes match
55
56
  // the claimed content type. Run this after a presigned upload
56
57
  // completes since `Content-Type` on a presigned PUT is caller-attested.
58
+ export { fileInfo, fileInfoFromBytes, fileKind, isPreviewable, resolveMime } from './file-info';
57
59
  export { detectMimeFromMagicBytes, verifyUploadedMime } from './mime-verify';
58
60
  // S3 presigned-POST policy signer (stacksjs/stacks#1888 Phase B).
59
61
  // Exposed as a standalone for callers that have their own S3 client
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./copy";export*from"./delete";export*from"./files";export*from"./folders";export*from"./fs";export*from"./glob";export*from"./hash";export*from"./helpers";export* as storage from"./storage";export*from"./zip";export{ensureBucket,ensureConfiguredBuckets}from"./provision";export*from"./adapters";export*from"./types";export*from"./drivers";export{serveFile}from"./static-serve";export{Storage,StorageManager}from"./facade";export{UploadedFile,uploadedFile,uploadedFiles}from"./uploaded-file";export{azureDisk,backblazeDisk,configFromEnv,filebaseDisk,gcsDisk,hetznerDisk,localDisk,r2Disk,s3Disk}from"./types/filesystem";export{clearRevokedSignedStorageTokens,createSignedStorageToken,isSignedStorageTokenRevoked,revokeSignedStorageToken,verifySignedStorageToken}from"./signed-url";export{parseDiskPath,PathSanitizeError,sanitizePresignedDir,sanitizePresignedFilename}from"./path-sanitize";export{detectMimeFromMagicBytes,verifyUploadedMime}from"./mime-verify";export{signS3PresignedPost}from"./s3-presigned-post";
1
+ export*from"./copy";export*from"./delete";export*from"./files";export*from"./folders";export*from"./fs";export*from"./glob";export*from"./hash";export*from"./helpers";export* as storage from"./storage";export*from"./zip";export{ensureBucket,ensureConfiguredBuckets}from"./provision";export*from"./adapters";export*from"./types";export*from"./drivers";export{serveFile}from"./static-serve";export{Storage,StorageManager}from"./facade";export{UploadedFile,uploadedFile,uploadedFiles}from"./uploaded-file";export{azureDisk,backblazeDisk,configFromEnv,filebaseDisk,gcsDisk,hetznerDisk,localDisk,r2Disk,s3Disk}from"./types/filesystem";export{clearRevokedSignedStorageTokens,createSignedStorageToken,isSignedStorageTokenRevoked,revokeSignedStorageToken,verifySignedStorageToken}from"./signed-url";export{parseDiskPath,PathSanitizeError,sanitizePresignedDir,sanitizePresignedFilename}from"./path-sanitize";export{fileInfo,fileInfoFromBytes,fileKind,isPreviewable,resolveMime}from"./file-info";export{detectMimeFromMagicBytes,verifyUploadedMime}from"./mime-verify";export{signS3PresignedPost}from"./s3-presigned-post";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/storage",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.40",
5
+ "version": "0.74.42",
6
6
  "description": "The Stacks file system.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,20 +61,20 @@
61
61
  "prepublishOnly": "bun run build"
62
62
  },
63
63
  "dependencies": {
64
- "@stacksjs/arrays": "0.74.40",
65
- "@stacksjs/cli": "0.74.40",
66
- "@stacksjs/config": "0.74.40",
67
- "@stacksjs/env": "0.74.40",
68
- "@stacksjs/error-handling": "0.74.40",
69
- "@stacksjs/logging": "0.74.40",
70
- "@stacksjs/path": "0.74.40",
71
- "@stacksjs/strings": "0.74.40",
64
+ "@stacksjs/arrays": "0.74.42",
65
+ "@stacksjs/cli": "0.74.42",
66
+ "@stacksjs/config": "0.74.42",
67
+ "@stacksjs/env": "0.74.42",
68
+ "@stacksjs/error-handling": "0.74.42",
69
+ "@stacksjs/logging": "0.74.42",
70
+ "@stacksjs/path": "0.74.42",
71
+ "@stacksjs/strings": "0.74.42",
72
72
  "@stacksjs/ts-cloud": "^0.16.0",
73
73
  "@stacksjs/ts-xml": "^0.1.0",
74
74
  "ts-images": "^0.2.11"
75
75
  },
76
76
  "devDependencies": {
77
- "@stacksjs/types": "0.74.40",
77
+ "@stacksjs/types": "0.74.42",
78
78
  "better-dx": "^0.2.24"
79
79
  }
80
80
  }