From dc45fc23f4b44c67138bfd26dd21db2c48e6528c Mon Sep 17 00:00:00 2001 From: Vasily Zubarev Date: Sat, 22 Mar 2025 12:50:29 +0100 Subject: [PATCH] feat:: more robust backup --- app/settings/backups/{files => data}/route.ts | 22 ++++++++++---- app/settings/backups/database/route.ts | 18 ----------- app/settings/backups/page.tsx | 30 ++++++++----------- app/settings/backups/restore/route.ts | 5 ++-- lib/db.ts | 8 ++++- prisma/seed.js | 4 +-- 6 files changed, 40 insertions(+), 47 deletions(-) rename app/settings/backups/{files => data}/route.ts (64%) delete mode 100644 app/settings/backups/database/route.ts diff --git a/app/settings/backups/files/route.ts b/app/settings/backups/data/route.ts similarity index 64% rename from app/settings/backups/files/route.ts rename to app/settings/backups/data/route.ts index 6ec0211..61a5c8b 100644 --- a/app/settings/backups/files/route.ts +++ b/app/settings/backups/data/route.ts @@ -1,3 +1,4 @@ +import { DATABASE_FILE } from "@/lib/db" import { FILE_UPLOAD_PATH } from "@/lib/files" import fs, { readdirSync } from "fs" import JSZip from "jszip" @@ -7,22 +8,31 @@ import path from "path" export async function GET(request: Request) { try { const zip = new JSZip() - const folder = zip.folder("uploads") - if (!folder) { + const rootFolder = zip.folder("data") + if (!rootFolder) { console.error("Failed to create zip folder") return new NextResponse("Internal Server Error", { status: 500 }) } - const files = getAllFilePaths(FILE_UPLOAD_PATH) - files.forEach((file) => { - folder.file(file.replace(FILE_UPLOAD_PATH, ""), fs.readFileSync(file)) + const databaseFile = fs.readFileSync(DATABASE_FILE) + rootFolder.file("database.sqlite", databaseFile) + + const uploadsFolder = rootFolder.folder("uploads") + if (!uploadsFolder) { + console.error("Failed to create uploads folder") + return new NextResponse("Internal Server Error", { status: 500 }) + } + + const uploadedFiles = getAllFilePaths(FILE_UPLOAD_PATH) + uploadedFiles.forEach((file) => { + uploadsFolder.file(file.replace(FILE_UPLOAD_PATH, ""), fs.readFileSync(file)) }) const archive = await zip.generateAsync({ type: "blob" }) return new NextResponse(archive, { headers: { "Content-Type": "application/octet-stream", - "Content-Disposition": `attachment; filename="uploads.zip"`, + "Content-Disposition": `attachment; filename="data.zip"`, }, }) } catch (error) { diff --git a/app/settings/backups/database/route.ts b/app/settings/backups/database/route.ts deleted file mode 100644 index 65f5240..0000000 --- a/app/settings/backups/database/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DATABASE_FILE } from "@/lib/db" -import fs from "fs" -import { NextResponse } from "next/server" - -export async function GET(request: Request) { - try { - const file = fs.readFileSync(DATABASE_FILE) - return new NextResponse(file, { - headers: { - "Content-Type": "application/octet-stream", - "Content-Disposition": `attachment; filename="database.sqlite"`, - }, - }) - } catch (error) { - console.error("Error exporting database:", error) - return new NextResponse("Internal Server Error", { status: 500 }) - } -} diff --git a/app/settings/backups/page.tsx b/app/settings/backups/page.tsx index 617ee85..85955fc 100644 --- a/app/settings/backups/page.tsx +++ b/app/settings/backups/page.tsx @@ -3,7 +3,7 @@ import { FormError } from "@/components/forms/error" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" -import { Download, Loader2 } from "lucide-react" +import { Download } from "lucide-react" import Link from "next/link" import { useActionState } from "react" import { restoreBackupAction } from "./actions" @@ -16,29 +16,25 @@ export default function BackupSettingsPage() {

Download backup

- + - - -
-
- You can use any SQLite client to view the database.sqlite file contents +
+ The archive consists of all uploaded files and the SQLite database. You can view the contents of the database + using any SQLite viewer.
- -

Restore database from backup

-
- Warning: This will overwrite your current database and destroy all the data! Don't forget to download backup - first. + +

How to restore from a backup

+
+ This feature doesn't work automatically yet. Use your docker deployment with backup archive to manually put + database.sqlite and uploaded files into the paths specified in DATABASE_URL and UPLOAD_PATH
-
+ {/* @@ -51,7 +47,7 @@ export default function BackupSettingsPage() { "Restore" )} -
+ */} {restoreState?.error && {restoreState.error}}
diff --git a/app/settings/backups/restore/route.ts b/app/settings/backups/restore/route.ts index 8d1f95a..eb2f88c 100644 --- a/app/settings/backups/restore/route.ts +++ b/app/settings/backups/restore/route.ts @@ -1,5 +1,3 @@ -import { DATABASE_FILE } from "@/lib/db" -import fs from "fs" import { NextResponse } from "next/server" export async function POST(request: Request) { @@ -14,7 +12,8 @@ export async function POST(request: Request) { const fileBuffer = await file.arrayBuffer() const fileData = Buffer.from(fileBuffer) - fs.writeFileSync(DATABASE_FILE, fileData) + // TODO: Implement restore + // fs.writeFileSync(DATABASE_FILE, fileData) return new NextResponse("File restored", { status: 200 }) } catch (error) { diff --git a/lib/db.ts b/lib/db.ts index bbcbc99..9d0c05e 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,4 +1,5 @@ import { PrismaClient } from "@prisma/client" +import path from "path" const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined @@ -8,4 +9,9 @@ export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: ["query" if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma -export const DATABASE_FILE = process.env.DATABASE_URL?.split(":").pop() || "db.sqlite" +export let DATABASE_FILE = process.env.DATABASE_URL?.replace("file:", "") ?? "db.sqlite" +if (DATABASE_FILE?.startsWith("/")) { + DATABASE_FILE = path.resolve(process.cwd(), DATABASE_FILE) +} else { + DATABASE_FILE = path.resolve(process.cwd(), "prisma", DATABASE_FILE) +} diff --git a/prisma/seed.js b/prisma/seed.js index 3e47559..64373a8 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -305,7 +305,7 @@ const fields = [ code: "name", name: "Name", type: "string", - llm_prompt: "human readable name, summarize what is the invoice about", + llm_prompt: "human readable name, summarize what is bought in the invoice", isRequired: true, isExtra: false, }, @@ -321,7 +321,7 @@ const fields = [ code: "merchant", name: "Merchant", type: "string", - llm_prompt: "merchant name", + llm_prompt: "merchant name, use the original spelling and language", isRequired: false, isExtra: false, },