@alepha/bucket-vercel 0.10.5 → 0.10.7

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
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/providers/VercelBlobProvider.ts","../src/providers/VercelFileStorageProvider.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;cAEa,aAAA;cACA;eACC;cACD;;;;cCkBP,yBAAS;yBAIb,aAAA,CAAA;;;EDzBW,UAAA,GAAA,SC4BU,OD5BG,CC4BK,MD5BL,CAAA,OC4BmB,SD5BnB,CAAA,CAAA,CAAA,CAAA;;;;;cCkCb,yBAAA,YAAqC;0BAAX,eAAA,CAChB;;IAdjB,qBAIJ,EAAA,MAAA;EAAA,CAAA;qBAAA,MAAA,EAYwB,MAZxB;qBAJa,IAAA,EAiBS,gBAjBT;EAAA,mBAAA,MAAA,EAkBa,GAlBb,CAAA,MAAA,CAAA;EAAA,mBAAA,aAAA,EAmBkB,aAnBlB;EAAA,mBAAA,eAAA,EAoBoB,mBApBpB;qBAO8B,OAAA,EAaV,aAAA,CAER,cAfkB,CAAA,OAAA,CAAA;aAAd,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAAA;YAAR,QAAA,CAAA,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EA+CtB,QA/CsB,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EAiD1B,OAjD0B,CAAA,MAAA,CAAA;EAAA,QAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6F8B,OA7F9B,CA6FsC,QA7FtC,CAAA;EAMjB,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAA0B,EAAA,MAAA,CAAA,EAoJmB,OApJnB,CAAA,OAAA,CAAA;EAAA,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAuKmB,OAvKnB,CAAA,IAAA,CAAA;;;;;;;;;;cCpB1B,oBAAkB,aAAA,CAAA,QAW7B,aAAA,CAX6B"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/providers/VercelBlobProvider.ts","../src/providers/VercelFileStorageProvider.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;cAEa,aAAA;cACC;eACC;cACD;;;;cCkBR,WAIJ,aAAA,CAJa;yBAIb,aAAA,CAAA;;;EDzBW,UAAA,GAAA,SC4BW,OD5BE,CC4BM,MD5BN,CAAA,OC4BoB,SD5BpB,CAAA,CAAA,CAAA,CACZ;;;;;cCiCD,yBAAA,YAAqC;0BAAX,eAAA,CACf;EAdlB,mBAIJ,GAAA,EAAA;IAJa,qBAAA,EAAA,MAAA;EAAA,CAAA;qBAAA,MAAA,EAgBY,MAhBZ;EAO+B,mBAAA,IAAA,EAUrB,gBAVqB;EAAd,mBAAA,MAAA,EAWH,GAXG,CAAA,MAAA,CAAA;EAAR,mBAAA,aAAA,EAYU,aAZV;EAAO,mBAAA,eAAA,EAaK,mBAbL;EAAA,mBAAA,OAAA,EAaK,aAAA,CAER,cAfG,CAAA,OAAA,CAAA;EAAA,WAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAAA;EAMlB,UAAA,QAAA,CAAA,CAAA,EAAA,MAA0B;EAAA,MAAA,CAAA,UACf,EAAA,MAAA,EAAA,IAAA,EAwCd,QAxCc,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EA0CnB,OA1CmB,CAAA,MAAA,CAAA;EAEG,QAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAoFkC,OApFlC,CAoF0C,QApF1C,CAAA;EACF,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAgJkC,OAhJlC,CAAA,OAAA,CAAA;EACI,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAkK8B,OAlK9B,CAAA,IAAA,CAAA;;;;;;;;;;cCzBhB,oBAAkB,aAAA,CAAA,QAW7B,aAAA,CAX6B"}
package/dist/index.js CHANGED
@@ -48,8 +48,7 @@ var VercelFileStorageProvider = class {
48
48
  async upload(bucketName, file, fileId) {
49
49
  fileId ??= this.createId();
50
50
  this.log.trace(`Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`);
51
- const storeName = this.convertName(bucketName);
52
- const pathname = `${storeName}/${fileId}`;
51
+ const pathname = `${this.convertName(bucketName)}/${fileId}`;
53
52
  try {
54
53
  const contentBuffer = Buffer.from(await file.arrayBuffer());
55
54
  const fileBuffer = this.metadataService.createFileBuffer(file, contentBuffer);
@@ -69,8 +68,7 @@ var VercelFileStorageProvider = class {
69
68
  }
70
69
  async download(bucketName, fileId) {
71
70
  this.log.trace(`Downloading file '${fileId}' from bucket '${bucketName}'...`);
72
- const storeName = this.convertName(bucketName);
73
- const pathname = `${storeName}/${fileId}`;
71
+ const pathname = `${this.convertName(bucketName)}/${fileId}`;
74
72
  try {
75
73
  const headResult = await this.vercelBlobApi.head(pathname, { token: this.env.BLOB_READ_WRITE_TOKEN });
76
74
  if (!headResult) throw new FileNotFoundError(`File '${fileId}' not found in bucket '${bucketName}'`);
@@ -95,19 +93,16 @@ var VercelFileStorageProvider = class {
95
93
  }
96
94
  async exists(bucketName, fileId) {
97
95
  this.log.trace(`Checking existence of file '${fileId}' in bucket '${bucketName}'...`);
98
- const storeName = this.convertName(bucketName);
99
- const pathname = `${storeName}/${fileId}`;
96
+ const pathname = `${this.convertName(bucketName)}/${fileId}`;
100
97
  try {
101
- const result = await this.vercelBlobApi.head(pathname, { token: this.env.BLOB_READ_WRITE_TOKEN });
102
- return result !== null;
98
+ return await this.vercelBlobApi.head(pathname, { token: this.env.BLOB_READ_WRITE_TOKEN }) !== null;
103
99
  } catch (error) {
104
100
  return false;
105
101
  }
106
102
  }
107
103
  async delete(bucketName, fileId) {
108
104
  this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);
109
- const storeName = this.convertName(bucketName);
110
- const pathname = `${storeName}/${fileId}`;
105
+ const pathname = `${this.convertName(bucketName)}/${fileId}`;
111
106
  try {
112
107
  await this.vercelBlobApi.del(pathname, { token: this.env.BLOB_READ_WRITE_TOKEN });
113
108
  } catch (error) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/providers/VercelBlobProvider.ts","../src/providers/VercelFileStorageProvider.ts","../src/index.ts"],"sourcesContent":["import { del, head, put } from \"@vercel/blob\";\n\nexport class VercelBlobApi {\n\tput: typeof put = put;\n\thead: typeof head = head;\n\tdel: typeof del = del;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Readable } from \"node:stream\";\nimport {\n\t$bucket,\n\tFileMetadataService,\n\tFileNotFoundError,\n\ttype FileStorageProvider,\n} from \"@alepha/bucket\";\nimport {\n\t$env,\n\t$hook,\n\t$inject,\n\tAlepha,\n\tAlephaError,\n\ttype FileLike,\n\ttype Static,\n\tt,\n} from \"@alepha/core\";\nimport { DateTimeProvider } from \"@alepha/datetime\";\nimport { createFile } from \"@alepha/file\";\nimport { $logger } from \"@alepha/logger\";\nimport { VercelBlobApi } from \"./VercelBlobProvider.ts\";\n\nconst envSchema = t.object({\n\tBLOB_READ_WRITE_TOKEN: t.text({\n\t\tsize: \"long\",\n\t}),\n});\n\ndeclare module \"@alepha/core\" {\n\tinterface Env extends Partial<Static<typeof envSchema>> {}\n}\n\n/**\n * Vercel Blob Storage implementation of File Storage Provider.\n */\nexport class VercelFileStorageProvider implements FileStorageProvider {\n\tprotected readonly log = $logger();\n\tprotected readonly env = $env(envSchema);\n\tprotected readonly alepha = $inject(Alepha);\n\tprotected readonly time = $inject(DateTimeProvider);\n\tprotected readonly stores: Set<string> = new Set();\n\tprotected readonly vercelBlobApi = $inject(VercelBlobApi);\n\tprotected readonly metadataService = $inject(FileMetadataService);\n\n\tprotected readonly onStart = $hook({\n\t\ton: \"start\",\n\t\thandler: async () => {\n\t\t\tfor (const bucket of this.alepha.descriptors($bucket)) {\n\t\t\t\tif (bucket.provider !== this) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst storeName = this.convertName(bucket.name);\n\n\t\t\t\tthis.log.debug(`Prepare store '${storeName}' ...`);\n\n\t\t\t\t// Vercel Blob doesn't require explicit store/container creation\n\t\t\t\t// We just track the store names for reference\n\t\t\t\tthis.stores.add(storeName);\n\n\t\t\t\tthis.log.info(`Store '${bucket.name}' OK`);\n\t\t\t}\n\t\t},\n\t});\n\n\tpublic convertName(name: string): string {\n\t\t// Convert to a valid path-like name for Vercel Blob\n\t\treturn name.replaceAll(\"/\", \"-\").toLowerCase();\n\t}\n\n\tprotected createId(): string {\n\t\treturn randomUUID();\n\t}\n\n\tpublic async upload(\n\t\tbucketName: string,\n\t\tfile: FileLike,\n\t\tfileId?: string,\n\t): Promise<string> {\n\t\tfileId ??= this.createId();\n\n\t\tthis.log.trace(\n\t\t\t`Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,\n\t\t);\n\n\t\tconst storeName = this.convertName(bucketName);\n\t\tconst pathname = `${storeName}/${fileId}`;\n\n\t\ttry {\n\t\t\t// Create a buffer with metadata and content\n\t\t\tconst contentBuffer = Buffer.from(await file.arrayBuffer());\n\t\t\tconst fileBuffer = this.metadataService.createFileBuffer(\n\t\t\t\tfile,\n\t\t\t\tcontentBuffer,\n\t\t\t);\n\n\t\t\t// Upload the complete buffer (metadata + content) to Vercel Blob\n\t\t\tconst result = await this.vercelBlobApi.put(\n\t\t\t\tpathname,\n\t\t\t\tfileBuffer as unknown as Readable,\n\t\t\t\t{\n\t\t\t\t\taccess: \"public\",\n\t\t\t\t\tcontentType: file.type || \"application/octet-stream\",\n\t\t\t\t\ttoken: this.env.BLOB_READ_WRITE_TOKEN,\n\t\t\t\t\tallowOverwrite: true,\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis.log.trace(`File uploaded successfully: ${result.url}`);\n\t\t\treturn fileId;\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Failed to upload file: ${error}`);\n\t\t\tif (error instanceof Error) {\n\t\t\t\tthrow new AlephaError(`Upload failed: ${error.message}`, {\n\t\t\t\t\tcause: error,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tpublic async download(bucketName: string, fileId: string): Promise<FileLike> {\n\t\tthis.log.trace(\n\t\t\t`Downloading file '${fileId}' from bucket '${bucketName}'...`,\n\t\t);\n\n\t\tconst storeName = this.convertName(bucketName);\n\t\tconst pathname = `${storeName}/${fileId}`;\n\n\t\ttry {\n\t\t\t// check if the file exists and get metadata\n\t\t\tconst headResult = await this.vercelBlobApi.head(pathname, {\n\t\t\t\ttoken: this.env.BLOB_READ_WRITE_TOKEN,\n\t\t\t});\n\n\t\t\tif (!headResult) {\n\t\t\t\tthrow new FileNotFoundError(\n\t\t\t\t\t`File '${fileId}' not found in bucket '${bucketName}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// fetch the actual file content (with metadata)\n\t\t\tconst response = await fetch(headResult.url);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new FileNotFoundError(\n\t\t\t\t\t`Failed to fetch file: ${response.statusText}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst arrayBuffer = await response.arrayBuffer();\n\t\t\tif (!arrayBuffer) {\n\t\t\t\tthrow new FileNotFoundError(\"File not found - empty response body\");\n\t\t\t}\n\n\t\t\t// Decode metadata from the buffer\n\t\t\tconst buffer = Buffer.from(arrayBuffer);\n\t\t\tconst { metadata, contentStart } =\n\t\t\t\tthis.metadataService.decodeMetadataFromBuffer(buffer);\n\n\t\t\t// Extract the actual content\n\t\t\tconst content = buffer.subarray(contentStart);\n\n\t\t\treturn createFile(content, {\n\t\t\t\tname: metadata.name,\n\t\t\t\ttype: metadata.type,\n\t\t\t\tsize: content.length,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof FileNotFoundError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tthis.log.error(`Failed to download file: ${error}`);\n\t\t\tif (error instanceof Error) {\n\t\t\t\tthrow new FileNotFoundError(\"Error downloading file\", { cause: error });\n\t\t\t}\n\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tpublic async exists(bucketName: string, fileId: string): Promise<boolean> {\n\t\tthis.log.trace(\n\t\t\t`Checking existence of file '${fileId}' in bucket '${bucketName}'...`,\n\t\t);\n\n\t\tconst storeName = this.convertName(bucketName);\n\t\tconst pathname = `${storeName}/${fileId}`;\n\n\t\ttry {\n\t\t\tconst result = await this.vercelBlobApi.head(pathname, {\n\t\t\t\ttoken: this.env.BLOB_READ_WRITE_TOKEN,\n\t\t\t});\n\t\t\treturn result !== null;\n\t\t} catch (error) {\n\t\t\t// Vercel Blob head() throws for non-existent files\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tpublic async delete(bucketName: string, fileId: string): Promise<void> {\n\t\tthis.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);\n\n\t\tconst storeName = this.convertName(bucketName);\n\t\tconst pathname = `${storeName}/${fileId}`;\n\n\t\ttry {\n\t\t\tawait this.vercelBlobApi.del(pathname, {\n\t\t\t\ttoken: this.env.BLOB_READ_WRITE_TOKEN,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Failed to delete file: ${error}`);\n\t\t\tif (error instanceof Error) {\n\t\t\t\tthrow new FileNotFoundError(\"Error deleting file\", { cause: error });\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","import { AlephaBucket, FileStorageProvider } from \"@alepha/bucket\";\nimport { $module } from \"@alepha/core\";\nimport { VercelFileStorageProvider } from \"./providers/VercelFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/VercelFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Bucket that provides Vercel Blob Storage capabilities.\n *\n * @see {@link VercelFileStorageProvider}\n * @module alepha.bucket.vercel\n */\nexport const AlephaBucketVercel = $module({\n\tname: \"alepha.bucket.vercel\",\n\tservices: [VercelFileStorageProvider],\n\tregister: (alepha) =>\n\t\talepha\n\t\t\t.with({\n\t\t\t\toptional: true,\n\t\t\t\tprovide: FileStorageProvider,\n\t\t\t\tuse: VercelFileStorageProvider,\n\t\t\t})\n\t\t\t.with(AlephaBucket),\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/providers/VercelBlobProvider.ts","../src/providers/VercelFileStorageProvider.ts","../src/index.ts"],"sourcesContent":["import { del, head, put } from \"@vercel/blob\";\n\nexport class VercelBlobApi {\n put: typeof put = put;\n head: typeof head = head;\n del: typeof del = del;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Readable } from \"node:stream\";\nimport {\n $bucket,\n FileMetadataService,\n FileNotFoundError,\n type FileStorageProvider,\n} from \"@alepha/bucket\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"@alepha/core\";\nimport { DateTimeProvider } from \"@alepha/datetime\";\nimport { createFile } from \"@alepha/file\";\nimport { $logger } from \"@alepha/logger\";\nimport { VercelBlobApi } from \"./VercelBlobProvider.ts\";\n\nconst envSchema = t.object({\n BLOB_READ_WRITE_TOKEN: t.text({\n size: \"long\",\n }),\n});\n\ndeclare module \"@alepha/core\" {\n interface Env extends Partial<Static<typeof envSchema>> {}\n}\n\n/**\n * Vercel Blob Storage implementation of File Storage Provider.\n */\nexport class VercelFileStorageProvider implements FileStorageProvider {\n protected readonly log = $logger();\n protected readonly env = $env(envSchema);\n protected readonly alepha = $inject(Alepha);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly stores: Set<string> = new Set();\n protected readonly vercelBlobApi = $inject(VercelBlobApi);\n protected readonly metadataService = $inject(FileMetadataService);\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n for (const bucket of this.alepha.descriptors($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n const storeName = this.convertName(bucket.name);\n\n this.log.debug(`Prepare store '${storeName}' ...`);\n\n // Vercel Blob doesn't require explicit store/container creation\n // We just track the store names for reference\n this.stores.add(storeName);\n\n this.log.info(`Store '${bucket.name}' OK`);\n }\n },\n });\n\n public convertName(name: string): string {\n // Convert to a valid path-like name for Vercel Blob\n return name.replaceAll(\"/\", \"-\").toLowerCase();\n }\n\n protected createId(): string {\n return randomUUID();\n }\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n this.log.trace(\n `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,\n );\n\n const storeName = this.convertName(bucketName);\n const pathname = `${storeName}/${fileId}`;\n\n try {\n // Create a buffer with metadata and content\n const contentBuffer = Buffer.from(await file.arrayBuffer());\n const fileBuffer = this.metadataService.createFileBuffer(\n file,\n contentBuffer,\n );\n\n // Upload the complete buffer (metadata + content) to Vercel Blob\n const result = await this.vercelBlobApi.put(\n pathname,\n fileBuffer as unknown as Readable,\n {\n access: \"public\",\n contentType: file.type || \"application/octet-stream\",\n token: this.env.BLOB_READ_WRITE_TOKEN,\n allowOverwrite: true,\n },\n );\n\n this.log.trace(`File uploaded successfully: ${result.url}`);\n return fileId;\n } catch (error) {\n this.log.error(`Failed to upload file: ${error}`);\n if (error instanceof Error) {\n throw new AlephaError(`Upload failed: ${error.message}`, {\n cause: error,\n });\n }\n\n throw error;\n }\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n this.log.trace(\n `Downloading file '${fileId}' from bucket '${bucketName}'...`,\n );\n\n const storeName = this.convertName(bucketName);\n const pathname = `${storeName}/${fileId}`;\n\n try {\n // check if the file exists and get metadata\n const headResult = await this.vercelBlobApi.head(pathname, {\n token: this.env.BLOB_READ_WRITE_TOKEN,\n });\n\n if (!headResult) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'`,\n );\n }\n\n // fetch the actual file content (with metadata)\n const response = await fetch(headResult.url);\n\n if (!response.ok) {\n throw new FileNotFoundError(\n `Failed to fetch file: ${response.statusText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n if (!arrayBuffer) {\n throw new FileNotFoundError(\"File not found - empty response body\");\n }\n\n // Decode metadata from the buffer\n const buffer = Buffer.from(arrayBuffer);\n const { metadata, contentStart } =\n this.metadataService.decodeMetadataFromBuffer(buffer);\n\n // Extract the actual content\n const content = buffer.subarray(contentStart);\n\n return createFile(content, {\n name: metadata.name,\n type: metadata.type,\n size: content.length,\n });\n } catch (error) {\n if (error instanceof FileNotFoundError) {\n throw error;\n }\n\n this.log.error(`Failed to download file: ${error}`);\n if (error instanceof Error) {\n throw new FileNotFoundError(\"Error downloading file\", { cause: error });\n }\n\n throw error;\n }\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n this.log.trace(\n `Checking existence of file '${fileId}' in bucket '${bucketName}'...`,\n );\n\n const storeName = this.convertName(bucketName);\n const pathname = `${storeName}/${fileId}`;\n\n try {\n const result = await this.vercelBlobApi.head(pathname, {\n token: this.env.BLOB_READ_WRITE_TOKEN,\n });\n return result !== null;\n } catch (error) {\n // Vercel Blob head() throws for non-existent files\n return false;\n }\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);\n\n const storeName = this.convertName(bucketName);\n const pathname = `${storeName}/${fileId}`;\n\n try {\n await this.vercelBlobApi.del(pathname, {\n token: this.env.BLOB_READ_WRITE_TOKEN,\n });\n } catch (error) {\n this.log.error(`Failed to delete file: ${error}`);\n if (error instanceof Error) {\n throw new FileNotFoundError(\"Error deleting file\", { cause: error });\n }\n throw error;\n }\n }\n}\n","import { AlephaBucket, FileStorageProvider } from \"@alepha/bucket\";\nimport { $module } from \"@alepha/core\";\nimport { VercelFileStorageProvider } from \"./providers/VercelFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/VercelFileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Bucket that provides Vercel Blob Storage capabilities.\n *\n * @see {@link VercelFileStorageProvider}\n * @module alepha.bucket.vercel\n */\nexport const AlephaBucketVercel = $module({\n name: \"alepha.bucket.vercel\",\n services: [VercelFileStorageProvider],\n register: (alepha) =>\n alepha\n .with({\n optional: true,\n provide: FileStorageProvider,\n use: VercelFileStorageProvider,\n })\n .with(AlephaBucket),\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "blob"
11
11
  ],
12
12
  "author": "Feunard",
13
- "version": "0.10.5",
13
+ "version": "0.10.7",
14
14
  "type": "module",
15
15
  "engines": {
16
16
  "node": ">=22.0.0"
@@ -23,17 +23,17 @@
23
23
  "src"
24
24
  ],
25
25
  "dependencies": {
26
- "@alepha/bucket": "0.10.5",
27
- "@alepha/core": "0.10.5",
28
- "@alepha/datetime": "0.10.5",
29
- "@alepha/file": "0.10.5",
30
- "@alepha/logger": "0.10.5",
26
+ "@alepha/bucket": "0.10.7",
27
+ "@alepha/core": "0.10.7",
28
+ "@alepha/datetime": "0.10.7",
29
+ "@alepha/file": "0.10.7",
30
+ "@alepha/logger": "0.10.7",
31
31
  "@vercel/blob": "^2.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@biomejs/biome": "^2.2.5",
35
- "@types/node": "^24.7.1",
36
- "tsdown": "^0.15.6",
34
+ "@biomejs/biome": "^2.2.6",
35
+ "@types/node": "^24.9.0",
36
+ "tsdown": "^0.15.9",
37
37
  "typescript": "^5.9.3",
38
38
  "vitest": "^3.2.4"
39
39
  },
package/src/index.ts CHANGED
@@ -15,14 +15,14 @@ export * from "./providers/VercelFileStorageProvider.ts";
15
15
  * @module alepha.bucket.vercel
16
16
  */
17
17
  export const AlephaBucketVercel = $module({
18
- name: "alepha.bucket.vercel",
19
- services: [VercelFileStorageProvider],
20
- register: (alepha) =>
21
- alepha
22
- .with({
23
- optional: true,
24
- provide: FileStorageProvider,
25
- use: VercelFileStorageProvider,
26
- })
27
- .with(AlephaBucket),
18
+ name: "alepha.bucket.vercel",
19
+ services: [VercelFileStorageProvider],
20
+ register: (alepha) =>
21
+ alepha
22
+ .with({
23
+ optional: true,
24
+ provide: FileStorageProvider,
25
+ use: VercelFileStorageProvider,
26
+ })
27
+ .with(AlephaBucket),
28
28
  });
@@ -1,7 +1,7 @@
1
1
  import { del, head, put } from "@vercel/blob";
2
2
 
3
3
  export class VercelBlobApi {
4
- put: typeof put = put;
5
- head: typeof head = head;
6
- del: typeof del = del;
4
+ put: typeof put = put;
5
+ head: typeof head = head;
6
+ del: typeof del = del;
7
7
  }
@@ -1,20 +1,20 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { Readable } from "node:stream";
3
3
  import {
4
- $bucket,
5
- FileMetadataService,
6
- FileNotFoundError,
7
- type FileStorageProvider,
4
+ $bucket,
5
+ FileMetadataService,
6
+ FileNotFoundError,
7
+ type FileStorageProvider,
8
8
  } from "@alepha/bucket";
9
9
  import {
10
- $env,
11
- $hook,
12
- $inject,
13
- Alepha,
14
- AlephaError,
15
- type FileLike,
16
- type Static,
17
- t,
10
+ $env,
11
+ $hook,
12
+ $inject,
13
+ Alepha,
14
+ AlephaError,
15
+ type FileLike,
16
+ type Static,
17
+ t,
18
18
  } from "@alepha/core";
19
19
  import { DateTimeProvider } from "@alepha/datetime";
20
20
  import { createFile } from "@alepha/file";
@@ -22,201 +22,201 @@ import { $logger } from "@alepha/logger";
22
22
  import { VercelBlobApi } from "./VercelBlobProvider.ts";
23
23
 
24
24
  const envSchema = t.object({
25
- BLOB_READ_WRITE_TOKEN: t.text({
26
- size: "long",
27
- }),
25
+ BLOB_READ_WRITE_TOKEN: t.text({
26
+ size: "long",
27
+ }),
28
28
  });
29
29
 
30
30
  declare module "@alepha/core" {
31
- interface Env extends Partial<Static<typeof envSchema>> {}
31
+ interface Env extends Partial<Static<typeof envSchema>> {}
32
32
  }
33
33
 
34
34
  /**
35
35
  * Vercel Blob Storage implementation of File Storage Provider.
36
36
  */
37
37
  export class VercelFileStorageProvider implements FileStorageProvider {
38
- protected readonly log = $logger();
39
- protected readonly env = $env(envSchema);
40
- protected readonly alepha = $inject(Alepha);
41
- protected readonly time = $inject(DateTimeProvider);
42
- protected readonly stores: Set<string> = new Set();
43
- protected readonly vercelBlobApi = $inject(VercelBlobApi);
44
- protected readonly metadataService = $inject(FileMetadataService);
45
-
46
- protected readonly onStart = $hook({
47
- on: "start",
48
- handler: async () => {
49
- for (const bucket of this.alepha.descriptors($bucket)) {
50
- if (bucket.provider !== this) {
51
- continue;
52
- }
53
-
54
- const storeName = this.convertName(bucket.name);
55
-
56
- this.log.debug(`Prepare store '${storeName}' ...`);
57
-
58
- // Vercel Blob doesn't require explicit store/container creation
59
- // We just track the store names for reference
60
- this.stores.add(storeName);
61
-
62
- this.log.info(`Store '${bucket.name}' OK`);
63
- }
64
- },
65
- });
66
-
67
- public convertName(name: string): string {
68
- // Convert to a valid path-like name for Vercel Blob
69
- return name.replaceAll("/", "-").toLowerCase();
70
- }
71
-
72
- protected createId(): string {
73
- return randomUUID();
74
- }
75
-
76
- public async upload(
77
- bucketName: string,
78
- file: FileLike,
79
- fileId?: string,
80
- ): Promise<string> {
81
- fileId ??= this.createId();
82
-
83
- this.log.trace(
84
- `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,
85
- );
86
-
87
- const storeName = this.convertName(bucketName);
88
- const pathname = `${storeName}/${fileId}`;
89
-
90
- try {
91
- // Create a buffer with metadata and content
92
- const contentBuffer = Buffer.from(await file.arrayBuffer());
93
- const fileBuffer = this.metadataService.createFileBuffer(
94
- file,
95
- contentBuffer,
96
- );
97
-
98
- // Upload the complete buffer (metadata + content) to Vercel Blob
99
- const result = await this.vercelBlobApi.put(
100
- pathname,
101
- fileBuffer as unknown as Readable,
102
- {
103
- access: "public",
104
- contentType: file.type || "application/octet-stream",
105
- token: this.env.BLOB_READ_WRITE_TOKEN,
106
- allowOverwrite: true,
107
- },
108
- );
109
-
110
- this.log.trace(`File uploaded successfully: ${result.url}`);
111
- return fileId;
112
- } catch (error) {
113
- this.log.error(`Failed to upload file: ${error}`);
114
- if (error instanceof Error) {
115
- throw new AlephaError(`Upload failed: ${error.message}`, {
116
- cause: error,
117
- });
118
- }
119
-
120
- throw error;
121
- }
122
- }
123
-
124
- public async download(bucketName: string, fileId: string): Promise<FileLike> {
125
- this.log.trace(
126
- `Downloading file '${fileId}' from bucket '${bucketName}'...`,
127
- );
128
-
129
- const storeName = this.convertName(bucketName);
130
- const pathname = `${storeName}/${fileId}`;
131
-
132
- try {
133
- // check if the file exists and get metadata
134
- const headResult = await this.vercelBlobApi.head(pathname, {
135
- token: this.env.BLOB_READ_WRITE_TOKEN,
136
- });
137
-
138
- if (!headResult) {
139
- throw new FileNotFoundError(
140
- `File '${fileId}' not found in bucket '${bucketName}'`,
141
- );
142
- }
143
-
144
- // fetch the actual file content (with metadata)
145
- const response = await fetch(headResult.url);
146
-
147
- if (!response.ok) {
148
- throw new FileNotFoundError(
149
- `Failed to fetch file: ${response.statusText}`,
150
- );
151
- }
152
-
153
- const arrayBuffer = await response.arrayBuffer();
154
- if (!arrayBuffer) {
155
- throw new FileNotFoundError("File not found - empty response body");
156
- }
157
-
158
- // Decode metadata from the buffer
159
- const buffer = Buffer.from(arrayBuffer);
160
- const { metadata, contentStart } =
161
- this.metadataService.decodeMetadataFromBuffer(buffer);
162
-
163
- // Extract the actual content
164
- const content = buffer.subarray(contentStart);
165
-
166
- return createFile(content, {
167
- name: metadata.name,
168
- type: metadata.type,
169
- size: content.length,
170
- });
171
- } catch (error) {
172
- if (error instanceof FileNotFoundError) {
173
- throw error;
174
- }
175
-
176
- this.log.error(`Failed to download file: ${error}`);
177
- if (error instanceof Error) {
178
- throw new FileNotFoundError("Error downloading file", { cause: error });
179
- }
180
-
181
- throw error;
182
- }
183
- }
184
-
185
- public async exists(bucketName: string, fileId: string): Promise<boolean> {
186
- this.log.trace(
187
- `Checking existence of file '${fileId}' in bucket '${bucketName}'...`,
188
- );
189
-
190
- const storeName = this.convertName(bucketName);
191
- const pathname = `${storeName}/${fileId}`;
192
-
193
- try {
194
- const result = await this.vercelBlobApi.head(pathname, {
195
- token: this.env.BLOB_READ_WRITE_TOKEN,
196
- });
197
- return result !== null;
198
- } catch (error) {
199
- // Vercel Blob head() throws for non-existent files
200
- return false;
201
- }
202
- }
203
-
204
- public async delete(bucketName: string, fileId: string): Promise<void> {
205
- this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);
206
-
207
- const storeName = this.convertName(bucketName);
208
- const pathname = `${storeName}/${fileId}`;
209
-
210
- try {
211
- await this.vercelBlobApi.del(pathname, {
212
- token: this.env.BLOB_READ_WRITE_TOKEN,
213
- });
214
- } catch (error) {
215
- this.log.error(`Failed to delete file: ${error}`);
216
- if (error instanceof Error) {
217
- throw new FileNotFoundError("Error deleting file", { cause: error });
218
- }
219
- throw error;
220
- }
221
- }
38
+ protected readonly log = $logger();
39
+ protected readonly env = $env(envSchema);
40
+ protected readonly alepha = $inject(Alepha);
41
+ protected readonly time = $inject(DateTimeProvider);
42
+ protected readonly stores: Set<string> = new Set();
43
+ protected readonly vercelBlobApi = $inject(VercelBlobApi);
44
+ protected readonly metadataService = $inject(FileMetadataService);
45
+
46
+ protected readonly onStart = $hook({
47
+ on: "start",
48
+ handler: async () => {
49
+ for (const bucket of this.alepha.descriptors($bucket)) {
50
+ if (bucket.provider !== this) {
51
+ continue;
52
+ }
53
+
54
+ const storeName = this.convertName(bucket.name);
55
+
56
+ this.log.debug(`Prepare store '${storeName}' ...`);
57
+
58
+ // Vercel Blob doesn't require explicit store/container creation
59
+ // We just track the store names for reference
60
+ this.stores.add(storeName);
61
+
62
+ this.log.info(`Store '${bucket.name}' OK`);
63
+ }
64
+ },
65
+ });
66
+
67
+ public convertName(name: string): string {
68
+ // Convert to a valid path-like name for Vercel Blob
69
+ return name.replaceAll("/", "-").toLowerCase();
70
+ }
71
+
72
+ protected createId(): string {
73
+ return randomUUID();
74
+ }
75
+
76
+ public async upload(
77
+ bucketName: string,
78
+ file: FileLike,
79
+ fileId?: string,
80
+ ): Promise<string> {
81
+ fileId ??= this.createId();
82
+
83
+ this.log.trace(
84
+ `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,
85
+ );
86
+
87
+ const storeName = this.convertName(bucketName);
88
+ const pathname = `${storeName}/${fileId}`;
89
+
90
+ try {
91
+ // Create a buffer with metadata and content
92
+ const contentBuffer = Buffer.from(await file.arrayBuffer());
93
+ const fileBuffer = this.metadataService.createFileBuffer(
94
+ file,
95
+ contentBuffer,
96
+ );
97
+
98
+ // Upload the complete buffer (metadata + content) to Vercel Blob
99
+ const result = await this.vercelBlobApi.put(
100
+ pathname,
101
+ fileBuffer as unknown as Readable,
102
+ {
103
+ access: "public",
104
+ contentType: file.type || "application/octet-stream",
105
+ token: this.env.BLOB_READ_WRITE_TOKEN,
106
+ allowOverwrite: true,
107
+ },
108
+ );
109
+
110
+ this.log.trace(`File uploaded successfully: ${result.url}`);
111
+ return fileId;
112
+ } catch (error) {
113
+ this.log.error(`Failed to upload file: ${error}`);
114
+ if (error instanceof Error) {
115
+ throw new AlephaError(`Upload failed: ${error.message}`, {
116
+ cause: error,
117
+ });
118
+ }
119
+
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ public async download(bucketName: string, fileId: string): Promise<FileLike> {
125
+ this.log.trace(
126
+ `Downloading file '${fileId}' from bucket '${bucketName}'...`,
127
+ );
128
+
129
+ const storeName = this.convertName(bucketName);
130
+ const pathname = `${storeName}/${fileId}`;
131
+
132
+ try {
133
+ // check if the file exists and get metadata
134
+ const headResult = await this.vercelBlobApi.head(pathname, {
135
+ token: this.env.BLOB_READ_WRITE_TOKEN,
136
+ });
137
+
138
+ if (!headResult) {
139
+ throw new FileNotFoundError(
140
+ `File '${fileId}' not found in bucket '${bucketName}'`,
141
+ );
142
+ }
143
+
144
+ // fetch the actual file content (with metadata)
145
+ const response = await fetch(headResult.url);
146
+
147
+ if (!response.ok) {
148
+ throw new FileNotFoundError(
149
+ `Failed to fetch file: ${response.statusText}`,
150
+ );
151
+ }
152
+
153
+ const arrayBuffer = await response.arrayBuffer();
154
+ if (!arrayBuffer) {
155
+ throw new FileNotFoundError("File not found - empty response body");
156
+ }
157
+
158
+ // Decode metadata from the buffer
159
+ const buffer = Buffer.from(arrayBuffer);
160
+ const { metadata, contentStart } =
161
+ this.metadataService.decodeMetadataFromBuffer(buffer);
162
+
163
+ // Extract the actual content
164
+ const content = buffer.subarray(contentStart);
165
+
166
+ return createFile(content, {
167
+ name: metadata.name,
168
+ type: metadata.type,
169
+ size: content.length,
170
+ });
171
+ } catch (error) {
172
+ if (error instanceof FileNotFoundError) {
173
+ throw error;
174
+ }
175
+
176
+ this.log.error(`Failed to download file: ${error}`);
177
+ if (error instanceof Error) {
178
+ throw new FileNotFoundError("Error downloading file", { cause: error });
179
+ }
180
+
181
+ throw error;
182
+ }
183
+ }
184
+
185
+ public async exists(bucketName: string, fileId: string): Promise<boolean> {
186
+ this.log.trace(
187
+ `Checking existence of file '${fileId}' in bucket '${bucketName}'...`,
188
+ );
189
+
190
+ const storeName = this.convertName(bucketName);
191
+ const pathname = `${storeName}/${fileId}`;
192
+
193
+ try {
194
+ const result = await this.vercelBlobApi.head(pathname, {
195
+ token: this.env.BLOB_READ_WRITE_TOKEN,
196
+ });
197
+ return result !== null;
198
+ } catch (error) {
199
+ // Vercel Blob head() throws for non-existent files
200
+ return false;
201
+ }
202
+ }
203
+
204
+ public async delete(bucketName: string, fileId: string): Promise<void> {
205
+ this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);
206
+
207
+ const storeName = this.convertName(bucketName);
208
+ const pathname = `${storeName}/${fileId}`;
209
+
210
+ try {
211
+ await this.vercelBlobApi.del(pathname, {
212
+ token: this.env.BLOB_READ_WRITE_TOKEN,
213
+ });
214
+ } catch (error) {
215
+ this.log.error(`Failed to delete file: ${error}`);
216
+ if (error instanceof Error) {
217
+ throw new FileNotFoundError("Error deleting file", { cause: error });
218
+ }
219
+ throw error;
220
+ }
221
+ }
222
222
  }