增加API分页和轻量级查询支持,完善备份恢复功能
主要改动: - API: 为 /api/pages 添加 lightweight 模式和分页支持 - 备份恢复: 增强 restore API 的错误处理和验证逻辑 - 工具函数: 新增 page-utils 辅助函数 - 会话管理: 优化 session 和 auth 相关逻辑 - 文档: 添加 CLAUDE.md 项目指南,包含编辑器配置注意事项 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,61 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { requireApiAuth } from '@/lib/api-auth';
|
||||
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
||||
import { MAX_TITLE_LENGTH, safeParseTags, normalizeTags } from '@/lib/page-utils';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = await requireApiAuth();
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const pages = await prisma.page.findMany({
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
// lightweight 模式:不返回 content,用于侧边栏等场景
|
||||
const lightweight = searchParams.get('lightweight') === 'true';
|
||||
const page = parseInt(searchParams.get('page') || '0', 10);
|
||||
const limit = parseInt(searchParams.get('limit') || '0', 10);
|
||||
|
||||
// 默认行为:返回所有页面(向后兼容)
|
||||
const usePagination = limit > 0 && limit <= 1000 && page > 0;
|
||||
|
||||
if (!usePagination) {
|
||||
const pages = await prisma.page.findMany({
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
const parsedPages = pages.map((p) => ({
|
||||
...p,
|
||||
content: lightweight ? '' : p.content,
|
||||
tags: safeParseTags(p.tags),
|
||||
}));
|
||||
return NextResponse.json(parsedPages);
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
const skip = (page - 1) * limit;
|
||||
const [pages, total] = await Promise.all([
|
||||
prisma.page.findMany({
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||
}),
|
||||
prisma.page.count(),
|
||||
]);
|
||||
|
||||
const parsedPages = pages.map((p) => ({
|
||||
...p,
|
||||
content: lightweight ? '' : p.content,
|
||||
tags: safeParseTags(p.tags),
|
||||
}));
|
||||
return NextResponse.json(parsedPages);
|
||||
|
||||
return NextResponse.json({
|
||||
data: parsedPages,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Error fetching pages' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import JSZip from "jszip";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireApiAuth } from "@/lib/api-auth";
|
||||
import { markdownToImportHtml } from "@/lib/markdown-import";
|
||||
import { pageToMarkdown } from "@/lib/export";
|
||||
import { sanitizeFilename } from "@/lib/page-utils";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
type ZipEntry = {
|
||||
path: string;
|
||||
@@ -93,6 +97,52 @@ function collectFolderPaths(entries: ZipEntry[]): string[] {
|
||||
return Array.from(folderSet).sort((a, b) => getDepth(a) - getDepth(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建当前数据库的备份 ZIP
|
||||
*/
|
||||
async function createBackupZip(): Promise<Buffer> {
|
||||
const allPages = await prisma.page.findMany({
|
||||
orderBy: [{ parentId: "asc" }, { order: "asc" }],
|
||||
});
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
// 递归添加页面到 ZIP
|
||||
function addPagesToZip(parentId: string | null, currentFolder: JSZip) {
|
||||
const children = allPages.filter((p) => p.parentId === parentId);
|
||||
|
||||
for (const page of children) {
|
||||
const cleanTitle = sanitizeFilename(page.title);
|
||||
|
||||
if (page.type === "folder") {
|
||||
const subFolder = currentFolder.folder(cleanTitle);
|
||||
if (subFolder) {
|
||||
addPagesToZip(page.id, subFolder);
|
||||
}
|
||||
} else {
|
||||
const markdown = pageToMarkdown({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
parentId: page.parentId,
|
||||
type: page.type as "file" | "folder",
|
||||
tags: JSON.parse(page.tags),
|
||||
icon: page.icon,
|
||||
order: page.order,
|
||||
isLocked: page.isLocked,
|
||||
createdAt: page.createdAt.toISOString(),
|
||||
updatedAt: page.updatedAt.toISOString(),
|
||||
});
|
||||
currentFolder.file(`${cleanTitle}.md`, markdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPagesToZip(null, zip);
|
||||
|
||||
return await zip.generateAsync({ type: "nodebuffer" });
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const authError = await requireApiAuth();
|
||||
if (authError) return authError;
|
||||
@@ -152,10 +202,31 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
);
|
||||
|
||||
// 在删除数据前创建自动备份
|
||||
let backupBuffer: Buffer | null = null;
|
||||
let backupPath: string | null = null;
|
||||
try {
|
||||
backupBuffer = await createBackupZip();
|
||||
|
||||
// 保存备份到 backups 目录
|
||||
const backupsDir = join(process.cwd(), "backups");
|
||||
await mkdir(backupsDir, { recursive: true });
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
backupPath = join(backupsDir, `auto-backup-before-restore-${timestamp}.zip`);
|
||||
await writeFile(backupPath, backupBuffer);
|
||||
|
||||
console.log(`Auto backup created: ${backupPath}`);
|
||||
} catch (backupError) {
|
||||
console.error("Failed to create backup before restore:", backupError);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create backup before restore. Restore aborted for safety." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// ⚠️ 注意:这是一个破坏性操作,会删除所有现有页面后重新导入。
|
||||
// TODO: 后续可在此添加自动备份逻辑(导出当前数据到临时文件),
|
||||
// 或在前端恢复前强制用户先手动备份。
|
||||
// 删除所有现有页面(已在上面创建备份)
|
||||
await tx.page.deleteMany();
|
||||
|
||||
const folderIdMap = new Map<string, string>();
|
||||
@@ -195,6 +266,7 @@ export async function POST(req: NextRequest) {
|
||||
count: entries.length,
|
||||
files: parsedFiles.length,
|
||||
folders: folderPaths.length,
|
||||
backupPath: backupPath ? backupPath.replace(process.cwd(), "") : null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Restore failed:", error);
|
||||
|
||||
Reference in New Issue
Block a user