mirror of
https://github.com/sbrow/envr.git
synced 2026-10-25 08:41:55 -04:00
feat: Added support for remote files (via ssh).
This commit is contained in:
@@ -14,6 +14,11 @@
|
||||
|
||||
8. Add docs for "building from source"
|
||||
|
||||
9. Add `envr show` command that uses pager (`less`) unless -no-pager is passed.
|
||||
|
||||
10. Run remote SSH sync operations in parallel.
|
||||
|
||||
11. Envr sync should require confirmation before pushing remote files.
|
||||
|
||||
## Double-check AI output
|
||||
|
||||
|
||||
+4
-1
@@ -16,6 +16,10 @@ cmd_restore :: proc(cmd: ^Command) {
|
||||
fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
|
||||
return
|
||||
}
|
||||
if _, _, is_remote := parse_remote_path(path); is_remote {
|
||||
fmt.wprintln(cmd.err, "Remote restore is not supported yet", flush = false)
|
||||
return
|
||||
}
|
||||
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
|
||||
if abs_err != nil {
|
||||
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
|
||||
@@ -50,4 +54,3 @@ cmd_restore :: proc(cmd: ^Command) {
|
||||
|
||||
fmt.wprintf(cmd.out, "Restored %s\n", file.path, flush = false)
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +399,14 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
|
||||
|
||||
// Caller is responsible for the returned memory
|
||||
new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
||||
if host, remote_path, is_remote := parse_remote_path(path); is_remote {
|
||||
contents, read_ok := read_remote_file(host, remote_path)
|
||||
if !read_ok {
|
||||
return EnvFile{}, false
|
||||
}
|
||||
return new_remote_env_file(path, contents), true
|
||||
}
|
||||
|
||||
abs_path, abs_err := filepath.abs(path)
|
||||
if abs_err != nil {
|
||||
fmt.eprintf("Error getting absolute path: %v\n", abs_err)
|
||||
@@ -428,12 +436,46 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
||||
true
|
||||
}
|
||||
|
||||
// Takes ownership of contents and returns an EnvFile that owns all its fields.
|
||||
new_remote_env_file :: proc(path: string, contents: []byte) -> EnvFile {
|
||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, contents, context.temp_allocator)
|
||||
hex_bytes := hex.encode(digest, context.allocator)
|
||||
|
||||
return EnvFile {
|
||||
path = strings.clone(path, context.allocator),
|
||||
dir = "",
|
||||
sha256 = string(hex_bytes),
|
||||
contents = string(contents),
|
||||
}
|
||||
}
|
||||
|
||||
// Reconciles `f` with the filesystem and persists changes to the database.
|
||||
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
||||
allocator := db_allocator(db)
|
||||
result: SyncFlag = {}
|
||||
old_path := f.path
|
||||
|
||||
if host, remote_path, is_remote := parse_remote_path(f.path); is_remote {
|
||||
contents, read_ok := read_remote_file(host, remote_path)
|
||||
if !read_ok {
|
||||
return {}, .ReadFailed
|
||||
}
|
||||
|
||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, contents, context.temp_allocator)
|
||||
hex_bytes := hex.encode(digest, allocator)
|
||||
current_sha := string(hex_bytes)
|
||||
if current_sha == f.sha256 {
|
||||
return {}, .None
|
||||
}
|
||||
|
||||
f.contents = string(contents)
|
||||
f.sha256 = current_sha
|
||||
if !db_persist(db, f, old_path) {
|
||||
return {}, .DbFailed
|
||||
}
|
||||
return {.BackedUp}, .None
|
||||
}
|
||||
|
||||
if !os.exists(f.dir) {
|
||||
moved, err := try_move_dir(db, f, allocator)
|
||||
if !moved {
|
||||
|
||||
@@ -72,10 +72,12 @@
|
||||
nativeBuildInputs = [
|
||||
pkgs.unstable.odin
|
||||
pkgs.pkg-config
|
||||
pkgs.makeWrapper
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
pkgs.git
|
||||
pkgs.openssh
|
||||
|
||||
pkgs.libsodium
|
||||
mysqlite
|
||||
@@ -108,6 +110,10 @@
|
||||
cp docs/man/*.1 $out/share/man/man1/
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
wrapProgram $out/bin/envr --prefix PATH : ${nixpkgs.lib.makeBinPath [ pkgs.openssh ]}
|
||||
'';
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#+test
|
||||
package main
|
||||
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_parse_remote_path :: proc(t: ^testing.T) {
|
||||
host, path, ok := parse_remote_path("spencer@example.com:/srv/app/.env")
|
||||
testing.expect(t, ok, "expected remote identity to be recognized")
|
||||
testing.expect_value(t, host, "spencer@example.com")
|
||||
testing.expect_value(t, path, "/srv/app/.env")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_remote_path_rejects_local_paths :: proc(t: ^testing.T) {
|
||||
_, _, ok := parse_remote_path("/srv/app/.env")
|
||||
testing.expect(t, !ok, "local path should not be treated as remote")
|
||||
_, _, ok = parse_remote_path("user@example.com:relative.env")
|
||||
testing.expect(t, !ok, "remote path must be absolute")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_quote_remote_shell_path :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, quote_remote_shell_path("/srv/app/.env"), "/srv/app/.env")
|
||||
testing.expect_value(t, quote_remote_shell_path("/srv/app/a'b.env"), "/srv/app/a'\"'\"'b.env")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_new_remote_env_file_owns_its_fields :: proc(t: ^testing.T) {
|
||||
input_path := strings.clone("spencer@example.com:/etc/umami.env", context.allocator)
|
||||
contents := make([]u8, len("SECRET=value"), context.allocator)
|
||||
copy(contents, "SECRET=value")
|
||||
|
||||
file := new_remote_env_file(input_path, contents)
|
||||
delete(input_path)
|
||||
defer delete_envfile(&file)
|
||||
|
||||
testing.expect_value(t, file.path, "spencer@example.com:/etc/umami.env")
|
||||
testing.expect_value(t, file.contents, "SECRET=value")
|
||||
}
|
||||
Reference in New Issue
Block a user