import { dirname, fromFileUrl, join } from "denoPath"; import { ensureDir } from "denoFs"; import { createBlobAccessToken } from "../services/webTokenService.ts"; import { DEFAULT_CACHE_TIMEOUT } from "./tokenEvaluator.ts"; export type BlobType = "image" | "audio" | "video"; export interface BlobStorageConfig { blobType: BlobType; baseDir?: string; useTimestamp?: boolean; defaultExtension?: string; } export interface UploadResult { success: boolean; filePath?: string; fileName?: string; url?: string; error?: string; } const BLOB_TYPE_DIRS: Record = { image: "images", audio: "audio", video: "video", }; class BlobStorageManager { private readonly baseDir: string; private readonly currentDir: string; constructor() { this.currentDir = dirname(fromFileUrl(import.meta.url)); this.baseDir = Deno.env.get("BLOB_PATH") || join(this.currentDir, "../../../Blobs"); } /** * Get the directory path for a specific blob type. */ getBlobDirectory(blobType: BlobType, baseDirOverride?: string): string { const baseDir = baseDirOverride || this.baseDir; return join(baseDir, BLOB_TYPE_DIRS[blobType]); } /** * Ensure the blob directory exists. */ async ensureBlobDirectory( blobType: BlobType, baseDirOverride?: string, ): Promise { const dir = this.getBlobDirectory(blobType, baseDirOverride); await ensureDir(dir); } /** * Generate a filename using the provided config. */ generateFileName( originalName: string, config: BlobStorageConfig, ): string { const { useTimestamp = false, defaultExtension } = config; let fileName = originalName; if (useTimestamp) { const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice( 0, 19, ); const nameWithoutExt = fileName.replace(/\.[^/.]+$/, ""); const extension = this.getFileExtension(fileName, defaultExtension); fileName = `${nameWithoutExt}-${timestamp}${extension}`; } const extension = this.getFileExtension(fileName, defaultExtension); if (extension && !fileName.endsWith(extension)) { fileName = fileName.replace(/\.[^/.]+$/, "") + extension; } return fileName; } /** * Get file extension with fallback to default. */ private getFileExtension( fileName: string, defaultExtension?: string, ): string { const lastDot = fileName.lastIndexOf("."); if (lastDot > 0) { return fileName.substring(lastDot); } return defaultExtension || ""; } /** * Upload blob to local storage. */ async uploadBlob( fileName: string, blob: Blob, config: BlobStorageConfig, ): Promise { try { await this.ensureBlobDirectory(config.blobType, config.baseDir); const finalFileName = this.generateFileName(fileName, config); const filePath = join( this.getBlobDirectory(config.blobType, config.baseDir), finalFileName, ); const content = new Uint8Array(await blob.arrayBuffer()); await Deno.writeFile(filePath, content); const url = this.getBlobUrl(finalFileName, config.blobType); return { success: true, filePath, fileName: finalFileName, url, }; } catch (error) { console.error(`Failed to save ${config.blobType} blob locally:`, error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Get the public URL for a blob file. */ getBlobUrl(fileName: string, blobType: BlobType): string { return `/api/${blobType}-blob/${fileName}`; } /** * Get the full file system path for a blob. */ getFilePath( fileName: string, blobType: BlobType, baseDirOverride?: string, ): string { return join(this.getBlobDirectory(blobType, baseDirOverride), fileName); } /** * Check if a file exists. */ async fileExists( fileName: string, blobType: BlobType, baseDirOverride?: string, ): Promise { try { const filePath = this.getFilePath(fileName, blobType, baseDirOverride); await Deno.stat(filePath); return true; } catch { return false; } } /** * Delete a blob file. */ async deleteBlob( fileName: string, blobType: BlobType, baseDirOverride?: string, ): Promise { try { const filePath = this.getFilePath(fileName, blobType, baseDirOverride); await Deno.remove(filePath); return true; } catch (error) { console.error(`Failed to delete ${blobType} blob:`, error); return false; } } } export const blobStorage = new BlobStorageManager(); export async function uploadToLocalBlobStorage( fileName: string, blob: Blob, blobType: BlobType, options: { useTimestamp?: boolean; defaultExtension?: string } = {}, ): Promise { return await blobStorage.uploadBlob(fileName, blob, { blobType, useTimestamp: options.useTimestamp, defaultExtension: options.defaultExtension, }); } export async function getLocalBlobUrl( fileName: string, blobType: BlobType, userId: string, stateId: string, ): Promise { const newTokenId = await createBlobAccessToken( userId, blobType, DEFAULT_CACHE_TIMEOUT, stateId, ); const url = blobStorage.getBlobUrl(fileName, blobType); return url + "?token_id=" + newTokenId + "&state_id=" + stateId; } export function getBlobDirectory(blobType: BlobType): string { return blobStorage.getBlobDirectory(blobType); }