#!/bin/sh set -eu retention_days="${MEDIA_TRASH_RETENTION_DAYS:-30}" case "$retention_days" in *[!0-9]*|'') echo "MEDIA_TRASH_RETENTION_DAYS 必须是非负整数" >&2; exit 2 ;; esac docker compose exec -T -e RETENTION_DAYS="$retention_days" web node - <<'NODE' const Database = require('better-sqlite3') const fs = require('node:fs') const path = require('node:path') const db = new Database('/app/data/db/huiyu.sqlite') const cutoff = new Date(Date.now() - Number(process.env.RETENTION_DAYS) * 86400000).toISOString() const rows = db.prepare("SELECT id, asset_key, original_path, derived_path, thumbnail_path FROM media_assets WHERE status = 'trash' AND deleted_at <= ?").all(cutoff) const root = path.resolve('/app/data/uploads/trash') const remove = relative => { const target = path.resolve(root, relative) if (!target.startsWith(root + path.sep)) throw new Error('unsafe media path') try { fs.unlinkSync(target) } catch (error) { if (error.code !== 'ENOENT') throw error } } const finish = db.transaction(row => { for (const relative of [row.original_path, row.derived_path, row.thumbnail_path]) remove(relative) db.prepare("UPDATE media_assets SET status = 'deleted', updated_at = ? WHERE id = ?").run(new Date().toISOString(), row.id) db.prepare("INSERT INTO audit_logs (action, entity_type, entity_key, metadata_json, created_at) VALUES ('media.purge', 'media', ?, '{}', ?)").run(row.asset_key, new Date().toISOString()) }) for (const row of rows) finish(row) db.close() console.log(`已物理清理 ${rows.length} 个回收站素材`) NODE