FE_Dev / server /utils /temp-storage-cache.ts
GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
import fs from 'fs';
import os from 'os';
import path from 'path';
/**
* HuggingFace Spacesの/tmpディレクトリを活用した画像キャッシュ
* メモリ制限を回避し、より大容量の画像を扱える
*/
export class TempStorageCache {
private static instance: TempStorageCache;
private cacheDir: string;
private metaData = new Map<string, { path: string; contentType: string; createdAt: number }>();
private maxAge = 24 * 60 * 60 * 1000; // 24時間
private constructor() {
// /tmp内に専用ディレクトリを作成
this.cacheDir = path.join(os.tmpdir(), 'image-cache');
this.ensureCacheDir();
// 定期的にクリーンアップ
setInterval(() => this.cleanup(), 6 * 60 * 60 * 1000); // 6時間ごと
}
static getInstance(): TempStorageCache {
if (!TempStorageCache.instance) {
TempStorageCache.instance = new TempStorageCache();
}
return TempStorageCache.instance;
}
/**
* キャッシュディレクトリを確保
*/
private ensureCacheDir(): void {
try {
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
}
} catch (error) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.error(`[${timestamp}] [TempStorageCache] Failed to create cache directory:`, error);
// フォールバック:メモリキャッシュモードに切り替え
}
}
/**
* 画像をストレージに保存
*/
store(id: string, buffer: Buffer, contentType: string = 'image/webp'): string | null {
try {
const fileName = `${id}.webp`;
const filePath = path.join(this.cacheDir, fileName);
// ファイルに書き込み
fs.writeFileSync(filePath, buffer);
// メタデータを保存
this.metaData.set(id, {
path: filePath,
contentType,
createdAt: Date.now(),
});
// APIエンドポイント経由のURL返却(シンプルなパス)
return `/api/temp-images/${id}`;
} catch (error) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.error(`[${timestamp}] [TempStorageCache] Failed to store ${id}:`, error);
return null;
}
}
/**
* 画像を取得
*/
get(id: string): { buffer: Buffer; contentType: string } | null {
const meta = this.metaData.get(id);
if (!meta) return null;
// 有効期限チェック
if (Date.now() - meta.createdAt > this.maxAge) {
this.remove(id);
return null;
}
try {
// ファイルから読み込み
if (fs.existsSync(meta.path)) {
const buffer = fs.readFileSync(meta.path);
return {
buffer,
contentType: meta.contentType,
};
}
} catch (error) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.error(`[${timestamp}] [TempStorageCache] Failed to read ${id}:`, error);
}
// ファイルが見つからない場合
this.metaData.delete(id);
return null;
}
/**
* 画像を削除
*/
private remove(id: string): void {
const meta = this.metaData.get(id);
if (meta) {
try {
if (fs.existsSync(meta.path)) {
fs.unlinkSync(meta.path);
}
} catch (error) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.error(`[${timestamp}] [TempStorageCache] Failed to delete ${id}:`, error);
}
this.metaData.delete(id);
}
}
/**
* 期限切れファイルをクリーンアップ
*/
private cleanup(): void {
const now = Date.now();
const expired: string[] = [];
for (const [id, meta] of this.metaData.entries()) {
if (now - meta.createdAt > this.maxAge) {
expired.push(id);
}
}
if (expired.length > 0) {
expired.forEach((id) => this.remove(id));
}
// ディスク容量チェック(オプション)
this.checkDiskUsage();
}
/**
* ディスク使用量をチェック
*/
private checkDiskUsage(): void {
try {
const files = fs.readdirSync(this.cacheDir);
let totalSize = 0;
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
const stat = fs.statSync(filePath);
totalSize += stat.size;
}
// 1GB超えたら古いファイルを削除
if (totalSize > 1024 * 1024 * 1024) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.warn(`[${timestamp}] [TempStorageCache] Disk usage exceeded 1GB, forcing cleanup`);
this.forceCleanup();
}
} catch (error) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.error(`[${timestamp}] [TempStorageCache] Failed to check disk usage:`, error);
}
}
/**
* 強制的に古いファイルを削除
*/
private forceCleanup(): void {
// 作成時刻でソートして古い順に削除
const sorted = Array.from(this.metaData.entries()).sort(([, a], [, b]) => a.createdAt - b.createdAt);
// 半分を削除
const toDelete = sorted.slice(0, Math.floor(sorted.length / 2));
toDelete.forEach(([id]) => this.remove(id));
}
/**
* キャッシュ状態を取得
*/
getStats() {
let totalSize = 0;
let fileCount = 0;
try {
const files = fs.readdirSync(this.cacheDir);
fileCount = files.length;
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
const stat = fs.statSync(filePath);
totalSize += stat.size;
}
} catch (error) {
// エラー時は0を返す
}
return {
entries: this.metaData.size,
files: fileCount,
sizeBytes: totalSize,
sizeMB: (totalSize / 1024 / 1024).toFixed(2),
cacheDir: this.cacheDir,
};
}
}