From 5126e10e9dab7f01ee2b86f8b18ab2c8d300fa53 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 23 Aug 2026 08:33:39 +0500 Subject: [PATCH] feat: add base dir to server --- README.md | 4 +++- src/config.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5565db..cb5d5f8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ host = "195.0.2.10" # алиас из ~/.ssh/config или IP port = 2222 user = "deploy" key_path = "~/.ssh/id_ed25519" +base_dir = "/var/www/pages" # корень для относительных sync target [projects.my-node-app] server = "my-vps" # ссылка на профиль @@ -65,7 +66,8 @@ commands = ["npm ci", "npm run build"] [[projects.my-node-app.sync]] # что заливать на сервер source = "dist" # файл или папка (относительно workdir) -target = "/var/www/pages" +target = "my-node-app" # относительный -> base_dir сервера; + # абсолютный "/path" используется как есть [projects.my-node-app.remote] # команды управления на сервере commands = ["pm2 restart pages"] diff --git a/src/config.rs b/src/config.rs index dc41244..29f945a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,6 +20,7 @@ pub struct Config { #[serde(deny_unknown_fields)] pub struct Server { pub host: String, + pub base_dir: Option, pub user: Option, pub port: Option, pub key_path: Option, @@ -115,6 +116,30 @@ impl Config { for rule in &mut project.sync { rule.source = expand_tilde(&rule.source) .with_context(|| format!("project '{name}': invalid sync source"))?; + rule.target = expand_tilde(&rule.target) + .with_context(|| format!("project '{name}': invalid sync target"))?; + } + let base_dir = project + .server + .as_ref() + .and_then(|s| servers.get(s)) + .and_then(|s| s.base_dir.as_deref()); + for rule in &mut project.sync { + if rule.target.to_string_lossy().starts_with('/') { + continue; + } + let Some(base) = base_dir else { + bail!( + "project '{name}': sync target '{}' is relative, but the referenced server has no 'base_dir'", + rule.target.display() + ); + }; + let rel = rule.target.to_string_lossy().replace('\\', "/"); + rule.target = PathBuf::from(format!( + "{}/{}", + base.trim_end_matches('/'), + rel.trim_start_matches('/') + )); } for key in project.env.keys() { let valid = !key.is_empty() @@ -292,6 +317,46 @@ workdir = "." assert!(err.to_string().contains("no host"), "{err:#}"); } + #[test] + fn relative_sync_target_resolves_against_server_base_dir() { + let s = r#" +[servers.main] +host = "10.0.0.9" +base_dir = "/var/www/pages/" + +[projects.web] +workdir = "." +server = "main" + +[[projects.web.sync]] +source = "dist" +target = "web-app" + +[[projects.web.sync]] +source = "dist/favicon.ico" +target = "/opt/static/favicon.ico" +"#; + let cfg = parse(s).unwrap(); + let sync = &cfg.projects.get("web").unwrap().sync; + assert_eq!(sync[0].target.to_string_lossy(), "/var/www/pages/web-app"); + assert_eq!(sync[1].target.to_string_lossy(), "/opt/static/favicon.ico"); + } + + #[test] + fn relative_sync_target_without_base_dir_is_error() { + let s = r#" +[projects.web] +workdir = "." +host = "10.0.0.9" + +[[projects.web.sync]] +source = "dist" +target = "web-app" +"#; + let err = parse(s).unwrap_err(); + assert!(err.to_string().contains("base_dir"), "{err:#}"); + } + #[test] fn empty_commands_section_is_rejected() { let s = r#"