From c92155a17b886f16adeb93c2c8a99d4f7b2afbf8 Mon Sep 17 00:00:00 2001 From: Spencer Brower Date: Thu, 11 Jun 2026 21:04:39 -0400 Subject: [PATCH] feat(odin): ported backup command. --- cli.odin | 2 + cmd_backup.odin | 34 +++++++++++++ db.odin | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ main.odin | 2 + stubs.odin | 4 -- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 cmd_backup.odin diff --git a/cli.odin b/cli.odin index 3736b1b..9a82de3 100644 --- a/cli.odin +++ b/cli.odin @@ -15,6 +15,8 @@ IMPLEMENTED_COMMANDS := []string{ "version", "deps", "list", + "backup", + "add", } parse_args :: proc() -> (cmd: Command, ok: bool) { diff --git a/cmd_backup.odin b/cmd_backup.odin new file mode 100644 index 0000000..5604a89 --- /dev/null +++ b/cmd_backup.odin @@ -0,0 +1,34 @@ +package main + +import "core:fmt" +import "core:strings" + +cmd_backup :: proc(cmd: ^Command) { + if len(cmd.args) != 1 { + fmt.println("Usage: envr backup ") + return + } + + path := cmd.args[0] + if len(strings.trim_space(path)) == 0 { + fmt.println("Error: No path provided") + return + } + + file, ok := new_env_file(path) + if !ok { + return + } + + db, db_ok := db_open() + if !db_ok { + return + } + defer db_close(&db) + + if !db_insert(&db, file) { + return + } + + fmt.printf("Saved %s into the database\n", path) +} diff --git a/db.odin b/db.odin index 5981b18..72864d0 100644 --- a/db.odin +++ b/db.odin @@ -1,6 +1,8 @@ package main import "core:c" +import "core:crypto/hash" +import "core:encoding/hex" import "core:encoding/json" import "core:fmt" import "core:os" @@ -265,6 +267,131 @@ db_attach_and_copy :: proc(mem_db: ^rawptr, src_path: string) -> bool { return true } +get_git_remotes :: proc(dir: string) -> [dynamic]string { + remotes: [dynamic]string + remote_set: map[string]bool + + b: strings.Builder + strings.builder_init(&b) + fmt.sbprintf(&b, "%s-git-remotes", make_temp_path()) + tmp_path := strings.to_string(b) + tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC) + if tmp_err != nil { + return remotes + } + + args := []string{"git", "remote", "-v"} + desc := os.Process_Desc{ + command = args, + stdout = tmp_file, + stderr = nil, + working_dir = dir, + } + + p, start_err := os.process_start(desc) + os.close(tmp_file) + if start_err != nil { + os.remove(tmp_path) + return remotes + } + + state, wait_err := os.process_wait(p) + if wait_err != nil || state.exit_code != 0 { + os.remove(tmp_path) + return remotes + } + + data, read_err := os.read_entire_file_from_path(tmp_path, context.allocator) + os.remove(tmp_path) + if read_err != nil { + return remotes + } + + output_str := string(data) + lines := strings.split(output_str, "\n") + + for &line in lines { + line = strings.trim_space(line) + if len(line) == 0 { + continue + } + parts := strings.fields(line) + if len(parts) >= 2 { + remote_set[parts[1]] = true + } + } + + for remote, _ in remote_set { + cloned, _ := strings.clone(remote) + append(&remotes, cloned) + } + + return remotes +} + +new_env_file :: proc(path: string) -> (EnvFile, bool) { + abs_path, abs_err := filepath.abs(path) + if abs_err != nil { + fmt.printf("Error getting absolute path: %v\n", abs_err) + return EnvFile{}, false + } + cloned_path, _ := strings.clone(abs_path) + + dir := filepath.dir(cloned_path) + cloned_dir, _ := strings.clone(dir) + + remotes := get_git_remotes(cloned_dir) + + data, read_err := os.read_entire_file_from_path(cloned_path, context.allocator) + if read_err != nil { + fmt.printf("Error reading file %s: %v\n", cloned_path, read_err) + return EnvFile{}, false + } + + digest := hash.hash_bytes(hash.Algorithm.SHA256, data) + hex_bytes, _ := hex.encode(digest) + sha_str := string(hex_bytes) + + return EnvFile{ + Path = cloned_path, + Dir = cloned_dir, + Remotes = remotes, + Sha256 = sha_str, + contents = string(data), + }, true +} + +db_insert :: proc(d: ^Db, file: EnvFile) -> bool { + remotes_json, marshal_err := json.marshal(file.Remotes) + if marshal_err != nil { + fmt.printf("Error marshaling remotes: %v\n", marshal_err) + return false + } + + sql := "INSERT OR REPLACE INTO envr_env_files (path, remotes, sha256, contents) VALUES (?, ?, ?, ?)" + stmt: ^rawptr + rc := sqlite.prepare_v2(d.db, string_to_cstring(sql), -1, &stmt, nil) + if rc != sqlite.OK { + fmt.printf("Error preparing insert: %s\n", sqlite.db_errmsg(d.db)) + return false + } + defer sqlite.finalize(stmt) + + rc = sqlite.bind_text(stmt, 1, string_to_cstring(file.Path), -1, nil) + rc = sqlite.bind_text(stmt, 2, string_to_cstring(string(remotes_json)), -1, nil) + rc = sqlite.bind_text(stmt, 3, string_to_cstring(file.Sha256), -1, nil) + rc = sqlite.bind_text(stmt, 4, string_to_cstring(file.contents), -1, nil) + + rc = sqlite.step(stmt) + if rc != sqlite.DONE { + fmt.printf("Error inserting: %s\n", sqlite.db_errmsg(d.db)) + return false + } + + d.changed = true + return true +} + cstring_to_string :: proc(cs: cstring) -> string { if cs == nil { return "" diff --git a/main.odin b/main.odin index abab14f..dd2fb7c 100644 --- a/main.odin +++ b/main.odin @@ -23,6 +23,8 @@ main :: proc() { cmd_deps(&cmd) case "list": cmd_list(&cmd) + case "backup", "add": + cmd_backup(&cmd) case: fmt.printf("Unknown command: %s\n", cmd.name) print_usage() diff --git a/stubs.odin b/stubs.odin index 403b993..cb22840 100644 --- a/stubs.odin +++ b/stubs.odin @@ -14,10 +14,6 @@ cmd_sync :: proc(cmd: ^Command) { fmt.println("TODO: sync") } -cmd_backup :: proc(cmd: ^Command) { - fmt.println("TODO: backup") -} - cmd_restore :: proc(cmd: ^Command) { fmt.println("TODO: restore") }