226 lines
5.5 KiB
TypeScript
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);
|
|
}
|