import { createReadStream, promises as fs } from 'node:fs' import type { ReadStream } from 'node:fs' import { dirname, join } from 'node:path' import { contentTypeFromKey } from '../content-type' import { getDataDir } from '../data-dir' import { validateStorageKey } from '../image-key' import type { StoredImage, StoredImageMeta, StorageBackend } from './types' const META_SUFFIX = '.meta.json' function keyToFilePath(key: string): string { return join(getDataDir(), ...key.split('/')) } async function readMeta(key: string): Promise> { try { const raw = await fs.readFile(keyToFilePath(key) + META_SUFFIX, 'utf8') return JSON.parse(raw) as Partial } catch { return {} } } export class LocalStorageBackend implements StorageBackend { readonly id: string readonly type = 'local' as const readonly servingMode: 'proxy' | 'public' readonly publicUrl: string constructor( id = 'local', servingMode: 'proxy' | 'public' = 'proxy', publicUrl = '' ) { this.id = id this.servingMode = servingMode this.publicUrl = publicUrl } async put(key: string, bytes: Uint8Array, _meta: StoredImageMeta): Promise { const filePath = keyToFilePath(key) await fs.mkdir(dirname(filePath), { recursive: true }) await fs.writeFile(filePath, bytes) } async head(key: string): Promise { if (!validateStorageKey(key)) return null let stat try { stat = await fs.stat(keyToFilePath(key)) } catch { return null } if (!stat.isFile()) return null const meta = await readMeta(key) return { key, size: stat.size, mtimeMs: stat.mtimeMs, contentType: meta.contentType ?? contentTypeFromKey(key), originalName: meta.originalName ?? key.split('/').pop() ?? 'image', uploadedAt: meta.uploadedAt ?? stat.mtime.toISOString(), userId: meta.userId ?? null, backendId: this.id } } async delete(key: string): Promise { const filePath = keyToFilePath(key) await fs.unlink(filePath) await fs.unlink(filePath + META_SUFFIX).catch(() => {}) } async createStream( key: string, range?: { start: number, end: number } ): Promise { return createReadStream(keyToFilePath(key), range) } async testConnection(): Promise<{ ok: boolean, message: string }> { try { await fs.access(getDataDir()) return { ok: true, message: '本地磁盘可访问' } } catch { return { ok: false, message: '无法访问数据目录' } } } async getUsageStats(): Promise<{ count: number, bytes: number }> { return { count: 0, bytes: 0 } } }