use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::io; use std::path::{Path, PathBuf}; use super::local::LocalDriver; use super::ssh::SshDriver; /// Internal trait defining the strategy for executing a command. /// /// This allows to have different execution behaviors (Local, SSH, ...) while keeping the /// `ContextCommand` API consistent. pub trait CommandRunner { fn add_arg(&mut self, arg: String); fn status(&mut self) -> io::Result; fn output(&mut self) -> io::Result; } pub trait ContextDriver { fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result; fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>; fn list_files(&self, path: &Path) -> io::Result>; fn create_runner(&self, program: String) -> Box; fn prepare_work_dir(&self) -> io::Result; } /// Represents an execution environment (Local or via SSH). /// /// This is the data model that configuration files store. It defines *where* operations happen. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type")] #[derive(Default)] pub enum Context { #[serde(rename = "local")] #[default] Local, #[serde(rename = "ssh")] Ssh { host: String, user: Option, port: Option, }, } impl Context { pub fn command>(&self, program: S) -> ContextCommand { let runner = self .driver() .create_runner(program.as_ref().to_string_lossy().to_string()); ContextCommand { runner } } /// Ensures that the source file/directory exists at the destination context. /// /// If Local: Returns the absolute path of `src`. /// If Remote: Copies `src` to `dest_root` on the remote and returns the path to the copied entity. pub fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result { self.driver().ensure_available(src, dest_root) } pub fn prepare_work_dir(&self) -> io::Result { self.driver().prepare_work_dir() } /// Retrieve a file or directory from the context to the local filesystem. /// /// The `src` path is on the context, `dest` is on the local machine. /// If `src` is a directory, it is copied recursively. pub fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { self.driver().retrieve_path(src, dest) } /// List files in a directory on the context. pub fn list_files(&self, path: &Path) -> io::Result> { self.driver().list_files(path) } fn driver(&self) -> Box { match self { Context::Local => Box::new(LocalDriver), Context::Ssh { host, user, port } => Box::new(SshDriver { host: host.clone(), user: user.clone(), port: *port, }), } } } /// A builder and executor for commands within a specific `Context`. /// /// This struct acts as a high-level facade (API) for command creation. It hides the implementation /// details of *how* the command is actually executed, allowing the user to simple chain arguments /// and call `status()` or `output()`. /// /// It delegates the actual work to a `CommandRunner`. pub struct ContextCommand { runner: Box, } impl ContextCommand { pub fn arg>(&mut self, arg: S) -> &mut Self { self.runner .add_arg(arg.as_ref().to_string_lossy().to_string()); self } // Support chaining args pub fn args(&mut self, args: I) -> &mut Self where I: IntoIterator, S: AsRef, { for arg in args { self.arg(arg); } self } pub fn status(&mut self) -> io::Result { self.runner.status() } // Capture output pub fn output(&mut self) -> io::Result { self.runner.output() } }