Skip to main content

specfold_core/files/store/
command.rs

1//! A store reached through a command the user already trusts (rclone, aws, gsutil, wrangler …).
2//! specfold never handles credentials: the command reads them from its own configuration.
3
4use super::types::Store;
5use anyhow::{Result, anyhow, bail};
6use std::path::Path;
7use std::process::Command;
8
9pub struct CommandStore {
10    pub put: Option<Vec<String>>,
11    pub get: Option<Vec<String>>,
12    pub exists: Option<Vec<String>>,
13}
14
15fn run(template: &[String], key: &str, src: &str, dst: &str) -> Result<std::process::Output> {
16    let args: Vec<String> =
17        template.iter().map(|a| a.replace("{key}", key).replace("{src}", src).replace("{dst}", dst)).collect();
18    let (prog, rest) = args.split_first().ok_or_else(|| anyhow!("empty store command"))?;
19    Command::new(prog).args(rest).output().map_err(|e| anyhow!("cannot run {prog}: {e}"))
20}
21
22impl Store for CommandStore {
23    fn put(&self, src: &Path, key: &str) -> Result<()> {
24        let t = self.put.as_ref().ok_or_else(|| anyhow!("files.store.put is not configured"))?;
25        let out = run(t, key, &src.to_string_lossy(), "")?;
26        if !out.status.success() {
27            bail!("store put failed: {}", String::from_utf8_lossy(&out.stderr).trim());
28        }
29        Ok(())
30    }
31
32    fn get(&self, key: &str, dst: &Path) -> Result<()> {
33        let t = self.get.as_ref().ok_or_else(|| anyhow!("files.store.get is not configured"))?;
34        if let Some(p) = dst.parent() {
35            std::fs::create_dir_all(p)?;
36        }
37        let out = run(t, key, "", &dst.to_string_lossy())?;
38        if !out.status.success() {
39            bail!("store get failed: {}", String::from_utf8_lossy(&out.stderr).trim());
40        }
41        Ok(())
42    }
43
44    fn exists(&self, key: &str) -> Result<Option<bool>> {
45        let Some(t) = &self.exists else { return Ok(None) };
46        Ok(Some(run(t, key, "", "")?.status.success()))
47    }
48}