158 lines
4.1 KiB
TypeScript
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;
|
|
}
|