首次发布git
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { hashPassword } from "@/lib/auth";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const count = await prisma.globalSettings.count();
|
||||
if (count > 0) {
|
||||
return NextResponse.json({ message: "Settings already initialized" }, { status: 200 });
|
||||
}
|
||||
|
||||
// Default password: "admin"
|
||||
const hashedPassword = await hashPassword("admin");
|
||||
await prisma.globalSettings.create({
|
||||
data: {
|
||||
id: "default",
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "Initialized default settings" }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Init Settings Error:", error);
|
||||
return NextResponse.json({ error: "Failed to initialize settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { hashPassword, verifyPassword } from "@/lib/auth";
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
try {
|
||||
const { currentPassword, newPassword } = await req.json();
|
||||
|
||||
// Cast to any to bypass build error until server restart allows prisma generate to run
|
||||
const settings = await (prisma as any).globalSettings.findUnique({
|
||||
where: { id: "default" },
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: "Settings not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(currentPassword, settings.password);
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: "Current password incorrect" }, { status: 401 });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
|
||||
await (prisma as any).globalSettings.update({
|
||||
where: { id: "default" },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to change password" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import JSZip from "jszip";
|
||||
import { marked } from "marked";
|
||||
|
||||
// Use a global prisma instance to avoid "too many connections" in dev
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
||||
const prisma = globalForPrisma.prisma || new PrismaClient();
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
|
||||
// Disable body parser strictly (Next.js App Router handles FormData naturally)
|
||||
// export const config = {
|
||||
// api: {
|
||||
// bodyParser: false,
|
||||
// },
|
||||
// };
|
||||
// No need for config in App Router route handlers.
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
||||
}
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
// Map to store directory paths to their new DB IDs
|
||||
// Data format: "folder/subfolder" -> UUID
|
||||
const pathIdMap = new Map<string, string>();
|
||||
|
||||
// Prepare data for proper insertion order (Folders first, then files?)
|
||||
// Actually, we need to process by path depth to ensure parents exist.
|
||||
const entries: Array<{ path: string; isDir: boolean; content?: string }> = [];
|
||||
|
||||
// 1. Read all entries
|
||||
const filePromises: Promise<void>[] = [];
|
||||
|
||||
zip.forEach((relativePath, zipEntry) => {
|
||||
if (relativePath.startsWith("__MACOSX") || relativePath.includes(".DS_Store")) {
|
||||
return; // Skip system files
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
if (zipEntry.dir) {
|
||||
// Remove trailing slash for consistency
|
||||
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
|
||||
if (cleanPath) {
|
||||
entries.push({ path: cleanPath, isDir: true });
|
||||
}
|
||||
} else {
|
||||
if (relativePath.endsWith(".md")) {
|
||||
const content = await zipEntry.async("string");
|
||||
entries.push({ path: relativePath, isDir: false, content });
|
||||
}
|
||||
}
|
||||
})();
|
||||
filePromises.push(promise);
|
||||
});
|
||||
|
||||
await Promise.all(filePromises);
|
||||
|
||||
// 2. Clear existing Pages (Transaction usually)
|
||||
// Since sqlite doesn't support nested transactions well in Prisma sometimes,
|
||||
// we'll just do it sequentially but quickly.
|
||||
// Ideally: await prisma.$transaction([prisma.page.deleteMany(), ...])
|
||||
// But logic is complex (recursive id generation), so we delete first.
|
||||
// WARNING: This is destructive.
|
||||
await prisma.page.deleteMany();
|
||||
|
||||
// 3. Sort entries by path depth (number of slashes)
|
||||
entries.sort((a, b) => {
|
||||
const depthA = a.path.split('/').length;
|
||||
const depthB = b.path.split('/').length;
|
||||
return depthA - depthB;
|
||||
});
|
||||
|
||||
// Helper to get or create parent folder
|
||||
const ensureParent = async (entryPath: string): Promise<string | null> => {
|
||||
const parts = entryPath.split('/');
|
||||
if (parts.length <= 1) return null; // Root level
|
||||
|
||||
const parentPath = parts.slice(0, -1).join('/');
|
||||
|
||||
// If parent already processed
|
||||
if (pathIdMap.has(parentPath)) {
|
||||
return pathIdMap.get(parentPath)!;
|
||||
}
|
||||
|
||||
// If parent folder was not explicitly in Zip (implicit folder), create it
|
||||
// Recursively ensure its parent exists
|
||||
const grandParentId = await ensureParent(parentPath);
|
||||
const folderName = parts[parts.length - 2];
|
||||
|
||||
const newFolder = await prisma.page.create({
|
||||
data: {
|
||||
title: folderName,
|
||||
type: 'folder',
|
||||
parentId: grandParentId
|
||||
}
|
||||
});
|
||||
|
||||
pathIdMap.set(parentPath, newFolder.id);
|
||||
return newFolder.id;
|
||||
};
|
||||
|
||||
// 4. Process entries
|
||||
for (const entry of entries) {
|
||||
// Determine parent
|
||||
// If it's a file "A/B.md", parent path is "A".
|
||||
// If it's a folder "A/B", parent path is "A".
|
||||
// Since we sorted by depth, "A" should be processed before "A/B".
|
||||
|
||||
// However, implicit folders might be skipped in sorting if they aren't in `entries`.
|
||||
// So `ensureParent` handles implicit creation.
|
||||
|
||||
const parentId = await ensureParent(entry.path);
|
||||
|
||||
if (entry.isDir) {
|
||||
// Check if already created by ensureParent
|
||||
if (!pathIdMap.has(entry.path)) {
|
||||
const name = entry.path.split('/').pop() || "Untitled Folder";
|
||||
const folder = await prisma.page.create({
|
||||
data: {
|
||||
title: name,
|
||||
type: 'folder',
|
||||
parentId: parentId
|
||||
}
|
||||
});
|
||||
pathIdMap.set(entry.path, folder.id);
|
||||
}
|
||||
} else {
|
||||
// It is a File (.md)
|
||||
const filename = entry.path.split('/').pop()?.replace('.md', '') || "Untitled";
|
||||
|
||||
// Parse Frontmatter
|
||||
let title = filename;
|
||||
let tags: string[] = [];
|
||||
let order = 0;
|
||||
let markdownBody = entry.content || "";
|
||||
|
||||
// Regex for frontmatter
|
||||
const fmMatch = markdownBody.match(/^---\n([\s\S]*?)\n---\n/);
|
||||
if (fmMatch) {
|
||||
const fmString = fmMatch[1];
|
||||
markdownBody = markdownBody.slice(fmMatch[0].length);
|
||||
|
||||
// Simple parsing
|
||||
// title: "Foo"
|
||||
// tags: ["a", "b"]
|
||||
// order: 1
|
||||
|
||||
const titleMatch = fmString.match(/title:\s*"(.*)"/);
|
||||
if (titleMatch) title = titleMatch[1];
|
||||
|
||||
const tagsMatch = fmString.match(/tags:\s*\[(.*)\]/);
|
||||
if (tagsMatch) {
|
||||
// "a", "b" -> split
|
||||
tags = tagsMatch[1].split(',').map(s => s.trim().replace(/^"|"$/g, '')).filter(Boolean);
|
||||
}
|
||||
|
||||
const orderMatch = fmString.match(/order:\s*(\d+)/);
|
||||
if (orderMatch) order = parseInt(orderMatch[1]);
|
||||
}
|
||||
|
||||
// Convert Markdown to HTML for storage (Editor uses HTML)
|
||||
// Ensure GFM is enabled (default true in new versions, but explicit is good)
|
||||
// breaks: true converts \n to <br> (GitHub style)
|
||||
const htmlContent = await marked(markdownBody, { gfm: true, breaks: true });
|
||||
|
||||
await prisma.page.create({
|
||||
data: {
|
||||
title: title,
|
||||
type: 'file',
|
||||
content: htmlContent,
|
||||
tags: JSON.stringify(tags),
|
||||
order: order,
|
||||
parentId: parentId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, count: entries.length });
|
||||
|
||||
} catch (e) {
|
||||
console.error("Restore failed:", e);
|
||||
return NextResponse.json({ error: "Restore failed: " + String(e) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user