@alepha/bucket-vercel 0.12.0 → 0.12.1

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { $env, $hook, $inject, $module, Alepha, AlephaError, t } from "alepha";
2
- import { $bucket, AlephaBucket, FileMetadataService, FileNotFoundError, FileStorageProvider } from "alepha/bucket";
2
+ import { $bucket, AlephaBucket, FileNotFoundError, FileStorageProvider } from "alepha/bucket";
3
3
  import { DateTimeProvider } from "alepha/datetime";
4
- import { FileSystemProvider } from "alepha/file";
4
+ import { FileDetector, FileSystemProvider } from "alepha/file";
5
5
  import { $logger } from "alepha/logger";
6
6
  import { del, head, put } from "@vercel/blob";
7
7
 
@@ -24,9 +24,9 @@ var VercelFileStorageProvider = class {
24
24
  alepha = $inject(Alepha);
25
25
  time = $inject(DateTimeProvider);
26
26
  fileSystem = $inject(FileSystemProvider);
27
+ fileDetector = $inject(FileDetector);
27
28
  stores = /* @__PURE__ */ new Set();
28
29
  vercelBlobApi = $inject(VercelBlobApi);
29
- metadataService = $inject(FileMetadataService);
30
30
  onStart = $hook({
31
31
  on: "start",
32
32
  handler: async () => {
@@ -42,17 +42,17 @@ var VercelFileStorageProvider = class {
42
42
  convertName(name) {
43
43
  return name.replaceAll("/", "-").toLowerCase();
44
44
  }
45
- createId() {
46
- return crypto.randomUUID();
45
+ createId(mimeType) {
46
+ const ext = this.fileDetector.getExtensionFromMimeType(mimeType);
47
+ return `${crypto.randomUUID()}.${ext}`;
47
48
  }
48
49
  async upload(bucketName, file, fileId) {
49
- fileId ??= this.createId();
50
+ fileId ??= this.createId(file.type);
50
51
  this.log.trace(`Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`);
51
52
  const pathname = `${this.convertName(bucketName)}/${fileId}`;
52
53
  try {
53
54
  const contentBuffer = Buffer.from(await file.arrayBuffer());
54
- const fileBuffer = this.metadataService.createFileBuffer(file, contentBuffer);
55
- const result = await this.vercelBlobApi.put(pathname, fileBuffer, {
55
+ const result = await this.vercelBlobApi.put(pathname, contentBuffer, {
56
56
  access: "public",
57
57
  contentType: file.type || "application/octet-stream",
58
58
  token: this.env.BLOB_READ_WRITE_TOKEN,
@@ -76,13 +76,11 @@ var VercelFileStorageProvider = class {
76
76
  if (!response.ok) throw new FileNotFoundError(`Failed to fetch file: ${response.statusText}`);
77
77
  const arrayBuffer = await response.arrayBuffer();
78
78
  if (!arrayBuffer) throw new FileNotFoundError("File not found - empty response body");
79
- const buffer = Buffer.from(arrayBuffer);
80
- const { metadata, contentStart } = this.metadataService.decodeMetadataFromBuffer(buffer);
81
- const content = buffer.subarray(contentStart);
79
+ const mimeType = this.fileDetector.getContentType(fileId);
82
80
  return this.fileSystem.createFile({
83
- buffer: content,
84
- name: metadata.name,
85
- type: metadata.type
81
+ buffer: Buffer.from(arrayBuffer),
82
+ name: fileId,
83
+ type: mimeType
86
84
  });
87
85
  } catch (error) {
88
86
  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 put: typeof put = put;\n head: typeof head = head;\n del: typeof del = del;\n}\n","import type { Readable } from \"node:stream\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport {\n $bucket,\n FileMetadataService,\n FileNotFoundError,\n type FileStorageProvider,\n} from \"alepha/bucket\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { FileSystemProvider } 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\" {\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 fileSystem = $inject(FileSystemProvider);\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(`Blob storage '${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 crypto.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 this.fileSystem.createFile({\n buffer: content,\n name: metadata.name,\n type: metadata.type,\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 { $module } from \"alepha\";\nimport { AlephaBucket, FileStorageProvider } from \"alepha/bucket\";\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":";;;;;;;;AAEA,IAAa,gBAAb,MAA2B;CACzB,MAAkB;CAClB,OAAoB;CACpB,MAAkB;;;;;ACiBpB,MAAM,YAAY,EAAE,OAAO,EACzB,uBAAuB,EAAE,KAAK,EAC5B,MAAM,QACP,CAAC,EACH,CAAC;;;;AASF,IAAa,4BAAb,MAAsE;CACpE,AAAmB,MAAM,SAAS;CAClC,AAAmB,MAAM,KAAK,UAAU;CACxC,AAAmB,SAAS,QAAQ,OAAO;CAC3C,AAAmB,OAAO,QAAQ,iBAAiB;CACnD,AAAmB,aAAa,QAAQ,mBAAmB;CAC3D,AAAmB,yBAAsB,IAAI,KAAK;CAClD,AAAmB,gBAAgB,QAAQ,cAAc;CACzD,AAAmB,kBAAkB,QAAQ,oBAAoB;CAEjE,AAAmB,UAAU,MAAM;EACjC,IAAI;EACJ,SAAS,YAAY;AACnB,QAAK,MAAM,UAAU,KAAK,OAAO,YAAY,QAAQ,EAAE;AACrD,QAAI,OAAO,aAAa,KACtB;IAGF,MAAM,YAAY,KAAK,YAAY,OAAO,KAAK;AAE/C,SAAK,IAAI,MAAM,kBAAkB,UAAU,OAAO;AAIlD,SAAK,OAAO,IAAI,UAAU;AAE1B,SAAK,IAAI,KAAK,iBAAiB,OAAO,KAAK,MAAM;;;EAGtD,CAAC;CAEF,AAAO,YAAY,MAAsB;AAEvC,SAAO,KAAK,WAAW,KAAK,IAAI,CAAC,aAAa;;CAGhD,AAAU,WAAmB;AAC3B,SAAO,OAAO,YAAY;;CAG5B,MAAa,OACX,YACA,MACA,QACiB;AACjB,aAAW,KAAK,UAAU;AAE1B,OAAK,IAAI,MACP,mBAAmB,KAAK,KAAK,eAAe,WAAW,aAAa,OAAO,MAC5E;EAGD,MAAM,WAAW,GADC,KAAK,YAAY,WAAW,CAChB,GAAG;AAEjC,MAAI;GAEF,MAAM,gBAAgB,OAAO,KAAK,MAAM,KAAK,aAAa,CAAC;GAC3D,MAAM,aAAa,KAAK,gBAAgB,iBACtC,MACA,cACD;GAGD,MAAM,SAAS,MAAM,KAAK,cAAc,IACtC,UACA,YACA;IACE,QAAQ;IACR,aAAa,KAAK,QAAQ;IAC1B,OAAO,KAAK,IAAI;IAChB,gBAAgB;IACjB,CACF;AAED,QAAK,IAAI,MAAM,+BAA+B,OAAO,MAAM;AAC3D,UAAO;WACA,OAAO;AACd,QAAK,IAAI,MAAM,0BAA0B,QAAQ;AACjD,OAAI,iBAAiB,MACnB,OAAM,IAAI,YAAY,kBAAkB,MAAM,WAAW,EACvD,OAAO,OACR,CAAC;AAGJ,SAAM;;;CAIV,MAAa,SAAS,YAAoB,QAAmC;AAC3E,OAAK,IAAI,MACP,qBAAqB,OAAO,iBAAiB,WAAW,MACzD;EAGD,MAAM,WAAW,GADC,KAAK,YAAY,WAAW,CAChB,GAAG;AAEjC,MAAI;GAEF,MAAM,aAAa,MAAM,KAAK,cAAc,KAAK,UAAU,EACzD,OAAO,KAAK,IAAI,uBACjB,CAAC;AAEF,OAAI,CAAC,WACH,OAAM,IAAI,kBACR,SAAS,OAAO,yBAAyB,WAAW,GACrD;GAIH,MAAM,WAAW,MAAM,MAAM,WAAW,IAAI;AAE5C,OAAI,CAAC,SAAS,GACZ,OAAM,IAAI,kBACR,yBAAyB,SAAS,aACnC;GAGH,MAAM,cAAc,MAAM,SAAS,aAAa;AAChD,OAAI,CAAC,YACH,OAAM,IAAI,kBAAkB,uCAAuC;GAIrE,MAAM,SAAS,OAAO,KAAK,YAAY;GACvC,MAAM,EAAE,UAAU,iBAChB,KAAK,gBAAgB,yBAAyB,OAAO;GAGvD,MAAM,UAAU,OAAO,SAAS,aAAa;AAE7C,UAAO,KAAK,WAAW,WAAW;IAChC,QAAQ;IACR,MAAM,SAAS;IACf,MAAM,SAAS;IAChB,CAAC;WACK,OAAO;AACd,OAAI,iBAAiB,kBACnB,OAAM;AAGR,QAAK,IAAI,MAAM,4BAA4B,QAAQ;AACnD,OAAI,iBAAiB,MACnB,OAAM,IAAI,kBAAkB,0BAA0B,EAAE,OAAO,OAAO,CAAC;AAGzE,SAAM;;;CAIV,MAAa,OAAO,YAAoB,QAAkC;AACxE,OAAK,IAAI,MACP,+BAA+B,OAAO,eAAe,WAAW,MACjE;EAGD,MAAM,WAAW,GADC,KAAK,YAAY,WAAW,CAChB,GAAG;AAEjC,MAAI;AAIF,UAHe,MAAM,KAAK,cAAc,KAAK,UAAU,EACrD,OAAO,KAAK,IAAI,uBACjB,CAAC,KACgB;WACX,OAAO;AAEd,UAAO;;;CAIX,MAAa,OAAO,YAAoB,QAA+B;AACrE,OAAK,IAAI,MAAM,kBAAkB,OAAO,iBAAiB,WAAW,MAAM;EAG1E,MAAM,WAAW,GADC,KAAK,YAAY,WAAW,CAChB,GAAG;AAEjC,MAAI;AACF,SAAM,KAAK,cAAc,IAAI,UAAU,EACrC,OAAO,KAAK,IAAI,uBACjB,CAAC;WACK,OAAO;AACd,QAAK,IAAI,MAAM,0BAA0B,QAAQ;AACjD,OAAI,iBAAiB,MACnB,OAAM,IAAI,kBAAkB,uBAAuB,EAAE,OAAO,OAAO,CAAC;AAEtE,SAAM;;;;;;;;;;;;;AC1MZ,MAAa,qBAAqB,QAAQ;CACxC,MAAM;CACN,UAAU,CAAC,0BAA0B;CACrC,WAAW,WACT,OACG,KAAK;EACJ,UAAU;EACV,SAAS;EACT,KAAK;EACN,CAAC,CACD,KAAK,aAAa;CACxB,CAAC"}
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 type { Readable } from \"node:stream\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport {\n $bucket,\n FileNotFoundError,\n type FileStorageProvider,\n} from \"alepha/bucket\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { FileDetector, FileSystemProvider } 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\" {\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 fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly stores: Set<string> = new Set();\n protected readonly vercelBlobApi = $inject(VercelBlobApi);\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(`Blob storage '${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(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${crypto.randomUUID()}.${ext}`;\n }\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\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 const contentBuffer = Buffer.from(await file.arrayBuffer());\n\n const result = await this.vercelBlobApi.put(\n pathname,\n contentBuffer 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 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 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 const mimeType = this.fileDetector.getContentType(fileId);\n\n return this.fileSystem.createFile({\n buffer: Buffer.from(arrayBuffer),\n name: fileId,\n type: mimeType,\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 { $module } from \"alepha\";\nimport { AlephaBucket, FileStorageProvider } from \"alepha/bucket\";\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
@@ -9,8 +9,8 @@
9
9
  "vercel",
10
10
  "blob"
11
11
  ],
12
- "author": "Feunard",
13
- "version": "0.12.0",
12
+ "author": "Nicolas Foures",
13
+ "version": "0.12.1",
14
14
  "type": "module",
15
15
  "engines": {
16
16
  "node": ">=22.0.0"
@@ -27,12 +27,12 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.10.1",
30
- "alepha": "0.12.0",
31
- "tsdown": "^0.16.4",
32
- "vitest": "^4.0.9"
30
+ "alepha": "0.12.1",
31
+ "tsdown": "^0.16.7",
32
+ "vitest": "^4.0.14"
33
33
  },
34
34
  "peerDependencies": {
35
- "alepha": "0.12.0"
35
+ "alepha": "0.12.1"
36
36
  },
37
37
  "scripts": {
38
38
  "lint": "alepha lint",
@@ -11,12 +11,11 @@ import {
11
11
  } from "alepha";
12
12
  import {
13
13
  $bucket,
14
- FileMetadataService,
15
14
  FileNotFoundError,
16
15
  type FileStorageProvider,
17
16
  } from "alepha/bucket";
18
17
  import { DateTimeProvider } from "alepha/datetime";
19
- import { FileSystemProvider } from "alepha/file";
18
+ import { FileDetector, FileSystemProvider } from "alepha/file";
20
19
  import { $logger } from "alepha/logger";
21
20
  import { VercelBlobApi } from "./VercelBlobProvider.ts";
22
21
 
@@ -39,9 +38,9 @@ export class VercelFileStorageProvider implements FileStorageProvider {
39
38
  protected readonly alepha = $inject(Alepha);
40
39
  protected readonly time = $inject(DateTimeProvider);
41
40
  protected readonly fileSystem = $inject(FileSystemProvider);
41
+ protected readonly fileDetector = $inject(FileDetector);
42
42
  protected readonly stores: Set<string> = new Set();
43
43
  protected readonly vercelBlobApi = $inject(VercelBlobApi);
44
- protected readonly metadataService = $inject(FileMetadataService);
45
44
 
46
45
  protected readonly onStart = $hook({
47
46
  on: "start",
@@ -69,8 +68,9 @@ export class VercelFileStorageProvider implements FileStorageProvider {
69
68
  return name.replaceAll("/", "-").toLowerCase();
70
69
  }
71
70
 
72
- protected createId(): string {
73
- return crypto.randomUUID();
71
+ protected createId(mimeType: string): string {
72
+ const ext = this.fileDetector.getExtensionFromMimeType(mimeType);
73
+ return `${crypto.randomUUID()}.${ext}`;
74
74
  }
75
75
 
76
76
  public async upload(
@@ -78,7 +78,7 @@ export class VercelFileStorageProvider implements FileStorageProvider {
78
78
  file: FileLike,
79
79
  fileId?: string,
80
80
  ): Promise<string> {
81
- fileId ??= this.createId();
81
+ fileId ??= this.createId(file.type);
82
82
 
83
83
  this.log.trace(
84
84
  `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,
@@ -88,17 +88,11 @@ export class VercelFileStorageProvider implements FileStorageProvider {
88
88
  const pathname = `${storeName}/${fileId}`;
89
89
 
90
90
  try {
91
- // Create a buffer with metadata and content
92
91
  const contentBuffer = Buffer.from(await file.arrayBuffer());
93
- const fileBuffer = this.metadataService.createFileBuffer(
94
- file,
95
- contentBuffer,
96
- );
97
92
 
98
- // Upload the complete buffer (metadata + content) to Vercel Blob
99
93
  const result = await this.vercelBlobApi.put(
100
94
  pathname,
101
- fileBuffer as unknown as Readable,
95
+ contentBuffer as unknown as Readable,
102
96
  {
103
97
  access: "public",
104
98
  contentType: file.type || "application/octet-stream",
@@ -130,7 +124,6 @@ export class VercelFileStorageProvider implements FileStorageProvider {
130
124
  const pathname = `${storeName}/${fileId}`;
131
125
 
132
126
  try {
133
- // check if the file exists and get metadata
134
127
  const headResult = await this.vercelBlobApi.head(pathname, {
135
128
  token: this.env.BLOB_READ_WRITE_TOKEN,
136
129
  });
@@ -141,7 +134,6 @@ export class VercelFileStorageProvider implements FileStorageProvider {
141
134
  );
142
135
  }
143
136
 
144
- // fetch the actual file content (with metadata)
145
137
  const response = await fetch(headResult.url);
146
138
 
147
139
  if (!response.ok) {
@@ -155,18 +147,12 @@ export class VercelFileStorageProvider implements FileStorageProvider {
155
147
  throw new FileNotFoundError("File not found - empty response body");
156
148
  }
157
149
 
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);
150
+ const mimeType = this.fileDetector.getContentType(fileId);
165
151
 
166
152
  return this.fileSystem.createFile({
167
- buffer: content,
168
- name: metadata.name,
169
- type: metadata.type,
153
+ buffer: Buffer.from(arrayBuffer),
154
+ name: fileId,
155
+ type: mimeType,
170
156
  });
171
157
  } catch (error) {
172
158
  if (error instanceof FileNotFoundError) {
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/providers/VercelBlobProvider.ts","../src/providers/VercelFileStorageProvider.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAEa,aAAA;cACC;eACC;cACD;;;;cCiBR,WAIJ,OAAA,CAJa;yBAIb,OAAA,CAAA;;;EDxBW,UAAA,GAAA,SC2BW,OD3BE,CC2BM,MD3BN,CAAA,OC2BoB,SD3BpB,CAAA,CAAA,CAAA,CACZ;;;;;cCgCD,yBAAA,YAAqC;0BAAX,cAAA,CACf;EAdlB,mBAIJ,GAAA,EAAA;IAJa,qBAAA,EAAA,MAAA;EAAA,CAAA;qBAAA,MAAA,EAgBY,MAhBZ;EAO+B,mBAAA,IAAA,EAUrB,gBAVqB;EAAd,mBAAA,UAAA,EAWD,kBAXC;EAAR,mBAAA,MAAA,EAYK,GAZL,CAAA,MAAA,CAAA;EAAO,mBAAA,aAAA,EAaG,aAbH;EAAA,mBAAA,eAAA,EAcK,mBAdL;EAAA,mBAAA,OAAA,EAcK,OAAA,CAER,cAhBG,CAAA,OAAA,CAAA;EAMlB,WAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAA0B;EAAA,UAAA,QACf,CAAA,CAAA,EAAA,MAAA;EAEG,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAuCjB,QAvCiB,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EAyCtB,OAzCsB,CAAA,MAAA,CAAA;EACF,QAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAoFoC,OApFpC,CAoF4C,QApF5C,CAAA;EACM,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAgJ4B,OAhJ5B,CAAA,OAAA,CAAA;EACF,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAkK8B,OAlK9B,CAAA,IAAA,CAAA;;;;;;;;;;cCzBhB,oBAAkB,OAAA,CAAA,QAW7B,OAAA,CAX6B,MAAA"}
@@ -1 +0,0 @@
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;;;;cCiBR,WAIJ,OAAA,CAJa;yBAIb,OAAA,CAAA;;;EDxBW,UAAA,GAAA,SC2BW,OD3BE,CC2BM,MD3BN,CAAA,OC2BoB,SD3BpB,CAAA,CAAA,CAAA,CACZ;;;;;cCgCD,yBAAA,YAAqC;0BAAX,cAAA,CACf;EAdlB,mBAIJ,GAAA,EAAA;IAJa,qBAAA,EAAA,MAAA;EAAA,CAAA;qBAAA,MAAA,EAgBY,MAhBZ;EAO+B,mBAAA,IAAA,EAUrB,gBAVqB;EAAd,mBAAA,UAAA,EAWD,kBAXC;EAAR,mBAAA,MAAA,EAYK,GAZL,CAAA,MAAA,CAAA;EAAO,mBAAA,aAAA,EAaG,aAbH;EAAA,mBAAA,eAAA,EAcK,mBAdL;EAAA,mBAAA,OAAA,EAcK,OAAA,CAER,cAhBG,CAAA,OAAA,CAAA;EAMlB,WAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAA0B;EAAA,UAAA,QACf,CAAA,CAAA,EAAA,MAAA;EAEG,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAuCjB,QAvCiB,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EAyCtB,OAzCsB,CAAA,MAAA,CAAA;EACF,QAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAoFoC,OApFpC,CAoF4C,QApF5C,CAAA;EACM,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAgJ4B,OAhJ5B,CAAA,OAAA,CAAA;EACF,MAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAkK8B,OAlK9B,CAAA,IAAA,CAAA;;;;;;;;;;cCzBhB,oBAAkB,OAAA,CAAA,QAW7B,OAAA,CAX6B,MAAA"}