truffle_core/crdt_doc/
backend.rs1use std::path::PathBuf;
4
5pub trait CrdtBackend: Send + Sync + 'static {
10 fn load_snapshot(&self, doc_id: &str) -> Option<Vec<u8>>;
12
13 fn load_updates(&self, doc_id: &str) -> Vec<Vec<u8>>;
15
16 fn save_update(&self, doc_id: &str, data: &[u8]);
18
19 fn save_snapshot(&self, doc_id: &str, data: &[u8]);
21
22 fn remove(&self, doc_id: &str);
24}
25
26#[derive(Debug, Default)]
35pub struct MemoryCrdtBackend;
36
37impl CrdtBackend for MemoryCrdtBackend {
38 fn load_snapshot(&self, _doc_id: &str) -> Option<Vec<u8>> {
39 None
40 }
41
42 fn load_updates(&self, _doc_id: &str) -> Vec<Vec<u8>> {
43 Vec::new()
44 }
45
46 fn save_update(&self, _doc_id: &str, _data: &[u8]) {}
47
48 fn save_snapshot(&self, _doc_id: &str, _data: &[u8]) {}
49
50 fn remove(&self, _doc_id: &str) {}
51}
52
53pub struct CrdtFileBackend {
71 base_dir: PathBuf,
72}
73
74impl CrdtFileBackend {
75 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
77 Self {
78 base_dir: base_dir.into(),
79 }
80 }
81
82 fn doc_dir(&self, doc_id: &str) -> PathBuf {
84 self.base_dir.join(sanitize(doc_id))
85 }
86
87 fn snapshot_path(&self, doc_id: &str) -> PathBuf {
89 self.doc_dir(doc_id).join("snapshot.bin")
90 }
91
92 fn updates_dir(&self, doc_id: &str) -> PathBuf {
94 self.doc_dir(doc_id).join("updates")
95 }
96}
97
98fn sanitize(s: &str) -> String {
100 let replaced = s.replace('/', "_").replace('\\', "_").replace("..", "__");
101 if replaced.is_empty() || replaced == "." {
102 "_".to_string()
103 } else {
104 replaced
105 }
106}
107
108impl CrdtBackend for CrdtFileBackend {
109 fn load_snapshot(&self, doc_id: &str) -> Option<Vec<u8>> {
110 let path = self.snapshot_path(doc_id);
111 std::fs::read(&path).ok()
112 }
113
114 fn load_updates(&self, doc_id: &str) -> Vec<Vec<u8>> {
115 let dir = self.updates_dir(doc_id);
116 let mut entries: Vec<_> = match std::fs::read_dir(&dir) {
117 Ok(rd) => rd
118 .filter_map(|e| e.ok())
119 .filter(|e| {
120 e.path()
121 .extension()
122 .map(|ext| ext == "bin")
123 .unwrap_or(false)
124 })
125 .collect(),
126 Err(_) => return Vec::new(),
127 };
128
129 entries.sort_by_key(|e| e.file_name());
131
132 entries
133 .into_iter()
134 .filter_map(|e| std::fs::read(e.path()).ok())
135 .collect()
136 }
137
138 fn save_update(&self, doc_id: &str, data: &[u8]) {
139 let dir = self.updates_dir(doc_id);
140 if let Err(e) = std::fs::create_dir_all(&dir) {
141 tracing::error!(
142 path = %dir.display(),
143 "crdt_file_backend: failed to create updates directory: {e}"
144 );
145 return;
146 }
147
148 let next_seq = match std::fs::read_dir(&dir) {
150 Ok(rd) => {
151 rd.filter_map(|e| e.ok())
152 .filter_map(|e| {
153 let name = e.file_name().to_string_lossy().into_owned();
154 if name.ends_with(".bin") {
155 name.trim_end_matches(".bin").parse::<usize>().ok()
156 } else {
157 None
158 }
159 })
160 .max()
161 .unwrap_or(0)
162 + 1
163 }
164 Err(_) => 1,
165 };
166
167 let path = dir.join(format!("{next_seq:06}.bin"));
168 if let Err(e) = std::fs::write(&path, data) {
169 tracing::error!(
170 path = %path.display(),
171 "crdt_file_backend: failed to write update: {e}"
172 );
173 }
174 }
175
176 fn save_snapshot(&self, doc_id: &str, data: &[u8]) {
177 let doc_dir = self.doc_dir(doc_id);
178 if let Err(e) = std::fs::create_dir_all(&doc_dir) {
179 tracing::error!(
180 path = %doc_dir.display(),
181 "crdt_file_backend: failed to create doc directory: {e}"
182 );
183 return;
184 }
185
186 let path = self.snapshot_path(doc_id);
187 let tmp_path = path.with_extension("bin.tmp");
188
189 if let Err(e) = std::fs::write(&tmp_path, data) {
191 tracing::error!(
192 path = %tmp_path.display(),
193 "crdt_file_backend: failed to write snapshot tmp: {e}"
194 );
195 return;
196 }
197 if let Err(e) = std::fs::rename(&tmp_path, &path) {
198 tracing::error!(
199 from = %tmp_path.display(),
200 to = %path.display(),
201 "crdt_file_backend: failed to rename snapshot tmp: {e}"
202 );
203 return;
204 }
205
206 let updates_dir = self.updates_dir(doc_id);
208 if updates_dir.exists() {
209 let _ = std::fs::remove_dir_all(&updates_dir);
210 }
211 }
212
213 fn remove(&self, doc_id: &str) {
214 let doc_dir = self.doc_dir(doc_id);
215 let _ = std::fs::remove_dir_all(&doc_dir);
216 }
217}