feat:: more robust backup

This commit is contained in:
Vasily Zubarev
2025-03-22 12:50:29 +01:00
parent 2de102d0dc
commit dc45fc23f4
6 changed files with 40 additions and 47 deletions

View File

@@ -1,3 +1,4 @@
import { DATABASE_FILE } from "@/lib/db"
import { FILE_UPLOAD_PATH } from "@/lib/files" import { FILE_UPLOAD_PATH } from "@/lib/files"
import fs, { readdirSync } from "fs" import fs, { readdirSync } from "fs"
import JSZip from "jszip" import JSZip from "jszip"
@@ -7,22 +8,31 @@ import path from "path"
export async function GET(request: Request) { export async function GET(request: Request) {
try { try {
const zip = new JSZip() const zip = new JSZip()
const folder = zip.folder("uploads") const rootFolder = zip.folder("data")
if (!folder) { if (!rootFolder) {
console.error("Failed to create zip folder") console.error("Failed to create zip folder")
return new NextResponse("Internal Server Error", { status: 500 }) return new NextResponse("Internal Server Error", { status: 500 })
} }
const files = getAllFilePaths(FILE_UPLOAD_PATH) const databaseFile = fs.readFileSync(DATABASE_FILE)
files.forEach((file) => { rootFolder.file("database.sqlite", databaseFile)
folder.file(file.replace(FILE_UPLOAD_PATH, ""), fs.readFileSync(file))
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" }) const archive = await zip.generateAsync({ type: "blob" })
return new NextResponse(archive, { return new NextResponse(archive, {
headers: { headers: {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="uploads.zip"`, "Content-Disposition": `attachment; filename="data.zip"`,
}, },
}) })
} catch (error) { } catch (error) {

View File

@@ -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 })
}
}

View File

@@ -3,7 +3,7 @@
import { FormError } from "@/components/forms/error" import { FormError } from "@/components/forms/error"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { Download, Loader2 } from "lucide-react" import { Download } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useActionState } from "react" import { useActionState } from "react"
import { restoreBackupAction } from "./actions" import { restoreBackupAction } from "./actions"
@@ -16,29 +16,25 @@ export default function BackupSettingsPage() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-bold">Download backup</h1> <h1 className="text-2xl font-bold">Download backup</h1>
<div className="flex flex-row gap-4"> <div className="flex flex-row gap-4">
<Link href="/settings/backups/database"> <Link href="/settings/backups/data">
<Button> <Button>
<Download /> Download database.sqlite <Download /> Download data directory
</Button>
</Link>
<Link href="/settings/backups/files">
<Button>
<Download /> Download files archive
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground max-w-xl">
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.
</div> </div>
</div> </div>
<Card className="flex flex-col gap-4 mt-24 p-5 bg-red-100 max-w-xl"> <Card className="flex flex-col gap-4 mt-16 p-5 bg-red-100 max-w-xl">
<h2 className="text-xl font-semibold">Restore database from backup</h2> <h2 className="text-xl font-semibold">How to restore from a backup</h2>
<div className="text-sm text-muted-foreground"> <div className="text-md">
Warning: This will overwrite your current database and destroy all the data! Don't forget to download backup This feature doesn't work automatically yet. Use your docker deployment with backup archive to manually put
first. database.sqlite and uploaded files into the paths specified in DATABASE_URL and UPLOAD_PATH
</div> </div>
<form action={restoreBackup}> {/* <form action={restoreBackup}>
<label> <label>
<input type="file" name="file" /> <input type="file" name="file" />
</label> </label>
@@ -51,7 +47,7 @@ export default function BackupSettingsPage() {
"Restore" "Restore"
)} )}
</Button> </Button>
</form> </form> */}
{restoreState?.error && <FormError>{restoreState.error}</FormError>} {restoreState?.error && <FormError>{restoreState.error}</FormError>}
</Card> </Card>
</div> </div>

View File

@@ -1,5 +1,3 @@
import { DATABASE_FILE } from "@/lib/db"
import fs from "fs"
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
export async function POST(request: Request) { export async function POST(request: Request) {
@@ -14,7 +12,8 @@ export async function POST(request: Request) {
const fileBuffer = await file.arrayBuffer() const fileBuffer = await file.arrayBuffer()
const fileData = Buffer.from(fileBuffer) 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 }) return new NextResponse("File restored", { status: 200 })
} catch (error) { } catch (error) {

View File

@@ -1,4 +1,5 @@
import { PrismaClient } from "@prisma/client" import { PrismaClient } from "@prisma/client"
import path from "path"
const globalForPrisma = globalThis as unknown as { const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined 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 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)
}

View File

@@ -305,7 +305,7 @@ const fields = [
code: "name", code: "name",
name: "Name", name: "Name",
type: "string", 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, isRequired: true,
isExtra: false, isExtra: false,
}, },
@@ -321,7 +321,7 @@ const fields = [
code: "merchant", code: "merchant",
name: "Merchant", name: "Merchant",
type: "string", type: "string",
llm_prompt: "merchant name", llm_prompt: "merchant name, use the original spelling and language",
isRequired: false, isRequired: false,
isExtra: false, isExtra: false,
}, },