Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |
| } | |
| } | |