1use std::collections::HashMap;
7use std::fs::{File, OpenOptions};
8use std::io::{BufRead, BufReader, Write};
9
10use parking_lot::Mutex;
11
12const RENAME_OP: &str = "ren";
13const DELETE_OP: &str = "del";
14
15pub struct FsLog {
20 log_path: std::path::PathBuf,
21 lock: Mutex<()>,
22}
23
24impl FsLog {
25 pub fn new(log_path: std::path::PathBuf) -> Self {
27 Self {
28 log_path,
29 lock: Mutex::new(()),
30 }
31 }
32
33 pub fn log_rename(&self, time: i64, old_path: &str, new_path: &str) {
35 let _guard = self.lock.lock();
36 let _ = self.append(&format!(
37 "{} {} {} {}",
38 time,
39 RENAME_OP,
40 percent_encoding::utf8_percent_encode(old_path, percent_encoding::NON_ALPHANUMERIC),
41 percent_encoding::utf8_percent_encode(new_path, percent_encoding::NON_ALPHANUMERIC),
42 ));
43 }
44
45 pub fn log_delete(&self, time: i64, path: &str) {
47 let _guard = self.lock.lock();
48 let _ = self.append(&format!(
49 "{} {} {}",
50 time,
51 DELETE_OP,
52 percent_encoding::utf8_percent_encode(path, percent_encoding::NON_ALPHANUMERIC),
53 ));
54 }
55
56 pub fn renames_since(
60 &self,
61 user_prefix: &str,
62 after_timestamp: i64,
63 ) -> HashMap<String, String> {
64 let _guard = self.lock.lock();
65 let file = match File::open(&self.log_path) {
66 Ok(f) => f,
67 Err(_) => return HashMap::new(),
68 };
69
70 let reader = BufReader::new(file);
71 let mut result = HashMap::new();
72
73 for line in reader.lines().map_while(Result::ok) {
74 let parts: Vec<&str> = line.split_whitespace().collect();
75 if parts.len() != 4 {
76 continue;
77 }
78 let timestamp: i64 = match parts[0].parse() {
79 Ok(t) => t,
80 Err(_) => continue,
81 };
82 if parts[1] != RENAME_OP {
83 continue;
84 }
85 if timestamp < after_timestamp {
86 continue;
87 }
88
89 let old_path = decode_path(parts[2]);
90 let new_path = decode_path(parts[3]);
91
92 if !old_path.starts_with(user_prefix) || !new_path.starts_with(user_prefix) {
93 continue;
94 }
95
96 let old_rel = old_path
97 .strip_prefix(user_prefix)
98 .unwrap_or(&old_path)
99 .to_string();
100 let new_rel = new_path
101 .strip_prefix(user_prefix)
102 .unwrap_or(&new_path)
103 .to_string();
104 result.insert(new_rel, old_rel);
105 }
106 result
107 }
108
109 fn append(&self, record: &str) -> std::io::Result<()> {
110 if let Some(parent) = self.log_path.parent() {
111 std::fs::create_dir_all(parent)?;
112 }
113 let mut file = OpenOptions::new()
114 .create(true)
115 .append(true)
116 .open(&self.log_path)?;
117 writeln!(file, "{}", record)?;
118 file.sync_all()?;
119 Ok(())
120 }
121}
122
123fn decode_path(encoded: &str) -> String {
124 percent_encoding::percent_decode_str(encoded)
125 .decode_utf8_lossy()
126 .to_string()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use tempfile::TempDir;
133
134 #[test]
135 fn test_log_and_read_renames() {
136 let dir = TempDir::new().unwrap();
137 let log = FsLog::new(dir.path().join("fslog"));
138 log.log_rename(1000, "/storage/1/a.md", "/storage/1/b.md");
139 log.log_rename(2000, "/storage/1/c.md", "/storage/1/d.md");
140
141 let renames = log.renames_since("/storage/1/", 1500);
142 assert_eq!(renames.len(), 1);
143 assert_eq!(renames.get("b.md"), None); }
145}