Skip to content

Commit cf08c7d

Browse files
committed
added move-projects-dir
1 parent 3a6c7c6 commit cf08c7d

File tree

7 files changed

+260
-41
lines changed

7 files changed

+260
-41
lines changed

tmc-langs-cli/src/app.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -665,19 +665,41 @@ fn create_settings_app() -> App<'static, 'static> {
665665
.takes_value(true),
666666
),
667667
)
668-
.subcommand(
669-
SubCommand::with_name("set")
670-
.about("Saves a value in the settings")
671-
.arg(Arg::with_name("key").help("The key."))
672-
.arg(Arg::with_name("json").help("The value in JSON.")),
673-
)
674668
.subcommand(
675669
SubCommand::with_name("list")
676670
.about("Prints every key=value pair in the settings file."),
677671
)
672+
.subcommand(
673+
SubCommand::with_name("move-projects-dir")
674+
.about(
675+
"Change the projects-dir setting, moving the contents into the new directory",
676+
)
677+
.arg(
678+
Arg::with_name("dir")
679+
.help("The directory where the projects should be moved.")
680+
.required(true)
681+
.takes_value(true),
682+
),
683+
)
678684
.subcommand(
679685
SubCommand::with_name("reset").about("Resets the settings file to the defaults"),
680686
)
687+
.subcommand(
688+
SubCommand::with_name("set")
689+
.about("Saves a value in the settings")
690+
.arg(
691+
Arg::with_name("key")
692+
.help("The key. Parsed as JSON, assumed to be a string if parsing fails.")
693+
.required(true)
694+
.takes_value(true),
695+
)
696+
.arg(
697+
Arg::with_name("json")
698+
.help("The value in JSON.")
699+
.required(true)
700+
.takes_value(true),
701+
),
702+
)
681703
.subcommand(
682704
SubCommand::with_name("unset")
683705
.about("Unsets a value from the settings")

tmc-langs-cli/src/config.rs

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,74 @@
11
//! Handles the CLI's configuration files and credentials.
22
33
use anyhow::{Context, Error};
4+
use serde::{Deserialize, Serialize};
45
use std::env;
56
use std::fs::{self, File};
67
use std::io::Write;
78
use std::path::{Path, PathBuf};
89
use toml::{value::Table, Value};
910

11+
#[derive(Debug, Serialize, Deserialize)]
12+
#[serde(rename_all = "kebab-case")]
13+
pub struct Config {
14+
pub projects_dir: PathBuf,
15+
#[serde(flatten)]
16+
pub table: Table,
17+
}
18+
19+
impl Config {
20+
pub fn get(&self, key: &str) -> ConfigValue {
21+
match key {
22+
"projects-dir" => ConfigValue::Path(&self.projects_dir),
23+
_ => ConfigValue::Value(self.table.get(key)),
24+
}
25+
}
26+
27+
pub fn insert(&mut self, key: String, value: Value) -> Result<(), anyhow::Error> {
28+
match key.as_str() {
29+
"projects-dir" => {
30+
if let Value::String(value) = value {
31+
let path = PathBuf::from(value);
32+
self.set_projects_dir(path)?;
33+
} else {
34+
anyhow::bail!("The value for projects-dir must be a string.")
35+
}
36+
}
37+
_ => {
38+
self.table.insert(key, value);
39+
}
40+
}
41+
Ok(())
42+
}
43+
44+
pub fn remove(&mut self, key: &str) -> Result<Option<Value>, anyhow::Error> {
45+
match key {
46+
"projects-dir" => anyhow::bail!("projects-dir must always be defined"),
47+
_ => Ok(self.table.remove(key)),
48+
}
49+
}
50+
51+
pub fn set_projects_dir(&mut self, mut target: PathBuf) -> Result<PathBuf, anyhow::Error> {
52+
// check if the directory is empty or not
53+
if fs::read_dir(&target)
54+
.with_context(|| format!("Failed to read directory at {}", target.display()))?
55+
.next()
56+
.is_some()
57+
{
58+
anyhow::bail!("Cannot set projects-dir to a non-empty directory.");
59+
}
60+
std::mem::swap(&mut self.projects_dir, &mut target);
61+
Ok(target)
62+
}
63+
}
64+
65+
#[derive(Debug, Serialize)]
66+
#[serde(untagged)]
67+
pub enum ConfigValue<'a> {
68+
Value(Option<&'a Value>),
69+
Path(&'a Path),
70+
}
71+
1072
// base directory for a given plugin's settings files
1173
fn get_tmc_dir(client_name: &str) -> Result<PathBuf, Error> {
1274
let config_dir = match env::var("TMC_LANGS_CONFIG_DIR") {
@@ -35,7 +97,7 @@ fn get_client_stub(client: &str) -> &str {
3597
}
3698

3799
// initializes the default configuration file at the given path
38-
fn init_config_at(client_name: &str, path: &Path) -> Result<Table, Error> {
100+
fn init_config_at(client_name: &str, path: &Path) -> Result<Config, Error> {
39101
let mut file = File::create(&path)
40102
.with_context(|| format!("Failed to create new config file at {}", path.display()))?;
41103

@@ -50,21 +112,20 @@ fn init_config_at(client_name: &str, path: &Path) -> Result<Table, Error> {
50112
)
51113
})?;
52114

53-
let mut config = Table::new();
54-
config.insert(
55-
"projects-folder".to_string(),
56-
Value::String(default_project_dir.to_string_lossy().into_owned()),
57-
);
115+
let config = Config {
116+
projects_dir: default_project_dir,
117+
table: Table::new(),
118+
};
58119

59120
let toml = toml::to_string_pretty(&config).context("Failed to serialize config")?;
60121
file.write_all(toml.as_bytes())
61122
.with_context(|| format!("Failed to write default config to {}", path.display()))?;
62123
Ok(config)
63124
}
64125

65-
pub fn load_config(client_name: &str) -> Result<Table, Error> {
126+
pub fn load_config(client_name: &str) -> Result<Config, Error> {
66127
let path = get_config_path(client_name)?;
67-
match fs::read(&path) {
128+
let config = match fs::read(&path) {
68129
Ok(bytes) => match toml::from_slice(&bytes) {
69130
Ok(config) => Ok(config),
70131
Err(_) => {
@@ -76,10 +137,19 @@ pub fn load_config(client_name: &str) -> Result<Table, Error> {
76137
}
77138
},
78139
Err(_) => init_config_at(client_name, &path),
140+
}?;
141+
if !config.projects_dir.exists() {
142+
fs::create_dir_all(&config.projects_dir).with_context(|| {
143+
format!(
144+
"Failed to create projects-dir at {}",
145+
config.projects_dir.display()
146+
)
147+
})?;
79148
}
149+
Ok(config)
80150
}
81151

82-
pub fn save_config(client_name: &str, config: Table) -> Result<(), Error> {
152+
pub fn save_config(client_name: &str, config: Config) -> Result<(), Error> {
83153
let path = get_config_path(client_name)?;
84154
let toml = toml::to_string_pretty(&config).context("Failed to serialize HashMap")?;
85155
fs::write(&path, toml.as_bytes())

tmc-langs-cli/src/main.rs

Lines changed: 140 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use tmc_langs_util::{
3535
};
3636
use toml::{map::Map as TomlMap, Value as TomlValue};
3737
use url::Url;
38+
use walkdir::WalkDir;
3839

3940
#[quit::main]
4041
fn main() {
@@ -1415,7 +1416,7 @@ fn run_settings(
14151416
warnings: &[anyhow::Error],
14161417
) -> Result<PrintToken> {
14171418
let client_name = matches.value_of("client-name").unwrap();
1418-
let mut map = config::load_config(client_name)?;
1419+
let mut config = config::load_config(client_name)?;
14191420

14201421
match matches.subcommand() {
14211422
("get", Some(matches)) => {
@@ -1425,37 +1426,156 @@ fn run_settings(
14251426
result: OutputResult::RetrievedData,
14261427
message: Some("Retrieved value".to_string()),
14271428
percent_done: 1.0,
1428-
data: map.get(key),
1429+
data: Some(config.get(key)),
14291430
});
14301431
print_output(&output, pretty, warnings)
14311432
}
1432-
("set", Some(matches)) => {
1433-
let key = matches.value_of("key").unwrap();
1434-
let value = matches.value_of("json").unwrap();
1433+
("list", Some(_)) => {
1434+
let output = Output::OutputData(OutputData {
1435+
status: Status::Finished,
1436+
result: OutputResult::RetrievedData,
1437+
message: Some("Retrieved settings".to_string()),
1438+
percent_done: 1.0,
1439+
data: Some(config),
1440+
});
1441+
print_output(&output, pretty, warnings)
1442+
}
1443+
("move-projects-dir", Some(matches)) => {
1444+
let dir = matches.value_of("dir").unwrap();
1445+
let target = PathBuf::from(dir);
14351446

1436-
let value = serde_json::from_str(value)
1437-
.with_context(|| format!("Failed to deserialize {} as JSON", value))?;
1438-
let value = json_to_toml(value)?;
1447+
if target.is_file() {
1448+
anyhow::bail!("The target path points to a file.")
1449+
}
1450+
if !target.exists() {
1451+
fs::create_dir_all(&target).with_context(|| {
1452+
format!("Failed to create directory at {}", target.display())
1453+
})?;
1454+
}
1455+
1456+
let target_canon = target
1457+
.canonicalize()
1458+
.with_context(|| format!("Failed to canonicalize {}", target.display()))?;
1459+
let prev_dir_canon = config.projects_dir.canonicalize().with_context(|| {
1460+
format!("Failed to canonicalize {}", config.projects_dir.display())
1461+
})?;
1462+
if target_canon == prev_dir_canon {
1463+
anyhow::bail!(
1464+
"Attempted to move the projects-dir to the directory it's already in."
1465+
)
1466+
}
1467+
1468+
let reporter = ProgressReporter::new(move |update| {
1469+
let output = Output::StatusUpdate::<()>(update);
1470+
print_output(&output, pretty, &[])?;
1471+
Ok(())
1472+
});
1473+
1474+
reporter
1475+
.progress("Moving projects-dir", 0.0, None)
1476+
.map_err(|e| anyhow::anyhow!(e))?;
1477+
1478+
let old_projects_dir = config.set_projects_dir(target.clone())?;
1479+
let mut file_count_copied = 0;
1480+
let mut file_count_total = 0;
1481+
for entry in WalkDir::new(&old_projects_dir) {
1482+
let entry = entry.with_context(|| {
1483+
format!("Failed to read file inside {}", old_projects_dir.display())
1484+
})?;
1485+
if entry.path().is_file() {
1486+
file_count_total += 1;
1487+
}
1488+
}
1489+
for entry in WalkDir::new(&old_projects_dir).contents_first(true) {
1490+
let entry = entry.with_context(|| {
1491+
format!("Failed to read file inside {}", old_projects_dir.display())
1492+
})?;
1493+
let entry_path = entry.path();
1494+
1495+
if entry_path.is_file() {
1496+
let relative = entry_path.strip_prefix(&old_projects_dir).unwrap();
1497+
let target_path = target.join(relative);
1498+
log::debug!(
1499+
"Moving {} -> {}",
1500+
entry_path.display(),
1501+
target_path.display()
1502+
);
1503+
1504+
// create parent dir for target and copy it, remove source file after
1505+
if let Some(parent) = target_path.parent() {
1506+
fs::create_dir_all(parent).with_context(|| {
1507+
format!("Failed to create directory at {}", parent.display())
1508+
})?;
1509+
}
1510+
fs::copy(entry_path, &target_path).with_context(|| {
1511+
format!(
1512+
"Failed to copy file from {} to {}",
1513+
entry_path.display(),
1514+
target_path.display()
1515+
)
1516+
})?;
1517+
fs::remove_file(entry_path).with_context(|| {
1518+
format!(
1519+
"Failed to remove file at {} after copying it",
1520+
entry_path.display()
1521+
)
1522+
})?;
14391523

1440-
map.insert(key.to_string(), value);
1441-
config::save_config(client_name, map)?;
1524+
file_count_copied += 1;
1525+
reporter
1526+
.progress(
1527+
format!("Moved file {} / {}", file_count_copied, file_count_total),
1528+
file_count_copied as f64 / file_count_total as f64,
1529+
None,
1530+
)
1531+
.map_err(|e| anyhow::anyhow!(e))?;
1532+
} else if entry_path.is_dir() {
1533+
log::debug!("Deleting {}", entry_path.display());
1534+
fs::remove_dir(entry_path).with_context(|| {
1535+
format!("Failed to remove directory at {}", entry_path.display())
1536+
})?;
1537+
}
1538+
}
1539+
1540+
config::save_config(client_name, config)?;
1541+
1542+
reporter
1543+
.finish_step("Finished moving project directory", None)
1544+
.map_err(|e| anyhow::anyhow!(e))?;
14421545

14431546
let output = Output::<()>::OutputData(OutputData {
14441547
status: Status::Finished,
14451548
result: OutputResult::ExecutedCommand,
1446-
message: Some("Set setting".to_string()),
1549+
message: Some("Moved project directory".to_string()),
14471550
percent_done: 1.0,
14481551
data: None,
14491552
});
14501553
print_output(&output, pretty, warnings)
14511554
}
1452-
("list", Some(_)) => {
1453-
let output = Output::OutputData(OutputData {
1555+
("set", Some(matches)) => {
1556+
let key = matches.value_of("key").unwrap();
1557+
let value = matches.value_of("json").unwrap();
1558+
1559+
let value = match serde_json::from_str(value) {
1560+
Ok(json) => json,
1561+
Err(_) => {
1562+
// interpret as string
1563+
JsonValue::String(value.to_string())
1564+
}
1565+
};
1566+
let value = json_to_toml(value)?;
1567+
1568+
config
1569+
.insert(key.to_string(), value.clone())
1570+
.with_context(|| format!("Failed to set {} to {}", key, value))?;
1571+
config::save_config(client_name, config)?;
1572+
1573+
let output = Output::<()>::OutputData(OutputData {
14541574
status: Status::Finished,
1455-
result: OutputResult::RetrievedData,
1456-
message: Some("Retrieved settings".to_string()),
1575+
result: OutputResult::ExecutedCommand,
1576+
message: Some("Set setting".to_string()),
14571577
percent_done: 1.0,
1458-
data: Some(map),
1578+
data: None,
14591579
});
14601580
print_output(&output, pretty, warnings)
14611581
}
@@ -1473,8 +1593,10 @@ fn run_settings(
14731593
}
14741594
("unset", Some(matches)) => {
14751595
let key = matches.value_of("setting").unwrap();
1476-
map.remove(key);
1477-
config::save_config(client_name, map)?;
1596+
config
1597+
.remove(key)
1598+
.with_context(|| format!("Failed to unset {}", key))?;
1599+
config::save_config(client_name, config)?;
14781600

14791601
let output = Output::<()>::OutputData(OutputData {
14801602
status: Status::Finished,

tmc-langs-core/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ pub enum CoreError {
3535
UrlParse(String, #[source] url::ParseError),
3636
#[error("Failed to write response to {0}")]
3737
HttpWriteResponse(PathBuf, #[source] reqwest::Error),
38-
#[error("Failed to deserialize response as JSON")]
39-
HttpJsonResponse(#[source] reqwest::Error),
38+
#[error("Failed to deserialize response from {0} as JSON")]
39+
HttpJsonResponse(Url, #[source] reqwest::Error),
4040

4141
#[error("Already authenticated")]
4242
AlreadyAuthenticated,

0 commit comments

Comments
 (0)