kioai / artifacts /api-server /src /lib /localTempStorage.ts
kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
import { randomUUID } from "crypto";
import fs from "fs";
import path from "path";
import { Readable } from "stream";
const TEMP_STORAGE_PATH = process.env.TEMP_STORAGE_PATH || "/app/temp-storage";
const MAX_FILE_AGE_MS = 24 * 60 * 60 * 1000; // 24 小時
export class ObjectNotFoundError extends Error {
constructor() {
super("Object not found");
this.name = "ObjectNotFoundError";
Object.setPrototypeOf(this, ObjectNotFoundError.prototype);
}
}
export interface LocalFile {
path: string;
url: string;
exists(): Promise<boolean>;
delete(): Promise<void>;
getMetadata(): Promise<{ contentType: string; size: number }>;
createReadStream(): fs.ReadStream;
}
export class LocalTempStorageService {
private storagePath: string;
constructor() {
this.storagePath = TEMP_STORAGE_PATH;
this.ensureStorageDir();
this.startCleanupInterval();
}
private ensureStorageDir(): void {
if (!fs.existsSync(this.storagePath)) {
fs.mkdirSync(this.storagePath, { recursive: true });
}
}
// 定期清理舊檔案
private startCleanupInterval(): void {
setInterval(() => {
this.cleanupOldFiles().catch((err) => {
console.error("[LocalTempStorage] Cleanup error:", err);
});
}, 60 * 60 * 1000); // 每小時執行一次
}
private async cleanupOldFiles(): Promise<void> {
try {
const now = Date.now();
const files = fs.readdirSync(this.storagePath);
let deletedCount = 0;
for (const file of files) {
const filePath = path.join(this.storagePath, file);
const stats = fs.statSync(filePath);
if (now - stats.mtimeMs > MAX_FILE_AGE_MS) {
fs.unlinkSync(filePath);
deletedCount++;
}
}
if (deletedCount > 0) {
console.log(`[LocalTempStorage] Cleaned up ${deletedCount} old files`);
}
} catch (error) {
console.error("[LocalTempStorage] Error during cleanup:", error);
}
}
async saveFile(buffer: Buffer, filename: string, contentType: string = "application/octet-stream"): Promise<LocalFile> {
const fileId = randomUUID();
const ext = path.extname(filename) || "";
const savedFilename = `${fileId}${ext}`;
const filePath = path.join(this.storagePath, savedFilename);
fs.writeFileSync(filePath, buffer);
// 儲存 metadata
const metadataPath = `${filePath}.meta`;
fs.writeFileSync(metadataPath, JSON.stringify({ contentType, originalName: filename }));
return this.getFile(savedFilename);
}
getFile(filename: string): LocalFile {
const filePath = path.join(this.storagePath, filename);
const url = `/api/temp-storage/${filename}`;
return {
path: filePath,
url,
async exists() {
return fs.existsSync(filePath);
},
async delete() {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
const metadataPath = `${filePath}.meta`;
if (fs.existsSync(metadataPath)) {
fs.unlinkSync(metadataPath);
}
},
async getMetadata() {
const metadataPath = `${filePath}.meta`;
if (fs.existsSync(metadataPath)) {
const meta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
const stats = fs.statSync(filePath);
return {
contentType: meta.contentType || "application/octet-stream",
size: stats.size,
};
}
const stats = fs.statSync(filePath);
return {
contentType: "application/octet-stream",
size: stats.size,
};
},
createReadStream() {
return fs.createReadStream(filePath);
},
};
}
async downloadFile(file: LocalFile): Promise<Response> {
const exists = await file.exists();
if (!exists) {
throw new ObjectNotFoundError();
}
const metadata = await file.getMetadata();
const nodeStream = file.createReadStream();
const webStream = Readable.toWeb(nodeStream) as ReadableStream;
const headers: Record<string, string> = {
"Content-Type": metadata.contentType,
"Content-Length": String(metadata.size),
"Cache-Control": "public, max-age=3600",
};
return new Response(webStream, { headers });
}
// 從 URL 提取檔案名稱
extractFilenameFromUrl(url: string): string | null {
const match = url.match(/\/api\/temp-storage\/([^?]+)/);
return match ? match[1] : null;
}
// 清理所有檔案 (用於測試或維護)
async cleanupAll(): Promise<void> {
const files = fs.readdirSync(this.storagePath);
for (const file of files) {
const filePath = path.join(this.storagePath, file);
fs.unlinkSync(filePath);
}
console.log(`[LocalTempStorage] Cleaned up all ${files.length} files`);
}
}
// 單例實例
export const localTempStorage = new LocalTempStorageService();