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"
|
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
|
## 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)
|
fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
|
||||||
return
|
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)
|
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
|
||||||
if abs_err != nil {
|
if abs_err != nil {
|
||||||
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
|
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)
|
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
|
// Caller is responsible for the returned memory
|
||||||
new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
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)
|
abs_path, abs_err := filepath.abs(path)
|
||||||
if abs_err != nil {
|
if abs_err != nil {
|
||||||
fmt.eprintf("Error getting absolute path: %v\n", abs_err)
|
fmt.eprintf("Error getting absolute path: %v\n", abs_err)
|
||||||
@@ -428,12 +436,46 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
|||||||
true
|
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.
|
// Reconciles `f` with the filesystem and persists changes to the database.
|
||||||
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
||||||
allocator := db_allocator(db)
|
allocator := db_allocator(db)
|
||||||
result: SyncFlag = {}
|
result: SyncFlag = {}
|
||||||
old_path := f.path
|
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) {
|
if !os.exists(f.dir) {
|
||||||
moved, err := try_move_dir(db, f, allocator)
|
moved, err := try_move_dir(db, f, allocator)
|
||||||
if !moved {
|
if !moved {
|
||||||
|
|||||||
@@ -72,10 +72,12 @@
|
|||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.unstable.odin
|
pkgs.unstable.odin
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
|
pkgs.makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
pkgs.git
|
pkgs.git
|
||||||
|
pkgs.openssh
|
||||||
|
|
||||||
pkgs.libsodium
|
pkgs.libsodium
|
||||||
mysqlite
|
mysqlite
|
||||||
@@ -108,6 +110,10 @@
|
|||||||
cp docs/man/*.1 $out/share/man/man1/
|
cp docs/man/*.1 $out/share/man/man1/
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
postFixup = ''
|
||||||
|
wrapProgram $out/bin/envr --prefix PATH : ${nixpkgs.lib.makeBinPath [ pkgs.openssh ]}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
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