@alepha/bucket-vercel 0.10.3 → 0.10.5

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FileStorageProvider } from "@alepha/bucket";
1
+ import { FileMetadataService, FileStorageProvider } from "@alepha/bucket";
2
2
  import * as _alepha_core1 from "@alepha/core";
3
3
  import { Alepha, FileLike, Static } from "@alepha/core";
4
4
  import { DateTimeProvider } from "@alepha/datetime";
@@ -31,8 +31,10 @@ declare class VercelFileStorageProvider implements FileStorageProvider {
31
31
  protected readonly time: DateTimeProvider;
32
32
  protected readonly stores: Set<string>;
33
33
  protected readonly vercelBlobApi: VercelBlobApi;
34
+ protected readonly metadataService: FileMetadataService;
34
35
  protected readonly onStart: _alepha_core1.HookDescriptor<"start">;
35
36
  convertName(name: string): string;
37
+ protected createId(): string;
36
38
  upload(bucketName: string, file: FileLike, fileId?: string): Promise<string>;
37
39
  download(bucketName: string, fileId: string): Promise<FileLike>;
38
40
  exists(bucketName: string, fileId: string): Promise<boolean>;
@@ -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;;;;cCgBP,yBAAS;yBAIb,aAAA,CAAA;;;EDvBW,UAAA,GAAA,SC0BU,OD1BG,CC0BK,MD1BL,CAAA,OC0BmB,SD1BnB,CAAA,CAAA,CAAA,CAAA;;;;;cCgCb,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,OAAA,EAmBkB,aAAA,CAEN,cArBZ,CAAA,OAAA,CAAA;aAO8B,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAAA;QAAd,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EA0CvB,QA1CuB,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EA4C3B,OA5C2B,CAAA,MAAA,CAAA;UAAR,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAkFqC,OAlFrC,CAkF6C,QAlF7C,CAAA;EAAO,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA0I4B,OA1I5B,CAAA,OAAA,CAAA;EAAA,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6J4B,OA7J5B,CAAA,IAAA,CAAA;AAM9B;;;;;;;;;cClBa,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;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"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { $bucket, AlephaBucket, FileNotFoundError, FileStorageProvider } from "@alepha/bucket";
1
+ import { $bucket, AlephaBucket, FileMetadataService, FileNotFoundError, FileStorageProvider } from "@alepha/bucket";
2
2
  import { $env, $hook, $inject, $module, Alepha, AlephaError, t } from "@alepha/core";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { DateTimeProvider } from "@alepha/datetime";
4
5
  import { createFile } from "@alepha/file";
5
6
  import { $logger } from "@alepha/logger";
@@ -14,7 +15,7 @@ var VercelBlobApi = class {
14
15
 
15
16
  //#endregion
16
17
  //#region src/providers/VercelFileStorageProvider.ts
17
- const envSchema = t.object({ BLOB_READ_WRITE_TOKEN: t.string({ size: "long" }) });
18
+ const envSchema = t.object({ BLOB_READ_WRITE_TOKEN: t.text({ size: "long" }) });
18
19
  /**
19
20
  * Vercel Blob Storage implementation of File Storage Provider.
20
21
  */
@@ -25,6 +26,7 @@ var VercelFileStorageProvider = class {
25
26
  time = $inject(DateTimeProvider);
26
27
  stores = /* @__PURE__ */ new Set();
27
28
  vercelBlobApi = $inject(VercelBlobApi);
29
+ metadataService = $inject(FileMetadataService);
28
30
  onStart = $hook({
29
31
  on: "start",
30
32
  handler: async () => {
@@ -40,15 +42,20 @@ var VercelFileStorageProvider = class {
40
42
  convertName(name) {
41
43
  return name.replaceAll("/", "-").toLowerCase();
42
44
  }
45
+ createId() {
46
+ return randomUUID();
47
+ }
43
48
  async upload(bucketName, file, fileId) {
44
- fileId = file.name;
49
+ fileId ??= this.createId();
45
50
  this.log.trace(`Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`);
46
51
  const storeName = this.convertName(bucketName);
47
52
  const pathname = `${storeName}/${fileId}`;
48
53
  try {
49
- const result = await this.vercelBlobApi.put(pathname, file.stream(), {
54
+ const contentBuffer = Buffer.from(await file.arrayBuffer());
55
+ const fileBuffer = this.metadataService.createFileBuffer(file, contentBuffer);
56
+ const result = await this.vercelBlobApi.put(pathname, fileBuffer, {
50
57
  access: "public",
51
- contentType: file.type,
58
+ contentType: file.type || "application/octet-stream",
52
59
  token: this.env.BLOB_READ_WRITE_TOKEN,
53
60
  allowOverwrite: true
54
61
  });
@@ -69,13 +76,15 @@ var VercelFileStorageProvider = class {
69
76
  if (!headResult) throw new FileNotFoundError(`File '${fileId}' not found in bucket '${bucketName}'`);
70
77
  const response = await fetch(headResult.url);
71
78
  if (!response.ok) throw new FileNotFoundError(`Failed to fetch file: ${response.statusText}`);
72
- const stream = response.body;
73
- if (!stream) throw new FileNotFoundError("File not found - empty response body");
74
- const originalType = response.headers.get("Content-Type") || "application/octet-stream";
75
- return createFile(stream, {
76
- name: fileId,
77
- type: originalType,
78
- size: headResult.size
79
+ const arrayBuffer = await response.arrayBuffer();
80
+ if (!arrayBuffer) throw new FileNotFoundError("File not found - empty response body");
81
+ const buffer = Buffer.from(arrayBuffer);
82
+ const { metadata, contentStart } = this.metadataService.decodeMetadataFromBuffer(buffer);
83
+ const content = buffer.subarray(contentStart);
84
+ return createFile(content, {
85
+ name: metadata.name,
86
+ type: metadata.type,
87
+ size: content.length
79
88
  });
80
89
  } catch (error) {
81
90
  if (error instanceof FileNotFoundError) throw 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 type { Readable } from \"node:stream\";\nimport {\n\t$bucket,\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.string({\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\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\tpublic async upload(\n\t\tbucketName: string,\n\t\tfile: FileLike,\n\t\tfileId?: string,\n\t): Promise<string> {\n\t\t// force file id as filename for Vercel because we can't store filename as metadata\n\t\t// it's bad, but we have no choice for now\n\t\tfileId = file.name;\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\tconst result = await this.vercelBlobApi.put(\n\t\t\t\tpathname,\n\t\t\t\tfile.stream() as Readable,\n\t\t\t\t{\n\t\t\t\t\taccess: \"public\",\n\t\t\t\t\tcontentType: file.type,\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\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 stream = response.body;\n\t\t\tif (!stream) {\n\t\t\t\tthrow new FileNotFoundError(\"File not found - empty response body\");\n\t\t\t}\n\n\t\t\tconst originalType =\n\t\t\t\tresponse.headers.get(\"Content-Type\") || \"application/octet-stream\";\n\n\t\t\treturn createFile(stream, {\n\t\t\t\tname: fileId,\n\t\t\t\ttype: originalType,\n\t\t\t\tsize: headResult.size,\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\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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"}
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "blob"
11
11
  ],
12
12
  "author": "Feunard",
13
- "version": "0.10.3",
13
+ "version": "0.10.5",
14
14
  "type": "module",
15
15
  "engines": {
16
16
  "node": ">=22.0.0"
@@ -23,16 +23,16 @@
23
23
  "src"
24
24
  ],
25
25
  "dependencies": {
26
- "@alepha/bucket": "0.10.3",
27
- "@alepha/core": "0.10.3",
28
- "@alepha/datetime": "0.10.3",
29
- "@alepha/file": "0.10.3",
30
- "@alepha/logger": "0.10.3",
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",
31
31
  "@vercel/blob": "^2.0.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@biomejs/biome": "^2.2.5",
35
- "@types/node": "^24.6.2",
35
+ "@types/node": "^24.7.1",
36
36
  "tsdown": "^0.15.6",
37
37
  "typescript": "^5.9.3",
38
38
  "vitest": "^3.2.4"
@@ -1,6 +1,8 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import type { Readable } from "node:stream";
2
3
  import {
3
4
  $bucket,
5
+ FileMetadataService,
4
6
  FileNotFoundError,
5
7
  type FileStorageProvider,
6
8
  } from "@alepha/bucket";
@@ -20,7 +22,7 @@ import { $logger } from "@alepha/logger";
20
22
  import { VercelBlobApi } from "./VercelBlobProvider.ts";
21
23
 
22
24
  const envSchema = t.object({
23
- BLOB_READ_WRITE_TOKEN: t.string({
25
+ BLOB_READ_WRITE_TOKEN: t.text({
24
26
  size: "long",
25
27
  }),
26
28
  });
@@ -39,6 +41,7 @@ export class VercelFileStorageProvider implements FileStorageProvider {
39
41
  protected readonly time = $inject(DateTimeProvider);
40
42
  protected readonly stores: Set<string> = new Set();
41
43
  protected readonly vercelBlobApi = $inject(VercelBlobApi);
44
+ protected readonly metadataService = $inject(FileMetadataService);
42
45
 
43
46
  protected readonly onStart = $hook({
44
47
  on: "start",
@@ -66,14 +69,16 @@ export class VercelFileStorageProvider implements FileStorageProvider {
66
69
  return name.replaceAll("/", "-").toLowerCase();
67
70
  }
68
71
 
72
+ protected createId(): string {
73
+ return randomUUID();
74
+ }
75
+
69
76
  public async upload(
70
77
  bucketName: string,
71
78
  file: FileLike,
72
79
  fileId?: string,
73
80
  ): Promise<string> {
74
- // force file id as filename for Vercel because we can't store filename as metadata
75
- // it's bad, but we have no choice for now
76
- fileId = file.name;
81
+ fileId ??= this.createId();
77
82
 
78
83
  this.log.trace(
79
84
  `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,
@@ -83,12 +88,20 @@ export class VercelFileStorageProvider implements FileStorageProvider {
83
88
  const pathname = `${storeName}/${fileId}`;
84
89
 
85
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
86
99
  const result = await this.vercelBlobApi.put(
87
100
  pathname,
88
- file.stream() as Readable,
101
+ fileBuffer as unknown as Readable,
89
102
  {
90
103
  access: "public",
91
- contentType: file.type,
104
+ contentType: file.type || "application/octet-stream",
92
105
  token: this.env.BLOB_READ_WRITE_TOKEN,
93
106
  allowOverwrite: true,
94
107
  },
@@ -128,7 +141,7 @@ export class VercelFileStorageProvider implements FileStorageProvider {
128
141
  );
129
142
  }
130
143
 
131
- // fetch the actual file content
144
+ // fetch the actual file content (with metadata)
132
145
  const response = await fetch(headResult.url);
133
146
 
134
147
  if (!response.ok) {
@@ -137,18 +150,23 @@ export class VercelFileStorageProvider implements FileStorageProvider {
137
150
  );
138
151
  }
139
152
 
140
- const stream = response.body;
141
- if (!stream) {
153
+ const arrayBuffer = await response.arrayBuffer();
154
+ if (!arrayBuffer) {
142
155
  throw new FileNotFoundError("File not found - empty response body");
143
156
  }
144
157
 
145
- const originalType =
146
- response.headers.get("Content-Type") || "application/octet-stream";
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);
147
165
 
148
- return createFile(stream, {
149
- name: fileId,
150
- type: originalType,
151
- size: headResult.size,
166
+ return createFile(content, {
167
+ name: metadata.name,
168
+ type: metadata.type,
169
+ size: content.length,
152
170
  });
153
171
  } catch (error) {
154
172
  if (error instanceof FileNotFoundError) {