integratedhub/utils/blobStorage.ts
2026-01-20 09:02:26 -06:00

226 lines
5.5 KiB
TypeScript

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<BlobType, string> = {
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<void> {
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<UploadResult> {
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<boolean> {
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<boolean> {
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<UploadResult> {
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<string> {
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);
}