diff --git a/.gitignore b/.gitignore index c1cf022..c6517ba 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ clients/nim/tests/test_client src/baradadb src/barabadb/client/client src/barabadb/core/raft +src/barabadb/core/backup # Temp *.tmp diff --git a/src/barabadb/core/backup.nim b/src/barabadb/core/backup.nim index 238101f..19cc7a9 100644 --- a/src/barabadb/core/backup.nim +++ b/src/barabadb/core/backup.nim @@ -151,28 +151,37 @@ proc formatTimestamp*(ts: int64): string = result = $ts proc parseBackupFilename*(filename: string): int64 = - ## Try to extract timestamp from backup_1234567890.tar.gz + ## Try to extract timestamp from backup_1234567890.tar.gz (or .tar) try: let name = extractFilename(filename) if name.startsWith("backup_"): - let tsStr = name[7..^8] # skip "backup_" and ".tar.gz" - result = parseBiggestInt(tsStr) + let tsStr = if name.endsWith(".tar.gz"): name[7..^8] + elif name.endsWith(".tar"): name[7..^5] + else: "" + if tsStr.len > 0: + result = parseBiggestInt(tsStr) + else: + result = 0 else: result = 0 except: result = 0 proc getArchiveSize*(input: string): int64 = - ## Return uncompressed size estimate from tar archive - let cmd = "tar -tzf " & quoteShell(input) & " | wc -l" - let (outStr, exitCode) = execCmdEx(cmd) - if exitCode == 0: - try: - result = parseBiggestInt(strip(outStr)) - except: - result = 0 + ## Return uncompressed size in bytes from archive. + ## Uses gzip -l for .gz; falls back to file size for plain .tar. + if input.endsWith(".gz"): + let cmd = "gzip -l " & quoteShell(input) & " 2>/dev/null | awk 'NR==2{print $2}'" + let (outStr, exitCode) = execCmdEx(cmd) + if exitCode == 0: + try: + result = parseBiggestInt(strip(outStr)) + except: + result = getFileSize(input) # fallback + else: + result = getFileSize(input) else: - result = 0 + result = getFileSize(input) proc getFreeSpace*(path: string): int64 = ## Return free disk space in bytes for the filesystem containing path @@ -287,7 +296,7 @@ proc restoreDataDir*(input: string, dataDir: string, verbose: bool = false, dryR createDir(dataDir) - let cmd = "tar -xzf " & quoteShell(input) & " -C " & quoteShell(dataDir) + let cmd = "tar -xzf " & quoteShell(input) & " --strip-components=1 -C " & quoteShell(dataDir) if verbose: echo "Running: ", cmd @@ -523,7 +532,6 @@ proc backupAllDatabases*(dataRoot: string, output: string, excludes: seq[string] if fileExists(output): echo "WARNING: Overwriting existing file: ", output - let workDir = getCurrentDir() let tempDir = getTempDir() / "baradb_backup_" & $getTime().toUnix() createDir(tempDir / "databases") @@ -608,7 +616,7 @@ proc restoreAllDatabases*(input: string, dataRoot: string, verbose: bool = false createDir(dataRoot) - let cmd = "tar -xzf " & quoteShell(input) & " -C " & quoteShell(dataRoot) + let cmd = "tar -xzf " & quoteShell(input) & " -C " & quoteShell(parentDir(dataRoot)) if verbose: echo "Running: ", cmd @@ -625,8 +633,8 @@ proc restoreAllDatabases*(input: string, dataRoot: string, verbose: bool = false echo "Rollback complete. Data restored to previous state." return false - # Verify metadata - let metaPath = dataRoot / BACKUP_META_FILE + # Verify metadata (extracted to parent dir due to archive layout) + let metaPath = parentDir(dataRoot) / BACKUP_META_FILE if fileExists(metaPath): try: let meta = parseJson(readFile(metaPath)) diff --git a/src/barabadb/core/httpserver.nim b/src/barabadb/core/httpserver.nim index f986870..6252c3e 100644 --- a/src/barabadb/core/httpserver.nim +++ b/src/barabadb/core/httpserver.nim @@ -524,6 +524,12 @@ proc restoreHandler(server: HttpServer): RequestHandler = let dbName = if body != nil and "database" in body: body["database"].getStr() else: "" let dataRoot = server.registry.dataRoot try: + # Verify archive integrity first + if not verifyArchive(inputFile, false): + logRestore(inputFile, dataRoot, false) + ctx.json(%*{"error": "Archive verification failed — file may be corrupted"}, 500) + return + let meta = readBackupMeta(inputFile) let isMultiDb = meta != nil and meta{"databases"} != nil var ok = false @@ -534,6 +540,8 @@ proc restoreHandler(server: HttpServer): RequestHandler = ok = restoreDataDir(inputFile, dbDir, false, false) else: ok = restoreAllDatabases(inputFile, dataRoot, false, false) + + logRestore(inputFile, dataRoot, ok) if ok: # Reload databases after restore server.registry.loadExistingDatabases()