1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! `stowe remote`: list, add or update the named remotes.
use anyhow::{Result, bail};
use clap::Subcommand;
use crate::mirror;
use crate::prompt::confirm_default_yes;
use crate::remote::remote_format;
use crate::remote::remote_reachable;
use crate::repo::Repo;
#[derive(Subcommand)]
pub enum RemoteCmd {
/// Add or update a named remote: `stowe remote add origin local:/path`.
Add {
name: String,
url: String,
/// On-disk format: `mirror` (playable, local only) or `backup` (blobs).
/// Omit to use the scheme default (local → mirror, s3 → backup).
#[arg(long, value_parser = ["mirror", "backup"])]
format: Option<String>,
/// Shell command that makes this remote available (mount the drive,
/// bring up an sshfs). Run automatically when it isn't reachable. May be
/// an inline command or a path to a script. Local remotes only.
#[arg(long)]
mount: Option<String>,
},
/// List configured remotes.
List,
}
pub fn run(cmd: Option<RemoteCmd>) -> Result<()> {
let repo = Repo::find()?;
match cmd {
Some(RemoteCmd::Add {
name,
url,
format,
mount,
}) => {
let mut cfg = repo.config()?;
match &format {
Some(fmt) => {
if fmt == "mirror" && mirror::local_root(&url).is_none() {
bail!("`mirror` format needs a local path - {url} can't be a mirror");
}
cfg.formats.insert(name.clone(), fmt.clone());
}
// No override → fall back to the scheme default.
None => {
cfg.formats.remove(&name);
}
}
// A mount command only means something for a path we have to make
// exist. An s3 store is reachable on credentials, not on mounting.
match &mount {
Some(cmd) => {
if mirror::local_root(&url).is_none() {
bail!("--mount only applies to local remotes; {url} has nothing to mount");
}
cfg.mounts.insert(name.clone(), cmd.clone());
}
None => {
cfg.mounts.remove(&name);
}
}
// If it's a local drive that isn't plugged in, confirm before saving
// (you may be adding it ahead of connecting, so default is yes).
// With a mount command we already know how to reach it, so no prompt.
if !remote_reachable(&url) && mount.is_none() {
let shown = crate::paths::short_url(&url);
if !confirm_default_yes(&format!(
"remote `{name}` ({shown}) isn't reachable right now. Add it anyway?"
)) {
println!("aborted - remote not added.");
return Ok(());
}
}
cfg.remotes.insert(name.clone(), url.clone());
repo.save_config(&cfg)?;
println!(
"remote `{name}` -> {} ({} format)",
crate::paths::short_url(&url),
remote_format(&cfg, &name, &url).name()
);
if let Some(cmd) = &mount {
println!(" mount: {cmd}");
}
}
// Bare `stowe remote` and `stowe remote list` both just list.
None | Some(RemoteCmd::List) => {
let cfg = repo.config()?;
if cfg.remotes.is_empty() {
println!("no remotes. add one, e.g.:");
println!(" stowe remote add origin local:/path/to/backup");
}
for (name, url) in &cfg.remotes {
println!(
"{name}\t{}\t[{}]",
crate::paths::short_url(url),
remote_format(&cfg, name, url).name()
);
if let Some(cmd) = cfg.mounts.get(name) {
println!(" mount: {cmd}");
}
}
}
}
Ok(())
}