forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroute.ts
More file actions
122 lines (109 loc) · 3.92 KB
/
Copy pathroute.ts
File metadata and controls
122 lines (109 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// src/app/api/settings/backup/export/route.ts
import { mkdir, writeFile } from "node:fs/promises"
import nodePath from "node:path"
import { NextResponse } from "next/server"
import { authenticate, decodeKey } from "@/lib/api-helpers"
import { encryptBackupPayload, generateBackupPayload, resolveBackupPassword } from "@/lib/backup"
import { db } from "@/lib/db"
import { appSettings, backupHistory } from "@/lib/db/schema"
import { BACKUP_PASSWORD_MAX } from "@/lib/limits"
import { log } from "@/lib/logger"
export async function POST(request: Request) {
const auth = await authenticate()
if (auth instanceof NextResponse) return auth
try {
const formData = await request.formData()
const formPassword = formData.get("backupPassword")
const [settings] = await db.select().from(appSettings).limit(1)
const storagePath = settings?.backupStoragePath ?? "/data/backups"
const payload = await generateBackupPayload()
// Resolve backup password: explicit form value > stored encrypted password
let backupPassword: string | null = null
if (formPassword && typeof formPassword === "string" && formPassword.length > 0) {
if (formPassword.length > BACKUP_PASSWORD_MAX) {
return NextResponse.json(
{ error: "Backup password must be 128 characters or fewer" },
{ status: 400 }
)
}
backupPassword = formPassword
} else if (settings?.backupEncryptionEnabled && settings.encryptedBackupPassword) {
try {
backupPassword = resolveBackupPassword(
true,
settings.encryptedBackupPassword,
decodeKey(auth)
)
} catch {
log.error("Failed to decrypt stored backup password for manual export")
return NextResponse.json(
{
error: "Failed to decrypt backup password. Re-enter your backup password in settings.",
},
{ status: 500 }
)
}
}
let serialized: string
let contentType: string
let ext: string
let encrypted = false
if (backupPassword) {
const envelope = await encryptBackupPayload(payload, backupPassword)
serialized = JSON.stringify(envelope)
contentType = "application/octet-stream"
ext = "ttbak"
encrypted = true
} else {
serialized = JSON.stringify(payload)
contentType = "application/json"
ext = "json"
}
const sizeBytes = Buffer.byteLength(serialized, "utf8")
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const filename = `tracker-tracker-backup-${timestamp}.${ext}`
// Save to disk alongside browser download
let filePath: string | null = null
try {
await mkdir(storagePath, { recursive: true })
filePath = nodePath.join(storagePath, filename)
await writeFile(filePath, serialized, "utf8")
log.info(`Manual backup saved: ${filePath} (${sizeBytes} bytes)`)
} catch (fsErr) {
log.error(fsErr, "Failed to save manual backup to disk — browser download will still proceed")
filePath = null
}
await db.insert(backupHistory).values({
sizeBytes,
encrypted,
frequency: null,
status: filePath ? "completed" : "disk_write_failed",
storagePath: filePath,
})
if (filePath) {
// Saved to disk — return JSON success (no browser download)
return NextResponse.json({
success: true,
filename,
sizeBytes,
storagePath: filePath,
})
}
// Disk write failed or no storage path — fall through to browser download
return new Response(serialized, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
})
} catch (err) {
log.error(
{
route: "POST /api/settings/backup/export",
error: String(err),
},
"backup export failed"
)
return NextResponse.json({ error: "Backup export failed" }, { status: 500 })
}
}