tmc_langs/
config.rs

1//! Handles the CLI's configuration files and credentials.
2
3mod credentials;
4mod projects_config;
5mod tmc_config;
6
7pub use self::{
8    credentials::Credentials,
9    projects_config::{ProjectsConfig, ProjectsDirTmcExercise, TmcCourseConfig},
10    tmc_config::TmcConfig,
11};
12use crate::{TMC_LANGS_CONFIG_DIR_VAR, data::LocalTmcExercise, error::LangsError};
13use std::{
14    collections::BTreeMap,
15    env,
16    path::{Path, PathBuf},
17};
18use tmc_langs_util::{
19    FileError,
20    file_util::{self, Lock, LockOptions},
21};
22
23// base directory for a given plugin's settings files
24fn get_tmc_dir(client_name: &str) -> Result<PathBuf, LangsError> {
25    let config_dir = match env::var(TMC_LANGS_CONFIG_DIR_VAR) {
26        Ok(v) => PathBuf::from(v),
27        Err(_) => dirs::config_dir().ok_or(LangsError::NoConfigDir)?,
28    };
29    Ok(config_dir.join(format!("tmc-{client_name}")))
30}
31
32/// Returns all of the exercises for the given course.
33pub fn list_local_tmc_course_exercises(
34    client_name: &str,
35    course_slug: &str,
36) -> Result<Vec<LocalTmcExercise>, LangsError> {
37    log::debug!("listing local course exercises of {course_slug} for {client_name}");
38
39    let projects_dir = TmcConfig::load(client_name)?.projects_dir;
40    let mut projects_config = ProjectsConfig::load(&projects_dir)?;
41
42    let exercises = projects_config
43        .tmc_courses
44        .remove(course_slug)
45        .map(|cc| cc.exercises)
46        .unwrap_or_default();
47    let mut local_exercises: Vec<LocalTmcExercise> = vec![];
48    for (exercise_slug, _) in exercises {
49        local_exercises.push(LocalTmcExercise {
50            exercise_path: projects_dir.join(course_slug).join(&exercise_slug),
51            exercise_slug,
52        })
53    }
54    Ok(local_exercises)
55}
56
57/// Migrates an exercise from a location that's not managed by tmc-langs to the projects directory.
58pub fn migrate_exercise(
59    tmc_config: TmcConfig,
60    course_slug: &str,
61    exercise_slug: &str,
62    exercise_id: u32,
63    exercise_checksum: &str,
64    exercise_path: &Path,
65) -> Result<(), LangsError> {
66    log::debug!(
67        "migrating exercise {} from {}",
68        exercise_id,
69        exercise_path.display()
70    );
71
72    let mut lock = Lock::dir(exercise_path, LockOptions::Write)?;
73    let _guard = lock.lock()?;
74    let mut projects_config = ProjectsConfig::load(&tmc_config.projects_dir)?;
75    let course_config = projects_config
76        .tmc_courses
77        .entry(course_slug.to_string())
78        .or_insert(TmcCourseConfig {
79            course: course_slug.to_string(),
80            exercises: BTreeMap::new(),
81        });
82
83    let target_dir = ProjectsConfig::get_tmc_exercise_download_target(
84        &tmc_config.projects_dir,
85        course_slug,
86        exercise_slug,
87    );
88    if target_dir.exists() {
89        return Err(LangsError::DirectoryExists(target_dir));
90    }
91
92    course_config.exercises.insert(
93        exercise_slug.to_string(),
94        ProjectsDirTmcExercise {
95            id: exercise_id,
96            checksum: exercise_checksum.to_string(),
97        },
98    );
99
100    super::move_dir(exercise_path, &target_dir)?;
101    course_config.save_to_projects_dir(&tmc_config.projects_dir)?;
102    Ok(())
103}
104
105/// Moves the projects directory from its current location to the target, taking all of the contained exercises with it.
106pub fn move_projects_dir(mut tmc_config: TmcConfig, target: PathBuf) -> Result<(), LangsError> {
107    log::debug!("moving projects dir to {}", target.display());
108
109    if target.is_file() {
110        return Err(FileError::UnexpectedFile(target).into());
111    }
112    if !target.exists() {
113        file_util::create_dir_all(&target)?;
114    }
115
116    let target_canon = target
117        .canonicalize()
118        .map_err(|e| LangsError::Canonicalize(target.clone(), e))?;
119    let prev_dir_canon = tmc_config
120        .projects_dir
121        .canonicalize()
122        .map_err(|e| LangsError::Canonicalize(target.clone(), e))?;
123    if target_canon == prev_dir_canon {
124        return Err(LangsError::MovingProjectsDirToItself);
125    }
126
127    let old_projects_dir = tmc_config.set_projects_dir(target.clone())?;
128
129    let mut lock = Lock::dir(old_projects_dir.clone(), LockOptions::Write)?;
130    let _guard = lock.lock()?;
131    super::move_dir(&old_projects_dir, &target)?;
132    tmc_config.save()?;
133    Ok(())
134}
135
136#[cfg(test)]
137#[allow(clippy::unwrap_used)]
138mod test {
139    use super::*;
140    use toml::value::Table;
141
142    fn init() {
143        use log::*;
144        use simple_logger::*;
145        let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init();
146    }
147
148    fn file_to(
149        target_dir: impl AsRef<std::path::Path>,
150        target_relative: impl AsRef<std::path::Path>,
151        contents: impl AsRef<[u8]>,
152    ) -> PathBuf {
153        let target = target_dir.as_ref().join(target_relative);
154        if let Some(parent) = target.parent() {
155            std::fs::create_dir_all(parent).unwrap();
156        }
157        std::fs::write(&target, contents.as_ref()).unwrap();
158        target
159    }
160
161    #[test]
162    fn migrates() {
163        init();
164
165        let projects_dir = tempfile::tempdir().unwrap();
166        let exercise_path = tempfile::tempdir().unwrap();
167
168        let tmc_config = TmcConfig {
169            location: PathBuf::new(),
170            projects_dir: projects_dir.path().to_path_buf(),
171            table: Table::new(),
172        };
173
174        file_to(&exercise_path, "some_file", "");
175
176        assert!(
177            !projects_dir
178                .path()
179                .join("course/exercise/some_file")
180                .exists()
181        );
182
183        migrate_exercise(
184            tmc_config,
185            "course",
186            "exercise",
187            0,
188            "checksum",
189            exercise_path.path(),
190        )
191        .unwrap();
192
193        assert!(
194            projects_dir
195                .path()
196                .join("course/exercise/some_file")
197                .exists()
198        );
199
200        assert!(!exercise_path.path().exists());
201    }
202
203    #[test]
204    fn moves_projects_dir() {
205        init();
206
207        // can't use a tempfile for the config location directly
208        // because windows won't let us replace a tempfile while it's "open"
209        let config_dir = tempfile::tempdir().unwrap();
210        let config_location = config_dir.path().join("tmc_config.temp");
211        let projects_dir = tempfile::tempdir().unwrap();
212        let target_dir = tempfile::tempdir().unwrap();
213
214        let tmc_config = TmcConfig {
215            location: config_location,
216            projects_dir: projects_dir.path().to_path_buf(),
217            table: Table::new(),
218        };
219
220        file_to(
221            projects_dir.path(),
222            "some course/some exercise/some file",
223            "",
224        );
225
226        assert!(
227            !target_dir
228                .path()
229                .join("some course/some exercise/some file")
230                .exists()
231        );
232
233        move_projects_dir(tmc_config, target_dir.path().to_path_buf()).unwrap();
234
235        assert!(
236            target_dir
237                .path()
238                .join("some course/some exercise/some file")
239                .exists()
240        );
241        assert!(!projects_dir.path().exists());
242    }
243}