Skip to main content
← dvs documentation Rust API reference

dvs/
init.rs

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::config::{Backend, Config};
9use crate::paths;
10
11/// Starts a new dvs project.
12/// We need a ready to use Config object + the current directory the user is in
13pub fn init(root_dir: impl AsRef<Path>, mut config: Config) -> Result<PathBuf> {
14    let root_dir = root_dir.as_ref();
15
16    // The storage directory must not live inside the repository, otherwise it
17    // would be tracked alongside the user's files.
18    match &mut config.backend {
19        Backend::Local(b) => {
20            let storage_path = &b.path;
21            let abs_storage = if storage_path.exists() {
22                fs::canonicalize(storage_path)?
23            } else if let Some(parent) = storage_path.parent().filter(|p| p.exists()) {
24                fs::canonicalize(parent)?.join(storage_path.file_name().unwrap())
25            } else {
26                std::path::absolute(storage_path)?
27            };
28            let abs_root = fs::canonicalize(root_dir)?;
29            if abs_storage.starts_with(&abs_root) {
30                bail!("The given storage path is within the repository.");
31            }
32            b.path = abs_storage;
33        }
34    }
35
36    if root_dir.join(paths::CONFIG_FILE_NAME).exists() {
37        bail!("dvs is already initialized (dvs.toml exists)");
38    }
39    if config.backend().is_initialized()? {
40        bail!("dvs is already initialized (backend storage exists)");
41    }
42    config.save(root_dir)?;
43
44    let metadata_dir = root_dir.join(config.metadata_folder_name());
45    let config_path = root_dir.join(paths::CONFIG_FILE_NAME);
46    let metadata_existed = metadata_dir.exists();
47
48    let result = (|| {
49        log::debug!("Creating metadata folder: {}", metadata_dir.display());
50        fs::create_dir(&metadata_dir)?;
51        log::debug!("Initializing backend");
52        config.backend().init()
53    })();
54
55    if let Err(e) = result {
56        // Best-effort cleanup of local artifacts we created
57        if !metadata_existed {
58            let _ = fs::remove_dir_all(&metadata_dir);
59        }
60        let _ = fs::remove_file(&config_path);
61        return Err(e);
62    }
63
64    let audit_entry = AuditEntry::new_init(Uuid::new_v4(), config.clone(), root_dir.to_path_buf());
65    if let Err(e) = config.backend().log_audit(&audit_entry) {
66        log::error!("Failed to write init audit log {audit_entry:?}: {e}");
67    }
68
69    log::info!("DVS repository initialized successfully");
70    Ok(root_dir.to_path_buf())
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::testutil::create_temp_git_repo;
77
78    #[test]
79    fn init_creates_config_and_directories() {
80        let (_tmp, root) = create_temp_git_repo();
81        let storage = root.parent().unwrap().join(".storage");
82
83        let config = Config::new_local(&storage, None).unwrap();
84        init(&root, config).unwrap();
85
86        // Config file should exist
87        assert!(root.join("dvs.toml").is_file());
88        // Metadata folder should exist
89        assert!(root.join(".dvs").is_dir());
90        // Storage folder should exist
91        assert!(storage.is_dir());
92    }
93
94    #[test]
95    fn init_logs_audit_entry() {
96        let (_tmp, root) = create_temp_git_repo();
97        let storage = root.parent().unwrap().join(".storage");
98
99        let config = Config::new_local(&storage, None).unwrap();
100        init(&root, config.clone()).unwrap();
101
102        let entries = config.backend().read_audit_file(&[]).unwrap();
103        assert_eq!(entries.len(), 1);
104        assert!(matches!(
105            entries[0].action,
106            crate::audit::Action::Init { .. }
107        ));
108    }
109
110    #[test]
111    fn init_fails_if_already_initialized() {
112        let (_tmp, root) = create_temp_git_repo();
113        let storage = root.parent().unwrap().join(".storage");
114
115        let config = Config::new_local(&storage, None).unwrap();
116        init(&root, config.clone()).unwrap();
117
118        // Second init should fail because dvs.toml exists
119        let result = init(&root, config.clone());
120        assert!(result.is_err(), "second init should fail");
121    }
122
123    #[test]
124    fn init_fails_if_backend_already_initialized() {
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        assert!(!config.backend().is_initialized().unwrap());
130        init(&root, config.clone()).unwrap();
131        assert!(config.backend().is_initialized().unwrap());
132
133        // Remove dvs.toml and .dvs but leave backend storage intact
134        fs::remove_file(root.join("dvs.toml")).unwrap();
135        fs::remove_dir_all(root.join(".dvs")).unwrap();
136
137        let result = init(&root, config);
138        assert!(
139            result.is_err(),
140            "should detect backend is already initialized"
141        );
142    }
143
144    #[test]
145    fn init_rejects_storage_inside_repo() {
146        let (_tmp, root) = create_temp_git_repo();
147        // Storage directory nested inside the repository root.
148        let storage = root.join("inside").join(".storage");
149
150        let config = Config::new_local(&storage, None).unwrap();
151        let result = init(&root, config);
152        assert!(
153            result.is_err(),
154            "init should reject storage inside the repository"
155        );
156        assert!(
157            result
158                .unwrap_err()
159                .to_string()
160                .contains("within the repository"),
161            "error should explain the storage path is inside the repo"
162        );
163        // Nothing should have been created.
164        assert!(!root.join("dvs.toml").exists());
165    }
166
167    #[test]
168    fn init_succeeds_in_subdirectory_of_initialized_project() {
169        let (_tmp, root) = create_temp_git_repo();
170        let storage = root.parent().unwrap().join(".storage");
171
172        // Initialize the parent project
173        let config = Config::new_local(&storage, None).unwrap();
174        init(&root, config).unwrap();
175
176        // Create a subdirectory and initialize a nested project there
177        let subdir = root.join("nested");
178        fs::create_dir(&subdir).unwrap();
179        let nested_storage = root.parent().unwrap().join(".nested-storage");
180        let nested_config = Config::new_local(&nested_storage, None).unwrap();
181        let result = init(&subdir, nested_config);
182        assert!(
183            result.is_ok(),
184            "init in subdirectory should succeed: {result:?}"
185        );
186        assert!(subdir.join("dvs.toml").is_file());
187        assert!(subdir.join(".dvs").is_dir());
188    }
189
190    #[cfg(unix)]
191    #[test]
192    fn init_cleans_up_on_backend_failure() {
193        let (_tmp, root) = create_temp_git_repo();
194        // Point storage at an impossible path so backend init fails
195        let storage = Path::new("/dev/null/impossible");
196
197        let config = Config::new_local(storage, None).unwrap();
198        let result = init(&root, config);
199        assert!(result.is_err());
200
201        // Local artifacts should have been cleaned up
202        assert!(
203            !root.join("dvs.toml").exists(),
204            "dvs.toml should be cleaned up"
205        );
206        assert!(!root.join(".dvs").exists(), ".dvs should be cleaned up");
207
208        // A retry with a valid storage path should now succeed
209        let valid_storage = root.parent().unwrap().join(".storage");
210        let config = Config::new_local(&valid_storage, None).unwrap();
211        init(&root, config).unwrap();
212        assert!(root.join("dvs.toml").is_file());
213        assert!(root.join(".dvs").is_dir());
214    }
215
216    #[test]
217    fn init_preserves_preexisting_metadata_dir_on_failure() {
218        let (_tmp, root) = create_temp_git_repo();
219        // Create a .dvs directory before init runs
220        let metadata_dir = root.join(".dvs");
221        fs::create_dir(&metadata_dir).unwrap();
222        let marker = metadata_dir.join("marker.txt");
223        fs::write(&marker, "keep me").unwrap();
224
225        let storage = Path::new("/dev/null/impossible");
226        let config = Config::new_local(storage, None).unwrap();
227        let result = init(&root, config);
228        assert!(result.is_err());
229
230        // dvs.toml should be cleaned up
231        assert!(
232            !root.join("dvs.toml").exists(),
233            "dvs.toml should be cleaned up"
234        );
235        // Pre-existing .dvs directory and its contents should be preserved
236        assert!(metadata_dir.is_dir(), ".dvs should be preserved");
237        assert!(
238            marker.is_file(),
239            "marker file inside .dvs should be preserved"
240        );
241    }
242}