File size: 2,923 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import path from 'node:path';
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import crypto from 'node:crypto';
import { Response } from 'express';
import { db } from '../../db/database';

const TREK_PHOTO_DIR = path.join(__dirname, '../../../uploads/photos/trek');
export const CACHE_TTL = 60 * 60 * 1000; // 1 hour

const inFlight = new Map<string, Promise<Buffer | null>>();

export function cacheKey(provider: string, assetId: string, kind: string, ownerId: number): string {
  return crypto.createHash('sha1').update(`${provider}:${assetId}:${kind}:${ownerId}`).digest('hex');
}

function ensureDir(): void {
  if (!fs.existsSync(TREK_PHOTO_DIR)) {
    fs.mkdirSync(TREK_PHOTO_DIR, { recursive: true });
  }
}

function cachedFilePath(key: string): string {
  return path.join(TREK_PHOTO_DIR, `${key}.bin`);
}

export function getFresh(key: string): { filePath: string; contentType: string } | null {
  const row = db.prepare(
    'SELECT content_type, fetched_at FROM trek_photo_cache_meta WHERE cache_key = ?'
  ).get(key) as { content_type: string; fetched_at: number } | undefined;

  if (!row) return null;

  if (Date.now() - row.fetched_at >= CACHE_TTL) {
    db.prepare('DELETE FROM trek_photo_cache_meta WHERE cache_key = ?').run(key);
    return null;
  }

  const fp = cachedFilePath(key);
  if (!fs.existsSync(fp)) {
    db.prepare('DELETE FROM trek_photo_cache_meta WHERE cache_key = ?').run(key);
    return null;
  }

  return { filePath: fp, contentType: row.content_type };
}

export async function put(key: string, bytes: Buffer, contentType: string): Promise<void> {
  ensureDir();
  const fp = cachedFilePath(key);
  const tmp = fp + '.tmp';

  await fsPromises.writeFile(tmp, bytes);
  await fsPromises.rename(tmp, fp);

  db.prepare(
    'INSERT OR REPLACE INTO trek_photo_cache_meta (cache_key, content_type, fetched_at) VALUES (?, ?, ?)'
  ).run(key, contentType, Date.now());
}

export function serveFresh(res: Response, key: string): boolean {
  const entry = getFresh(key);
  if (!entry) return false;

  res.set('Content-Type', entry.contentType);
  res.set('Cache-Control', 'public, max-age=3600');
  res.sendFile(entry.filePath);
  return true;
}

export function getInFlight(key: string): Promise<Buffer | null> | undefined {
  return inFlight.get(key);
}

export function setInFlight(key: string, promise: Promise<Buffer | null>): void {
  inFlight.set(key, promise);
  promise.finally(() => inFlight.delete(key));
}

export function sweepExpired(): void {
  const cutoff = Date.now() - CACHE_TTL * 2;
  const stale = db.prepare(
    'SELECT cache_key FROM trek_photo_cache_meta WHERE fetched_at < ?'
  ).all(cutoff) as { cache_key: string }[];

  for (const row of stale) {
    db.prepare('DELETE FROM trek_photo_cache_meta WHERE cache_key = ?').run(row.cache_key);
    const fp = cachedFilePath(row.cache_key);
    if (fs.existsSync(fp)) fs.unlinkSync(fp);
  }
}