Files
zhimingge/lib/history/server-store.ts
T

109 lines
2.9 KiB
TypeScript

import fs from "fs/promises";
import path from "path";
import { randomUUID } from "crypto";
import {
HISTORY_MAX_ITEMS,
type CalcHistoryCreate,
type CalcHistoryEntry,
} from "@/lib/history/types";
function getDataDir(): string {
const configured = process.env.HISTORY_DATA_DIR?.trim();
if (configured) {
return configured;
}
return path.join(process.cwd(), "data", "history");
}
export function historyStoreErrorMessage(err: unknown): string {
if (!(err instanceof Error)) {
return "保存测算历史失败";
}
const code = (err as NodeJS.ErrnoException).code;
const dir = getDataDir();
if (code === "EACCES" || code === "EPERM") {
return `测算历史目录无写入权限(${dir}),请检查 HISTORY_DATA_DIR 或目录权限`;
}
if (code === "ENOENT") {
return `测算历史目录不存在(${dir}),请创建目录或配置 HISTORY_DATA_DIR`;
}
if (code === "ENOSPC") {
return "磁盘空间不足,无法保存测算历史";
}
return `保存测算历史失败:${err.message}`;
}
function sanitizeUsername(username: string): string {
const safe = username.replace(/[^a-zA-Z0-9_-]/g, "_");
return safe || "guest";
}
function userFile(username: string): string {
return path.join(getDataDir(), `${sanitizeUsername(username)}.json`);
}
async function ensureDir(): Promise<void> {
await fs.mkdir(getDataDir(), { recursive: true });
}
async function readUserHistory(username: string): Promise<CalcHistoryEntry[]> {
await ensureDir();
try {
const raw = await fs.readFile(userFile(username), "utf-8");
const parsed = JSON.parse(raw) as CalcHistoryEntry[];
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw error;
}
}
async function writeUserHistory(
username: string,
entries: CalcHistoryEntry[],
): Promise<void> {
await ensureDir();
const file = userFile(username);
const tmp = `${file}.tmp`;
await fs.writeFile(tmp, JSON.stringify(entries, null, 2), "utf-8");
await fs.rename(tmp, file);
}
export async function listHistoryEntries(
username: string,
): Promise<CalcHistoryEntry[]> {
return readUserHistory(username);
}
export async function addHistoryEntry(
username: string,
entry: CalcHistoryCreate,
): Promise<CalcHistoryEntry> {
const full: CalcHistoryEntry = {
...entry,
id: randomUUID(),
createdAt: new Date().toISOString(),
};
const list = [full, ...(await readUserHistory(username))].slice(
0,
HISTORY_MAX_ITEMS,
);
await writeUserHistory(username, list);
return full;
}
export async function deleteHistoryEntry(
username: string,
id: string,
): Promise<boolean> {
const current = await readUserHistory(username);
const next = current.filter((entry) => entry.id !== id);
if (next.length === current.length) {
return false;
}
await writeUserHistory(username, next);
return true;
}