1mod 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
23fn 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
32pub 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
57pub 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
105pub 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 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}