File size: 5,686 Bytes
163c686 29ea797 163c686 c70e382 4afcd55 163c686 4afcd55 163c686 c70e382 163c686 c70e382 163c686 5e8ee96 163c686 c70e382 4afcd55 163c686 c70e382 4afcd55 163c686 4afcd55 163c686 c70e382 163c686 29ea797 163c686 | 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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | import { spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, renameSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
const root = process.cwd();
const dataDir = join(root, "data");
const dbPath = join(dataDir, "notes.db");
const remoteFolder = sanitizeRemoteFolder(process.env.REMOTE_FOLDER || "huggingface:notes");
const backupFile = "notes.db";
const uploadFile = "notes.db.uploading";
const mode = process.argv.includes("--restore") ? "restore" : "backup";
try {
if (mode === "restore") {
await restoreBackup();
} else {
await uploadBackup();
}
} catch (error) {
console.error(JSON.stringify({
ok: false,
mode,
error: error.message
}));
process.exitCode = 1;
}
async function restoreBackup() {
mkdirSync(dataDir, { recursive: true });
const remote = remotePath(backupFile);
const workDir = mkdtempSync(join(tmpdir(), "notes-restore-"));
const restorePath = join(workDir, "notes.db");
const copied = await rclone(["copyto", remote, restorePath], { allowFailure: true });
if (!copied.ok) {
rmSync(workDir, { recursive: true, force: true });
console.log(JSON.stringify({ ok: true, mode: "restore", restored: false, reason: "remote_backup_unavailable" }));
return;
}
assertUsableSqlite(restorePath);
const restoredSummary = inspectDatabase(restorePath);
const bytes = statSync(restorePath).size;
renameSync(restorePath, dbPath);
rmSync(workDir, { recursive: true, force: true });
console.log(JSON.stringify({ ok: true, mode: "restore", restored: true, remote, bytes, ...restoredSummary }));
}
async function uploadBackup() {
if (!existsSync(dbPath) || statSync(dbPath).size === 0) {
throw new Error("local_database_missing");
}
const localSummary = inspectDatabase(dbPath);
if (localSummary.users === 0) {
console.log(JSON.stringify({ ok: true, mode: "backup", skipped: true, reason: "database_unconfigured", ...localSummary }));
return;
}
const workDir = mkdtempSync(join(tmpdir(), "notes-backup-"));
const snapshotPath = join(workDir, "notes.db");
createSnapshot(snapshotPath);
const bytes = statSync(snapshotPath).size;
const snapshotSummary = inspectDatabase(snapshotPath);
const uploading = remotePath(uploadFile);
const target = remotePath(backupFile);
await rclone(["copyto", snapshotPath, uploading]);
await rclone(["deletefile", target], { allowFailure: true });
await rclone(["moveto", uploading, target]);
await rclone(["touch", target], { allowFailure: true });
const remoteSummary = await verifyRemoteBackup(target, snapshotSummary);
rmSync(workDir, { recursive: true, force: true });
console.log(JSON.stringify({
ok: true,
mode: "backup",
remote: target,
bytes,
...snapshotSummary,
remoteVerified: true,
remoteUsers: remoteSummary.users,
remoteNotes: remoteSummary.notes,
remoteFolders: remoteSummary.folders,
updatedAt: Date.now()
}));
}
async function verifyRemoteBackup(target, expected) {
const workDir = mkdtempSync(join(tmpdir(), "notes-verify-"));
const verifyPath = join(workDir, "notes.db");
try {
await rclone(["copyto", target, verifyPath]);
assertUsableSqlite(verifyPath);
const actual = inspectDatabase(verifyPath);
if (actual.users !== expected.users || actual.notes !== expected.notes || actual.folders !== expected.folders) {
throw new Error("remote_backup_verify_mismatch");
}
return actual;
} finally {
rmSync(workDir, { recursive: true, force: true });
}
}
function createSnapshot(snapshotPath) {
const db = new DatabaseSync(dbPath);
try {
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
db.exec(`VACUUM INTO '${escapeSqlitePath(snapshotPath)}'`);
} finally {
db.close();
}
assertUsableSqlite(snapshotPath);
}
function assertUsableSqlite(path) {
const db = new DatabaseSync(path);
try {
db.prepare("SELECT name FROM sqlite_master LIMIT 1").all();
} finally {
db.close();
}
}
function inspectDatabase(path) {
const db = new DatabaseSync(path);
try {
return {
users: countRows(db, "users"),
notes: countRows(db, "notes"),
folders: countRows(db, "folders")
};
} finally {
db.close();
}
}
function countRows(db, table) {
const exists = db.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
if (!exists?.count) return 0;
return db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count;
}
function rclone(args, { allowFailure = false } = {}) {
return new Promise((resolve, reject) => {
const child = spawn("rclone", args, {
env: process.env,
stdio: ["ignore", "pipe", "pipe"]
});
let stdout = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", () => {});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0 || allowFailure) {
resolve({ ok: code === 0, stdout });
return;
}
reject(new Error(`rclone_failed_${code}`));
});
});
}
function remotePath(file) {
return `${remoteFolder.replace(/\/+$/, "")}/${file}`;
}
function sanitizeRemoteFolder(value) {
const trimmed = String(value || "").trim();
if (!trimmed) throw new Error("remote_folder_missing");
if (/[\0\r\n]/.test(trimmed)) throw new Error("remote_folder_invalid");
if (!trimmed.includes(":")) throw new Error("remote_folder_must_include_remote_name");
return trimmed;
}
function escapeSqlitePath(path) {
return path.replaceAll("'", "''");
}
|