File size: 5,971 Bytes
68f7925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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,
    };
  }
}