1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, bail};
4use fs_err as fs;
5use uuid::Uuid;
6
7use crate::audit::AuditEntry;
8use crate::backends::Backend;
9use crate::config::Config;
10use crate::paths;
11
12pub fn create_metadata_folder(root_dir: impl AsRef<Path>, config: &Config) -> Result<()> {
15 let metadata_dir = root_dir.as_ref().join(config.metadata_folder_name());
16 log::debug!("Creating metadata folder: {}", metadata_dir.display());
17 if let Err(e) = fs::create_dir(&metadata_dir)
18 && e.kind() != std::io::ErrorKind::AlreadyExists
19 {
20 return Err(e.into());
21 }
22 Ok(())
23}
24
25pub fn init(root_dir: impl AsRef<Path>, mut config: Config) -> Result<PathBuf> {
28 let root_dir = root_dir.as_ref();
29
30 match &mut config.backend {
33 Backend::Local(b) => {
34 let storage_path = &b.path;
35 let abs_storage = if storage_path.exists() {
36 fs::canonicalize(storage_path)?
37 } else if let Some(parent) = storage_path.parent().filter(|p| p.exists()) {
38 fs::canonicalize(parent)?.join(storage_path.file_name().unwrap())
39 } else {
40 std::path::absolute(storage_path)?
41 };
42 let abs_root = fs::canonicalize(root_dir)?;
43 if abs_storage.starts_with(&abs_root) {
44 bail!("The given storage path is within the repository.");
45 }
46 b.path = abs_storage;
47 }
48 Backend::Server(_) => { }
49 }
50
51 if root_dir.join(paths::CONFIG_FILE_NAME).exists() {
52 bail!("dvs is already initialized (dvs.toml exists)");
53 }
54
55 let metadata_dir = root_dir.join(config.metadata_folder_name());
57 if metadata_dir.exists() && fs::read_dir(&metadata_dir)?.next().is_some() {
58 bail!(
59 "dvs is already initialized ({} exists and is not empty)",
60 config.metadata_folder_name()
61 );
62 }
63
64 log::debug!("Initializing backend");
65 if config.backend().init(config.compression())? {
66 if let Backend::Server(s) = config.backend() {
67 bail!("the project {} already exists on {}", s.name, s.url);
68 }
69 bail!("dvs is already initialized (backend storage exists)");
70 }
71
72 if matches!(config.backend(), Backend::Server(_)) {
74 config.set_compression(None);
75 }
76
77 config.save(root_dir)?;
78
79 let config_path = root_dir.join(paths::CONFIG_FILE_NAME);
80
81 if let Err(e) = create_metadata_folder(root_dir, &config) {
82 let _ = fs::remove_file(&config_path);
84 return Err(e);
85 }
86
87 match &config.backend {
88 Backend::Local(b) => {
89 let audit_entry =
90 AuditEntry::new_init(Uuid::new_v4(), config.clone(), root_dir.to_path_buf());
91 if let Err(e) = b.log_audit_entry(&audit_entry) {
92 log::error!("Failed to write init audit log {audit_entry:?}: {e}");
93 }
94 }
95 Backend::Server(_) => { }
96 }
97
98 log::info!("DVS repository initialized successfully");
99 Ok(root_dir.to_path_buf())
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::testutil::create_temp_git_repo;
106
107 #[test]
108 fn init_creates_config_and_directories() {
109 let (_tmp, root) = create_temp_git_repo();
110 let storage = root.parent().unwrap().join(".storage");
111
112 let config = Config::new_local(&storage, None).unwrap();
113 init(&root, config).unwrap();
114
115 assert!(root.join("dvs.toml").is_file());
117 assert!(root.join(".dvs").is_dir());
119 assert!(storage.is_dir());
121 }
122
123 #[test]
124 fn init_logs_audit_entry() {
125 let (_tmp, root) = create_temp_git_repo();
126 let storage = root.parent().unwrap().join(".storage");
127
128 let config = Config::new_local(&storage, None).unwrap();
129 init(&root, config.clone()).unwrap();
130
131 let entries = config.backend().get_audit_entries(&[]).unwrap();
132 assert_eq!(entries.len(), 1);
133 assert!(matches!(
134 entries[0].action,
135 crate::audit::Action::Init { .. }
136 ));
137 }
138
139 #[test]
140 fn init_fails_if_already_initialized() {
141 let (_tmp, root) = create_temp_git_repo();
142 let storage = root.parent().unwrap().join(".storage");
143
144 let config = Config::new_local(&storage, None).unwrap();
145 init(&root, config.clone()).unwrap();
146
147 let result = init(&root, config.clone());
149 assert!(result.is_err(), "second init should fail");
150 }
151
152 #[test]
153 fn init_fails_if_backend_already_initialized() {
154 let (_tmp, root) = create_temp_git_repo();
155 let storage = root.parent().unwrap().join(".storage");
156
157 let config = Config::new_local(&storage, None).unwrap();
158 init(&root, config.clone()).unwrap();
159
160 fs::remove_file(root.join("dvs.toml")).unwrap();
162 fs::remove_dir_all(root.join(".dvs")).unwrap();
163
164 let result = init(&root, config);
165 assert!(
166 result.is_err(),
167 "should detect backend is already initialized"
168 );
169 }
170
171 #[test]
172 fn init_rejects_storage_inside_repo() {
173 let (_tmp, root) = create_temp_git_repo();
174 let storage = root.join("inside").join(".storage");
176
177 let config = Config::new_local(&storage, None).unwrap();
178 let result = init(&root, config);
179 assert!(
180 result.is_err(),
181 "init should reject storage inside the repository"
182 );
183 assert!(
184 result
185 .unwrap_err()
186 .to_string()
187 .contains("within the repository"),
188 "error should explain the storage path is inside the repo"
189 );
190 assert!(!root.join("dvs.toml").exists());
192 }
193
194 #[test]
195 fn init_succeeds_in_subdirectory_of_initialized_project() {
196 let (_tmp, root) = create_temp_git_repo();
197 let storage = root.parent().unwrap().join(".storage");
198
199 let config = Config::new_local(&storage, None).unwrap();
201 init(&root, config).unwrap();
202
203 let subdir = root.join("nested");
205 fs::create_dir(&subdir).unwrap();
206 let nested_storage = root.parent().unwrap().join(".nested-storage");
207 let nested_config = Config::new_local(&nested_storage, None).unwrap();
208 let result = init(&subdir, nested_config);
209 assert!(
210 result.is_ok(),
211 "init in subdirectory should succeed: {result:?}"
212 );
213 assert!(subdir.join("dvs.toml").is_file());
214 assert!(subdir.join(".dvs").is_dir());
215 }
216
217 #[cfg(unix)]
218 #[test]
219 fn init_cleans_up_on_backend_failure() {
220 let (_tmp, root) = create_temp_git_repo();
221 let storage = Path::new("/dev/null/impossible");
223
224 let config = Config::new_local(storage, None).unwrap();
225 let result = init(&root, config);
226 assert!(result.is_err());
227
228 assert!(
230 !root.join("dvs.toml").exists(),
231 "dvs.toml should be cleaned up"
232 );
233 assert!(!root.join(".dvs").exists(), ".dvs should be cleaned up");
234
235 let valid_storage = root.parent().unwrap().join(".storage");
237 let config = Config::new_local(&valid_storage, None).unwrap();
238 init(&root, config).unwrap();
239 assert!(root.join("dvs.toml").is_file());
240 assert!(root.join(".dvs").is_dir());
241 }
242
243 #[test]
244 fn init_preserves_preexisting_metadata_dir_on_failure() {
245 let (_tmp, root) = create_temp_git_repo();
246 let metadata_dir = root.join(".dvs");
248 fs::create_dir(&metadata_dir).unwrap();
249 let marker = metadata_dir.join("marker.txt");
250 fs::write(&marker, "keep me").unwrap();
251
252 let storage = Path::new("/dev/null/impossible");
253 let config = Config::new_local(storage, None).unwrap();
254 let result = init(&root, config);
255 assert!(result.is_err());
256
257 assert!(
259 !root.join("dvs.toml").exists(),
260 "dvs.toml should be cleaned up"
261 );
262 assert!(metadata_dir.is_dir(), ".dvs should be preserved");
264 assert!(
265 marker.is_file(),
266 "marker file inside .dvs should be preserved"
267 );
268 }
269}