Skip to content

Commit cba08ad

Browse files
committed
Implement db restore
1 parent cbb22ad commit cba08ad

File tree

5 files changed

+196
-3
lines changed

5 files changed

+196
-3
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,11 @@ cat backup.db | kv db import -
462462
# Or simply:
463463
kv db import < backup.db
464464

465+
# Restore from automatic backup (created during import)
466+
kv db restore
467+
# Output: Database restored from: /path/to/kv.db.backup
468+
# Note: This restores the backup created during the last import operation
469+
465470
# Practical examples:
466471

467472
# Create daily backups
@@ -487,9 +492,12 @@ gunzip -c kv-backup.db.gz | kv db import -
487492
488493
**Safety features:**
489494
- Import automatically creates a backup at `<db-path>.backup` before replacing
495+
- You can restore from this backup anytime using `kv db restore`
490496
- Import validates the file is a valid database before proceeding
491497
- Export fails if file exists (use `--force` to override)
492498
- Export validates destination directory exists
499+
- Restore validates the backup is a valid database before restoring
500+
- Restore preserves the backup file after restoration
493501
494502
### Utility Commands
495503

src/cmd/export.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ var exportCmd = &cobra.Command{
1919
Long: `Export the entire database to a binary file.
2020
2121
The exported file captures the complete state of the database and can be
22-
imported later using the 'import' command to restore the exact state.
22+
imported later using the 'kv db import' command to restore the exact state.
23+
When importing, a backup of the current database is automatically created,
24+
which can be restored using 'kv db restore' if needed.
2325
2426
Use "-" as the file path to write to stdout (useful for piping).`,
2527
Example: ` # Export database to a file

src/cmd/import.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ The imported file must be a valid database file created with the 'export' comman
2020
This will completely replace the current database with the imported one.
2121
2222
WARNING: This operation is destructive. The current database will be backed up
23-
to <db-path>.backup before importing.
23+
to <db-path>.backup before importing. You can restore from this backup using
24+
the 'kv db restore' command if needed.
2425
2526
Use "-" as the file path to read from stdin (useful for piping).`,
2627
Example: ` # Import database from a file
@@ -87,7 +88,8 @@ Use "-" as the file path to read from stdin (useful for piping).`,
8788
if err != nil {
8889
common.Fail("Failed to backup current database: %v", err)
8990
}
90-
fmt.Printf("Current database backed up to: %s\n", backupPath)
91+
92+
fmt.Print("Current database backed up")
9193
}
9294

9395
// Remove WAL files (should be empty after VACUUM, but remove just in case)

src/cmd/restore.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/AmrSaber/kv/src/common"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// restoreCmd represents the restore command
12+
var restoreCmd = &cobra.Command{
13+
Use: "restore",
14+
Short: "Restore database from automatic backup",
15+
Long: `Restore the database from the automatic backup file created during import.
16+
17+
When you run 'kv db import', a backup of the current database is automatically
18+
created at <db-path>.backup. This command restores that backup.
19+
20+
The backup file is preserved after restoration, allowing you to restore again
21+
if needed.`,
22+
Args: cobra.NoArgs,
23+
Run: func(cmd *cobra.Command, args []string) {
24+
// Get database path
25+
dbPath := common.GetDBPath()
26+
backupPath := dbPath + ".backup"
27+
28+
// Check if backup exists
29+
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
30+
common.Fail("No backup file found")
31+
}
32+
33+
// Validate backup is a valid SQLite database
34+
if err := common.ValidateSqliteFile(backupPath); err != nil {
35+
common.Fail("Invalid backup file: %v", err)
36+
}
37+
38+
// Close database connection
39+
common.CloseDB()
40+
41+
// Remove current database and remove WAL files
42+
_ = os.Remove(dbPath)
43+
_ = os.Remove(dbPath + "-wal")
44+
_ = os.Remove(dbPath + "-shm")
45+
46+
// Copy backup to current location
47+
err := common.CopyFile(backupPath, dbPath)
48+
if err != nil {
49+
common.Fail("Failed to restore from backup: %v", err)
50+
}
51+
52+
// Reopen database (migrations will run automatically)
53+
common.GetDB()
54+
55+
fmt.Printf("Database restored from backup")
56+
},
57+
}
58+
59+
func init() {
60+
dbCmd.AddCommand(restoreCmd)
61+
}

tests/export_import_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,123 @@ func TestExportImportRoundTrip(t *testing.T) {
274274
t.Error("History should contain all versions")
275275
}
276276
}
277+
278+
func TestRestoreCommand(t *testing.T) {
279+
cleanup := SetupTestDB(t)
280+
defer cleanup()
281+
282+
t.Run("restore fails when no backup exists", func(t *testing.T) {
283+
output := RunKVFailure(t, "db", "restore")
284+
if !strings.Contains(output, "No backup file found") {
285+
t.Errorf("Expected 'No backup file found' error, got: %s", output)
286+
}
287+
})
288+
289+
// Setup test data and create a backup via import
290+
RunKVSuccess(t, "set", "original1", "value1")
291+
RunKVSuccess(t, "set", "original2", "value2", "--password", "pass")
292+
RunKVSuccess(t, "set", "original3", "value3")
293+
RunKVSuccess(t, "hide", "original3")
294+
295+
tmpDir := t.TempDir()
296+
exportPath := filepath.Join(tmpDir, "backup.db")
297+
RunKVSuccess(t, "db", "export", exportPath)
298+
299+
// Import to create a backup
300+
RunKVSuccess(t, "db", "import", exportPath)
301+
302+
t.Run("restore fails with invalid backup file", func(t *testing.T) {
303+
// Get DB path and backup path
304+
dbPath := common.GetDBPath()
305+
backupPath := dbPath + ".backup"
306+
307+
// Save current backup
308+
validBackupPath := backupPath + ".valid"
309+
err := common.CopyFile(backupPath, validBackupPath)
310+
if err != nil {
311+
t.Fatal(err)
312+
}
313+
defer func() {
314+
// Restore valid backup
315+
_ = os.Remove(backupPath)
316+
_ = common.CopyFile(validBackupPath, backupPath)
317+
_ = os.Remove(validBackupPath)
318+
}()
319+
320+
// Replace backup with invalid content
321+
err = os.WriteFile(backupPath, []byte("not a database"), 0o644)
322+
if err != nil {
323+
t.Fatal(err)
324+
}
325+
326+
output := RunKVFailure(t, "db", "restore")
327+
if !strings.Contains(output, "Invalid backup") {
328+
t.Errorf("Expected 'Invalid backup' error, got: %s", output)
329+
}
330+
})
331+
332+
t.Run("restore successfully restores from backup", func(t *testing.T) {
333+
// Modify database
334+
RunKVSuccess(t, "set", "new-key", "new-value")
335+
RunKVSuccess(t, "delete", "original1")
336+
337+
// Verify changes
338+
output := RunKVSuccess(t, "get", "new-key")
339+
if output != "new-value" {
340+
t.Error("new-key should exist before restore")
341+
}
342+
343+
RunKVFailure(t, "get", "original1")
344+
345+
// Restore
346+
output = RunKVSuccess(t, "db", "restore")
347+
if !strings.Contains(output, "restored from") {
348+
t.Errorf("Expected success message, got: %s", output)
349+
}
350+
351+
// Verify restoration
352+
output = RunKVSuccess(t, "get", "original1")
353+
if output != "value1" {
354+
t.Errorf("Expected 'value1', got: %s", output)
355+
}
356+
357+
output = RunKVFailure(t, "get", "new-key")
358+
if !strings.Contains(output, "does not exist") {
359+
t.Error("new-key should not exist after restore")
360+
}
361+
})
362+
363+
t.Run("restore preserves backup file", func(t *testing.T) {
364+
dbPath := common.GetDBPath()
365+
backupPath := dbPath + ".backup"
366+
367+
// Verify backup still exists
368+
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
369+
t.Error("Backup file should still exist after restore")
370+
}
371+
372+
// Verify it's still valid
373+
if err := common.ValidateSqliteFile(backupPath); err != nil {
374+
t.Errorf("Backup file should still be valid: %v", err)
375+
}
376+
})
377+
378+
t.Run("restore preserves all data attributes", func(t *testing.T) {
379+
// Verify locked key
380+
output := RunKVSuccess(t, "list", "original2")
381+
if !strings.Contains(output, "[Locked]") {
382+
t.Error("original2 should be locked")
383+
}
384+
385+
output = RunKVSuccess(t, "get", "original2", "--password", "pass")
386+
if output != "value2" {
387+
t.Errorf("Expected 'value2', got: %s", output)
388+
}
389+
390+
// Verify hidden key
391+
output = RunKVSuccess(t, "list", "original3")
392+
if !strings.Contains(output, "[Hidden]") {
393+
t.Error("original3 should be hidden")
394+
}
395+
})
396+
}

0 commit comments

Comments
 (0)