bitcasky_common/fs/
core.rs

1use std::{
2    fs::{self, File},
3    io::Result,
4    path::{Path, PathBuf},
5};
6
7use log::debug;
8
9use crate::{fs::FileType, storage_id::StorageId};
10
11const TESTING_DIRECTORY: &str = "Testing";
12
13pub struct IdentifiedFile {
14    pub file_type: FileType,
15    pub file: File,
16    pub storage_id: Option<StorageId>,
17    pub path: PathBuf,
18}
19
20pub fn check_directory_is_writable(base_dir: &Path) -> bool {
21    if fs::metadata(base_dir)
22        .map(|meta| meta.permissions().readonly())
23        .unwrap_or(false)
24    {
25        return false;
26    }
27
28    // create a directory deliberately to check we have writable permission for target path
29    let testing_path = base_dir.join(TESTING_DIRECTORY);
30    if testing_path.exists() && !fs::remove_dir(&testing_path).map(|_| true).unwrap_or(false) {
31        return false;
32    }
33
34    if !fs::create_dir(&testing_path)
35        .and_then(|_| fs::remove_dir(testing_path).map(|_| true))
36        .unwrap_or(false)
37    {
38        return false;
39    }
40    true
41}
42
43pub fn create_file<P: AsRef<Path>>(
44    base_dir: P,
45    file_type: FileType,
46    storage_id: Option<StorageId>,
47) -> std::io::Result<File> {
48    let path = file_type.get_path(base_dir, storage_id);
49    File::options()
50        .write(true)
51        .create(true)
52        .read(true)
53        .open(path)
54}
55
56pub fn open_file<P: AsRef<Path>>(
57    base_dir: P,
58    file_type: FileType,
59    storage_id: Option<StorageId>,
60) -> std::io::Result<IdentifiedFile> {
61    let path = file_type.get_path(base_dir, storage_id);
62    let file = if std::fs::metadata(&path)?.permissions().readonly() {
63        File::options().read(true).open(&path)?
64    } else {
65        File::options().read(true).write(true).open(&path)?
66    };
67    Ok(IdentifiedFile {
68        file_type,
69        file,
70        storage_id,
71        path,
72    })
73}
74
75pub fn delete_file(
76    base_dir: &Path,
77    file_type: FileType,
78    storage_id: Option<StorageId>,
79) -> Result<()> {
80    let path = file_type.get_path(base_dir, storage_id);
81    if path.exists() {
82        fs::remove_file(path)?;
83        if let Some(id) = storage_id {
84            debug!(
85                "Delete {} type file with id: {} under path: {:?}",
86                file_type, id, base_dir
87            )
88        } else {
89            debug!("Delete {} type file under path: {:?}", file_type, base_dir)
90        }
91    }
92    Ok(())
93}
94
95pub fn move_file(
96    file_type: FileType,
97    storage_id: Option<StorageId>,
98    from_dir: &Path,
99    to_dir: &Path,
100) -> Result<()> {
101    let from_p = file_type.get_path(from_dir, storage_id);
102    if from_p.exists() {
103        let to_p = file_type.get_path(to_dir, storage_id);
104        return fs::rename(from_p, to_p);
105    }
106    Ok(())
107}
108
109pub fn truncate_file(file: &mut File, capacity: usize) -> std::io::Result<()> {
110    // fs4 provides some cross-platform bindings which help for Windows.
111    #[cfg(not(unix))]
112    file.allocate(capacity as u64)?;
113    // For all unix systems we can just use ftruncate directly
114    #[cfg(unix)]
115    {
116        rustix::fs::ftruncate(file, capacity as u64)?;
117    }
118    Ok(())
119}
120
121pub fn change_storage_id(
122    base_dir: &Path,
123    file_type: FileType,
124    from_storage_id: StorageId,
125    to_storage_id: StorageId,
126) -> Result<()> {
127    debug!(
128        "Change file id from {} to {}",
129        from_storage_id, to_storage_id
130    );
131    let from_p = file_type.get_path(base_dir, Some(from_storage_id));
132    let to_p = file_type.get_path(base_dir, Some(to_storage_id));
133    fs::rename(from_p, to_p)?;
134    Ok(())
135}
136
137pub fn create_dir(base_dir: &Path) -> Result<()> {
138    if !base_dir.exists() {
139        std::fs::create_dir(base_dir)?;
140    }
141
142    Ok(())
143}
144
145pub fn delete_dir(base_dir: &Path) -> Result<()> {
146    fs::remove_dir_all(base_dir)?;
147    Ok(())
148}
149
150pub fn get_storage_ids_in_dir(dir_path: &Path, file_type: FileType) -> Vec<StorageId> {
151    let mut actual_storage_ids = vec![];
152    for path in fs::read_dir(dir_path).unwrap() {
153        let file_dir_entry = path.unwrap();
154        let file_path = file_dir_entry.path();
155        if file_path.is_dir() {
156            continue;
157        }
158        if !file_type.check_file_belongs_to_type(&file_path) {
159            continue;
160        }
161
162        let id = file_type
163            .parse_storage_id_from_file_name(&file_path)
164            .unwrap();
165        actual_storage_ids.push(id);
166    }
167    actual_storage_ids.sort();
168    actual_storage_ids
169}
170
171// used by some tests
172#[allow(dead_code)]
173pub fn is_empty_dir(dir: &Path) -> Result<bool> {
174    let paths = fs::read_dir(dir)?;
175
176    for path in paths {
177        let file_path = path?;
178        if file_path.path() == dir {
179            continue;
180        }
181        return Ok(false);
182    }
183    Ok(true)
184}
185
186#[cfg(test)]
187mod tests {
188    use std::{
189        io::{ErrorKind, Read, Result, Write},
190        vec,
191    };
192
193    use super::*;
194    use bytes::{Buf, Bytes, BytesMut};
195    use test_log::test;
196    use utilities::common::get_temporary_directory_path;
197
198    fn open_file_by_path(file_type: FileType, file_path: &Path) -> Result<IdentifiedFile> {
199        if file_type.check_file_belongs_to_type(file_path) {
200            let storage_id = file_type.parse_storage_id_from_file_name(file_path);
201            let file = File::options().read(true).open(file_path)?;
202            return Ok(IdentifiedFile {
203                file_type,
204                file,
205                storage_id,
206                path: file_path.to_path_buf(),
207            });
208        }
209        let file_name = file_path
210            .file_name()
211            .map(|s| s.to_str().unwrap_or(""))
212            .unwrap_or("");
213        Err(std::io::Error::new(
214            ErrorKind::InvalidInput,
215            format!("invalid file name: {}", file_name),
216        ))
217    }
218
219    #[test]
220    fn test_create_file() {
221        let dir = get_temporary_directory_path();
222        let storage_id = Some(123);
223        let file_path = FileType::DataFile.get_path(&dir, storage_id);
224        assert!(!file_path.exists());
225        create_file(&dir, FileType::DataFile, storage_id).unwrap();
226        assert!(file_path.exists());
227    }
228
229    #[test]
230    fn test_delete_file() {
231        let dir = get_temporary_directory_path();
232        let storage_id = Some(123);
233        let file_path = FileType::DataFile.get_path(&dir, storage_id);
234        assert!(!file_path.exists());
235        create_file(&dir, FileType::DataFile, storage_id).unwrap();
236        assert!(file_path.exists());
237        delete_file(&dir, FileType::DataFile, storage_id).unwrap();
238        assert!(!file_path.exists());
239    }
240
241    #[test]
242    fn test_open_file() {
243        let dir = get_temporary_directory_path();
244        let storage_id = Some(123);
245        let mut file = create_file(&dir, FileType::DataFile, storage_id).unwrap();
246        let mut bs = BytesMut::with_capacity(8);
247        let expect_val: u64 = 12345;
248        bs.extend_from_slice(&expect_val.to_be_bytes());
249        let bs = bs.freeze();
250        file.write_all(&bs).unwrap();
251
252        {
253            let mut f = open_file(&dir, FileType::DataFile, storage_id).unwrap();
254            assert_eq!(storage_id, f.storage_id);
255            assert_eq!(FileType::DataFile, f.file_type);
256
257            let mut header_buf = vec![0; 8];
258            f.file.read_exact(&mut header_buf).unwrap();
259
260            let mut actual = Bytes::from(header_buf);
261            assert_eq!(expect_val, actual.get_u64());
262        }
263
264        {
265            let p = FileType::DataFile.get_path(&dir, storage_id);
266            let mut f = open_file_by_path(FileType::DataFile, &p).unwrap();
267            assert_eq!(storage_id, f.storage_id);
268            assert_eq!(FileType::DataFile, f.file_type);
269
270            let mut header_buf = vec![0; 8];
271            f.file.read_exact(&mut header_buf).unwrap();
272
273            let mut actual = Bytes::from(header_buf);
274            assert_eq!(expect_val, actual.get_u64());
275        }
276    }
277
278    #[test]
279    fn test_commit_file() {
280        let from_dir = get_temporary_directory_path();
281        let to_dir = get_temporary_directory_path();
282        let storage_id = Some(123);
283        create_file(&from_dir, FileType::DataFile, storage_id).unwrap();
284        assert!(FileType::DataFile.get_path(&from_dir, storage_id).exists());
285        move_file(FileType::DataFile, storage_id, &from_dir, &to_dir).unwrap();
286        assert!(!FileType::DataFile.get_path(&from_dir, storage_id).exists());
287        assert!(FileType::DataFile.get_path(&to_dir, storage_id).exists());
288    }
289
290    #[test]
291    fn test_change_storage_id() {
292        let dir = get_temporary_directory_path();
293        let storage_id = 123;
294        let new_storage_id = 456;
295        create_file(&dir, FileType::DataFile, Some(storage_id)).unwrap();
296        assert!(FileType::DataFile.get_path(&dir, Some(storage_id)).exists());
297        change_storage_id(&dir, FileType::DataFile, storage_id, new_storage_id).unwrap();
298        assert!(!FileType::DataFile.get_path(&dir, Some(storage_id)).exists());
299        assert!(FileType::DataFile
300            .get_path(&dir, Some(new_storage_id))
301            .exists());
302    }
303
304    #[test]
305    fn test_create_dir() {
306        let dir = get_temporary_directory_path().join(TESTING_DIRECTORY);
307        assert!(!dir.exists());
308        create_dir(&dir).unwrap();
309        assert!(dir.exists());
310    }
311
312    #[test]
313    fn test_delete_dir() {
314        let dir = get_temporary_directory_path().join(TESTING_DIRECTORY);
315        create_dir(&dir).unwrap();
316        create_file(&dir, FileType::DataFile, Some(1230)).unwrap();
317        assert!(dir.exists());
318        delete_dir(&dir).unwrap();
319        assert!(!dir.exists());
320    }
321
322    #[test]
323    fn test_is_empty_dir() {
324        let dir = get_temporary_directory_path().join(TESTING_DIRECTORY);
325        create_dir(&dir).unwrap();
326        assert!(is_empty_dir(&dir).unwrap());
327        create_file(&dir, FileType::DataFile, Some(1230)).unwrap();
328        assert!(!is_empty_dir(&dir).unwrap());
329    }
330
331    #[test]
332    fn test_get_storage_ids_in_dir() {
333        let dir = get_temporary_directory_path();
334        create_file(&dir, FileType::DataFile, Some(103)).unwrap();
335        create_file(&dir, FileType::HintFile, Some(100)).unwrap();
336        create_file(&dir, FileType::DataFile, Some(102)).unwrap();
337        create_file(&dir, FileType::DataFile, Some(101)).unwrap();
338        let storage_ids = get_storage_ids_in_dir(&dir, FileType::DataFile);
339        assert_eq!(vec![101, 102, 103], storage_ids);
340    }
341}