diff --git a/tmc-langs-cli/src/config.rs b/tmc-langs-cli/src/config.rs index 9dcc2c9ab77..0894c4a8d63 100644 --- a/tmc-langs-cli/src/config.rs +++ b/tmc-langs-cli/src/config.rs @@ -10,8 +10,9 @@ pub use self::tmc_config::{ConfigValue, TmcConfig}; use crate::output::LocalExercise; use anyhow::{Context, Error}; -use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::{collections::BTreeMap, env, fs}; +use tmc_langs_framework::file_util; // base directory for a given plugin's settings files fn get_tmc_dir(client_name: &str) -> Result { @@ -26,7 +27,8 @@ pub fn list_local_course_exercises( client_name: &str, course_slug: &str, ) -> Result, anyhow::Error> { - let projects_dir = TmcConfig::load(client_name)?.projects_dir; + let config_path = TmcConfig::get_location(client_name)?; + let projects_dir = TmcConfig::load(client_name, &config_path)?.projects_dir; let mut projects_config = ProjectsConfig::load(&projects_dir)?; let exercises = projects_config @@ -43,3 +45,185 @@ pub fn list_local_course_exercises( } Ok(local_exercises) } + +pub fn migrate( + tmc_config: &TmcConfig, + course_slug: &str, + exercise_slug: &str, + exercise_id: usize, + exercise_checksum: &str, + exercise_path: &Path, +) -> anyhow::Result<()> { + let mut lock = file_util::FileLock::new(exercise_path.to_path_buf())?; + let guard = lock.lock()?; + + let mut projects_config = ProjectsConfig::load(&tmc_config.projects_dir)?; + let course_config = projects_config + .courses + .entry(course_slug.to_string()) + .or_insert(CourseConfig { + course: course_slug.to_string(), + exercises: BTreeMap::new(), + }); + + let target_dir = ProjectsConfig::get_exercise_download_target( + &tmc_config.projects_dir, + course_slug, + exercise_slug, + ); + if target_dir.exists() { + anyhow::bail!( + "Tried to migrate exercise to {}; however, something already exists at that path.", + target_dir.display() + ); + } + + course_config.exercises.insert( + exercise_slug.to_string(), + Exercise { + id: exercise_id, + checksum: exercise_checksum.to_string(), + }, + ); + + super::move_dir(exercise_path, guard, &target_dir)?; + course_config.save_to_projects_dir(&tmc_config.projects_dir)?; + Ok(()) +} + +pub fn move_projects_dir( + mut tmc_config: TmcConfig, + config_path: &Path, + target: PathBuf, +) -> anyhow::Result<()> { + if target.is_file() { + anyhow::bail!("The target path points to a file.") + } + if !target.exists() { + fs::create_dir_all(&target) + .with_context(|| format!("Failed to create directory at {}", target.display()))?; + } + + let target_canon = target + .canonicalize() + .with_context(|| format!("Failed to canonicalize {}", target.display()))?; + let prev_dir_canon = tmc_config.projects_dir.canonicalize().with_context(|| { + format!( + "Failed to canonicalize {}", + tmc_config.projects_dir.display() + ) + })?; + if target_canon == prev_dir_canon { + anyhow::bail!("Attempted to move the projects-dir to the directory it's already in.") + } + + let old_projects_dir = tmc_config.set_projects_dir(target.clone())?; + + let mut lock = file_util::FileLock::new(old_projects_dir.clone())?; + let guard = lock.lock()?; + + super::move_dir(&old_projects_dir, guard, &target)?; + tmc_config.save(config_path)?; + Ok(()) +} + +#[cfg(test)] +mod test { + use toml::value::Table; + + use super::*; + + fn init() { + use log::*; + use simple_logger::*; + let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init(); + } + + fn file_to( + target_dir: impl AsRef, + target_relative: impl AsRef, + contents: impl AsRef<[u8]>, + ) -> PathBuf { + let target = target_dir.as_ref().join(target_relative); + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + std::fs::write(&target, contents.as_ref()).unwrap(); + target + } + + #[test] + fn migrates() { + init(); + + let projects_dir = tempfile::tempdir().unwrap(); + let exercise_path = tempfile::tempdir().unwrap(); + + let tmc_config = TmcConfig { + projects_dir: projects_dir.path().to_path_buf(), + table: Table::new(), + }; + + file_to(&exercise_path, "some_file", ""); + + assert!(!projects_dir + .path() + .join("course/exercise/some_file") + .exists()); + + migrate( + &tmc_config, + "course", + "exercise", + 0, + "checksum", + exercise_path.path(), + ) + .unwrap(); + + assert!(projects_dir + .path() + .join("course/exercise/some_file") + .exists()); + + assert!(!exercise_path.path().exists()); + } + + #[test] + fn moves_projects_dir() { + init(); + + let projects_dir = tempfile::tempdir().unwrap(); + let target_dir = tempfile::tempdir().unwrap(); + + let config_path = tempfile::NamedTempFile::new().unwrap(); + let tmc_config = TmcConfig { + projects_dir: projects_dir.path().to_path_buf(), + table: Table::new(), + }; + + file_to( + projects_dir.path(), + "some course/some exercise/some file", + "", + ); + + assert!(!target_dir + .path() + .join("some course/some exercise/some file") + .exists()); + + move_projects_dir( + tmc_config, + config_path.path(), + target_dir.path().to_path_buf(), + ) + .unwrap(); + + assert!(target_dir + .path() + .join("some course/some exercise/some file") + .exists()); + assert!(!projects_dir.path().exists()); + } +} diff --git a/tmc-langs-cli/src/config/tmc_config.rs b/tmc-langs-cli/src/config/tmc_config.rs index e492218bd2b..9406d9f6467 100644 --- a/tmc-langs-cli/src/config/tmc_config.rs +++ b/tmc-langs-cli/src/config/tmc_config.rs @@ -61,8 +61,7 @@ impl TmcConfig { Ok(target) } - pub fn save(self, client_name: &str) -> Result<()> { - let path = Self::get_location(client_name)?; + pub fn save(self, path: &Path) -> Result<()> { if let Some(parent) = path.parent() { file_util::create_dir_all(parent)?; } @@ -82,9 +81,7 @@ impl TmcConfig { Ok(()) } - pub fn load(client_name: &str) -> Result { - let path = Self::get_location(client_name)?; - + pub fn load(client_name: &str, path: &Path) -> Result { // try to open config file let config = match file_util::open_file_lock(&path) { Ok(mut lock) => { @@ -161,11 +158,11 @@ impl TmcConfig { } // path to the configuration file - fn get_location(client_name: &str) -> Result { + pub fn get_location(client_name: &str) -> Result { super::get_tmc_dir(client_name).map(|dir| dir.join("config.toml")) } - // some client use a different name for the directory + // some clients use a different name for the directory fn get_client_stub(client: &str) -> &str { match client { "vscode_plugin" => "vscode", diff --git a/tmc-langs-cli/src/lib.rs b/tmc-langs-cli/src/lib.rs index 03d0b157dab..a0066fb294c 100644 --- a/tmc-langs-cli/src/lib.rs +++ b/tmc-langs-cli/src/lib.rs @@ -38,7 +38,10 @@ use tmc_client::{ }; use tmc_client::{ClientError, FeedbackAnswer, TmcClient, Token}; use tmc_langs_framework::{ - domain::StyleValidationResult, error::CommandError, file_util, warning_reporter, + domain::StyleValidationResult, + error::CommandError, + file_util::{self, FileLockGuard}, + warning_reporter, }; use tmc_langs_util::{ progress_reporter, @@ -656,7 +659,8 @@ fn run_core( ("check-exercise-updates", Some(_)) => { let mut updated_exercises = vec![]; - let projects_dir = TmcConfig::load(client_name)?.projects_dir; + let config_path = TmcConfig::get_location(client_name)?; + let projects_dir = TmcConfig::load(client_name, &config_path)?.projects_dir; let config = ProjectsConfig::load(&projects_dir)?; let local_exercises = config .courses @@ -758,7 +762,8 @@ fn run_core( .collect::>()?; let exercises_details = client.get_exercises_details(exercises)?; - let projects_dir = TmcConfig::load(client_name)?.projects_dir; + let config_path = TmcConfig::get_location(client_name)?; + let projects_dir = TmcConfig::load(client_name, &config_path)?.projects_dir; let mut projects_config = ProjectsConfig::load(&projects_dir)?; // separate downloads into ones that don't need to be downloaded and ones that do @@ -1332,7 +1337,8 @@ fn run_core( let mut to_be_skipped = vec![]; let mut course_data = HashMap::>::new(); - let projects_dir = TmcConfig::load(client_name)?.projects_dir; + let config_path = TmcConfig::get_location(client_name)?; + let projects_dir = TmcConfig::load(client_name, &config_path)?.projects_dir; let mut projects_config = ProjectsConfig::load(&projects_dir)?; let local_exercises = projects_config .courses @@ -1441,7 +1447,9 @@ fn run_core( fn run_settings(matches: &ArgMatches, pretty: bool) -> Result { let client_name = matches.value_of("client-name").unwrap(); - let mut tmc_config = TmcConfig::load(client_name)?; + + let config_path = TmcConfig::get_location(client_name)?; + let mut tmc_config = TmcConfig::load(client_name, &config_path)?; match matches.subcommand() { ("get", Some(matches)) => { @@ -1468,36 +1476,14 @@ fn run_settings(matches: &ArgMatches, pretty: bool) -> Result { let exercise_checksum = matches.value_of("exercise-checksum").unwrap(); - file_util::lock!(exercise_path); - - let mut projects_config = ProjectsConfig::load(&tmc_config.projects_dir)?; - let course_config = projects_config - .courses - .entry(course_slug.to_string()) - .or_insert(CourseConfig { - course: course_slug.to_string(), - exercises: BTreeMap::new(), - }); - - let target_dir = ProjectsConfig::get_exercise_download_target( - &tmc_config.projects_dir, + config::migrate( + &tmc_config, course_slug, exercise_slug, - ); - if target_dir.exists() { - anyhow::bail!("Tried to migrate exercise to {}; however, something already exists at that path.", target_dir.display()); - } - - course_config.exercises.insert( - exercise_slug.to_string(), - Exercise { - id: exercise_id, - checksum: exercise_checksum.to_string(), - }, - ); - - move_dir(exercise_path, &target_dir)?; - course_config.save_to_projects_dir(&tmc_config.projects_dir)?; + exercise_id, + exercise_checksum, + exercise_path, + )?; let output = Output::finished_with_data("migrated exercise", None); print_output(&output, pretty) @@ -1506,33 +1492,7 @@ fn run_settings(matches: &ArgMatches, pretty: bool) -> Result { let dir = matches.value_of("dir").unwrap(); let target = PathBuf::from(dir); - if target.is_file() { - anyhow::bail!("The target path points to a file.") - } - if !target.exists() { - fs::create_dir_all(&target).with_context(|| { - format!("Failed to create directory at {}", target.display()) - })?; - } - - let target_canon = target - .canonicalize() - .with_context(|| format!("Failed to canonicalize {}", target.display()))?; - let prev_dir_canon = tmc_config.projects_dir.canonicalize().with_context(|| { - format!( - "Failed to canonicalize {}", - tmc_config.projects_dir.display() - ) - })?; - if target_canon == prev_dir_canon { - anyhow::bail!( - "Attempted to move the projects-dir to the directory it's already in." - ) - } - - let old_projects_dir = tmc_config.set_projects_dir(target.clone())?; - move_dir(&old_projects_dir, &target)?; - tmc_config.save(client_name)?; + config::move_projects_dir(tmc_config, &config_path, target)?; let output = Output::finished_with_data("moved project directory", None); print_output(&output, pretty) @@ -1553,7 +1513,7 @@ fn run_settings(matches: &ArgMatches, pretty: bool) -> Result { tmc_config .insert(key.to_string(), value.clone()) .with_context(|| format!("Failed to set {} to {}", key, value))?; - tmc_config.save(client_name)?; + tmc_config.save(&config_path)?; let output = Output::finished_with_data("set setting", None); print_output(&output, pretty) @@ -1569,7 +1529,7 @@ fn run_settings(matches: &ArgMatches, pretty: bool) -> Result { tmc_config .remove(key) .with_context(|| format!("Failed to unset {}", key))?; - tmc_config.save(client_name)?; + tmc_config.save(&config_path)?; let output = Output::finished_with_data("unset setting", None); print_output(&output, pretty) @@ -1718,7 +1678,7 @@ fn json_to_toml(json: JsonValue) -> Result { } } -fn move_dir(source: &Path, target: &Path) -> anyhow::Result<()> { +fn move_dir(source: &Path, source_lock: FileLockGuard, target: &Path) -> anyhow::Result<()> { let mut file_count_copied = 0; let mut file_count_total = 0; for entry in WalkDir::new(source) { @@ -1733,7 +1693,7 @@ fn move_dir(source: &Path, target: &Path) -> anyhow::Result<()> { format!("Moving dir {} -> {}", source.display(), target.display()), ); - for entry in WalkDir::new(source).contents_first(true) { + for entry in WalkDir::new(source).contents_first(true).min_depth(1) { let entry = entry.with_context(|| format!("Failed to read file inside {}", source.display()))?; let entry_path = entry.path(); @@ -1790,6 +1750,9 @@ fn move_dir(source: &Path, target: &Path) -> anyhow::Result<()> { } } + drop(source_lock); + fs::remove_dir(source)?; + finish_stage("Finished moving project directory"); Ok(()) } diff --git a/tmc-langs-util/src/error.rs b/tmc-langs-util/src/error.rs index 38825db0f78..8c119c9f6d1 100644 --- a/tmc-langs-util/src/error.rs +++ b/tmc-langs-util/src/error.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use thiserror::Error; use tmc_langs_framework::error::FileIo; +#[cfg(unix)] use crate::task_executor::ModeBits; #[derive(Debug, Error)] diff --git a/tmc-langs-util/src/task_executor/course_refresher.rs b/tmc-langs-util/src/task_executor/course_refresher.rs index b9696c12b03..3bb85f47a18 100644 --- a/tmc-langs-util/src/task_executor/course_refresher.rs +++ b/tmc-langs-util/src/task_executor/course_refresher.rs @@ -325,7 +325,7 @@ fn execute_zip( } #[cfg(not(unix))] -fn set_permissions(path: &Path) -> Result<(), UtilError> { +fn set_permissions(_path: &Path) -> Result<(), UtilError> { // NOP on non-Unix platforms Ok(()) }