All files / web/src/lib asset-store.ts

0% Statements 0/107
0% Branches 0/1
0% Functions 0/1
0% Lines 0/107

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                                                                                                                                                                                       
// File-based asset store for generated files
import * as fs from 'fs'
import * as path from 'path'
import { promisify } from 'util'

const writeFile = promisify(fs.writeFile)
const readFile = promisify(fs.readFile)
const unlink = promisify(fs.unlink)
const readdir = promisify(fs.readdir)
const stat = promisify(fs.stat)

export interface StoredAsset {
  data: Buffer
  filename: string
  mimeType: string
  createdAt: Date
}

// Use temp directory for storing generated assets
const ASSETS_DIR = path.join(process.cwd(), '.tmp/assets')

// Ensure assets directory exists
if (!fs.existsSync(ASSETS_DIR)) {
  fs.mkdirSync(ASSETS_DIR, { recursive: true })
}

export const assetStore = {
  async set(id: string, asset: StoredAsset): Promise<void> {
    const assetPath = path.join(ASSETS_DIR, `${id}.bin`)
    const metaPath = path.join(ASSETS_DIR, `${id}.meta.json`)

    // Store binary data
    await writeFile(assetPath, asset.data)

    // Store metadata
    const metadata = {
      filename: asset.filename,
      mimeType: asset.mimeType,
      createdAt: asset.createdAt.toISOString(),
    }
    await writeFile(metaPath, JSON.stringify(metadata))
    console.log('💾 Asset stored to file:', assetPath)
  },

  async get(id: string): Promise<StoredAsset | undefined> {
    const assetPath = path.join(ASSETS_DIR, `${id}.bin`)
    const metaPath = path.join(ASSETS_DIR, `${id}.meta.json`)

    try {
      const data = await readFile(assetPath)
      const metaData = JSON.parse(await readFile(metaPath, 'utf-8'))

      return {
        data,
        filename: metaData.filename,
        mimeType: metaData.mimeType,
        createdAt: new Date(metaData.createdAt),
      }
    } catch (_error) {
      console.log('❌ Asset not found in file system:', assetPath)
      return undefined
    }
  },

  async keys(): Promise<string[]> {
    try {
      const files = await readdir(ASSETS_DIR)
      return files.filter((f) => f.endsWith('.bin')).map((f) => f.replace('.bin', ''))
    } catch {
      return []
    }
  },

  get size(): number {
    try {
      return fs.readdirSync(ASSETS_DIR).filter((f) => f.endsWith('.bin')).length
    } catch {
      return 0
    }
  },
}

// Clean up old assets every hour
setInterval(
  async () => {
    const cutoff = Date.now() - 60 * 60 * 1000 // 1 hour ago
    try {
      const files = await readdir(ASSETS_DIR)
      for (const file of files) {
        if (!file.endsWith('.bin')) continue

        const filePath = path.join(ASSETS_DIR, file)
        const stats = await stat(filePath)

        if (stats.mtime.getTime() < cutoff) {
          const id = file.replace('.bin', '')
          await unlink(filePath).catch(() => {})
          await unlink(path.join(ASSETS_DIR, `${id}.meta.json`)).catch(() => {})
          console.log('🗑️ Cleaned up old asset:', id)
        }
      }
    } catch (error) {
      console.error('❌ Error cleaning up assets:', error)
    }
  },
  60 * 60 * 1000
)