Files
blog/write-server/src/lib/recycle.ts
T
2026-06-22 00:41:05 +08:00

158 lines
4.1 KiB
TypeScript

import fs from "fs";
import path from "path";
import {
RECYCLE_DIR,
RECYCLE_POSTS_DIR,
RECYCLE_META_DIR,
RECYCLE_DAYS,
} from "./config";
export interface RecycleItem {
slug: string;
title: string;
deletedAt: string; // ISO date
originalPath: string;
expiresAt: string; // ISO date
daysRemaining: number;
}
export function initRecycleBin(): void {
if (!fs.existsSync(RECYCLE_DIR)) {
fs.mkdirSync(RECYCLE_DIR, { recursive: true });
}
if (!fs.existsSync(RECYCLE_POSTS_DIR)) {
fs.mkdirSync(RECYCLE_POSTS_DIR, { recursive: true });
}
if (!fs.existsSync(RECYCLE_META_DIR)) {
fs.mkdirSync(RECYCLE_META_DIR, { recursive: true });
}
}
export function moveToRecycle(
slug: string,
sourceDir: string,
title: string
): void {
initRecycleBin();
const now = new Date();
const expires = new Date(now);
expires.setDate(expires.getDate() + RECYCLE_DAYS);
const meta: Omit<RecycleItem, "daysRemaining"> = {
slug,
title,
deletedAt: now.toISOString(),
originalPath: sourceDir,
expiresAt: expires.toISOString(),
};
// Move post directory to recycle (fallback to copy+delete for cross-filesystem)
const targetDir = path.join(RECYCLE_POSTS_DIR, slug);
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true });
}
try {
fs.renameSync(sourceDir, targetDir);
} catch (_e) {
// Cross-filesystem: copy then delete
fs.cpSync(sourceDir, targetDir, { recursive: true });
fs.rmSync(sourceDir, { recursive: true, force: true });
}
// Save metadata
const metaPath = path.join(RECYCLE_META_DIR, `${slug}.json`);
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
}
export function listRecycle(): RecycleItem[] {
initRecycleBin();
if (!fs.existsSync(RECYCLE_META_DIR)) return [];
const items: RecycleItem[] = [];
const files = fs.readdirSync(RECYCLE_META_DIR).filter((f) => f.endsWith(".json"));
for (const file of files) {
const metaPath = path.join(RECYCLE_META_DIR, file);
const raw = fs.readFileSync(metaPath, "utf-8");
const meta = JSON.parse(raw);
const now = new Date();
const expires = new Date(meta.expiresAt);
const daysRemaining = Math.max(
0,
Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
);
items.push({ ...meta, daysRemaining });
}
items.sort(
(a, b) =>
new Date(b.deletedAt).getTime() - new Date(a.deletedAt).getTime()
);
return items;
}
export function restoreFromRecycle(slug: string): boolean {
const metaPath = path.join(RECYCLE_META_DIR, `${slug}.json`);
if (!fs.existsSync(metaPath)) return false;
const raw = fs.readFileSync(metaPath, "utf-8");
const meta = JSON.parse(raw);
const recyclePostPath = path.join(RECYCLE_POSTS_DIR, slug);
if (!fs.existsSync(recyclePostPath)) return false;
// Ensure target directory exists
const targetDir = path.dirname(meta.originalPath);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// If target already exists, remove it first
if (fs.existsSync(meta.originalPath)) {
fs.rmSync(meta.originalPath, { recursive: true, force: true });
}
try {
fs.renameSync(recyclePostPath, meta.originalPath);
} catch (_e) {
fs.cpSync(recyclePostPath, meta.originalPath, { recursive: true });
fs.rmSync(recyclePostPath, { recursive: true, force: true });
}
fs.unlinkSync(metaPath);
return true;
}
export function permanentDelete(slug: string): boolean {
const metaPath = path.join(RECYCLE_META_DIR, `${slug}.json`);
const recyclePostPath = path.join(RECYCLE_POSTS_DIR, slug);
let deleted = false;
if (fs.existsSync(recyclePostPath)) {
fs.rmSync(recyclePostPath, { recursive: true, force: true });
deleted = true;
}
if (fs.existsSync(metaPath)) {
fs.unlinkSync(metaPath);
deleted = true;
}
return deleted;
}
export function cleanExpired(): number {
const items = listRecycle();
let count = 0;
for (const item of items) {
if (item.daysRemaining <= 0) {
permanentDelete(item.slug);
count++;
}
}
return count;
}