1use std::{
6 fmt, fs, io,
7 path::{Path, PathBuf},
8 sync::Arc,
9};
10
11use crate::{FsEntry, types::DiskUsage};
12
13pub trait FileSystem: fmt::Debug + Send + Sync {
18 fn read_dir(&self, path: &Path) -> io::Result<Vec<FsEntry>>;
20 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
22 fn create_file(&self, path: &Path) -> io::Result<()>;
24 fn rename(&self, source: &Path, destination: &Path) -> io::Result<()>;
26 fn copy_file(&self, source: &Path, destination: &Path) -> io::Result<()>;
28 fn copy_dir(&self, source: &Path, destination: &Path) -> io::Result<()>;
30 fn remove_file(&self, path: &Path) -> io::Result<()>;
32 fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
34 fn exists(&self, path: &Path) -> bool;
36 fn is_dir(&self, path: &Path) -> bool;
38 fn disk_usage(&self, path: &Path) -> Option<DiskUsage>;
40 fn dir_size(&self, path: &Path) -> (u64, bool);
43
44 fn item_count(&self, _path: &Path) -> Option<usize> {
51 None
52 }
53
54 fn restore_deleted(&self, _path: &Path) -> io::Result<()> {
60 Err(io::Error::new(
61 io::ErrorKind::Unsupported,
62 "restoring deleted items is not supported here",
63 ))
64 }
65
66 fn is_local(&self) -> bool {
70 false
71 }
72}
73
74pub type SharedFileSystem = Arc<dyn FileSystem>;
76
77#[derive(Debug, Clone, Copy, Default)]
79pub struct StdFileSystem;
80
81impl FileSystem for StdFileSystem {
82 fn read_dir(&self, path: &Path) -> io::Result<Vec<FsEntry>> {
83 let mut entries = Vec::new();
84 for entry in fs::read_dir(path)?.flatten() {
85 let path = entry.path();
86 let name = entry.file_name().to_string_lossy().into_owned();
87 let Ok(metadata) = entry.metadata() else {
88 continue;
89 };
90 let is_dir = metadata.is_dir();
91 let extension = if is_dir {
92 String::new()
93 } else {
94 path.extension()
95 .map(|value| value.to_string_lossy().to_lowercase())
96 .unwrap_or_default()
97 };
98 entries.push(FsEntry {
99 name,
100 path,
101 is_dir,
102 size: (!is_dir).then_some(metadata.len()),
103 item_count: None,
105 extension,
106 });
107 }
108 Ok(entries)
109 }
110
111 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
112 fs::create_dir_all(path)
113 }
114
115 fn create_file(&self, path: &Path) -> io::Result<()> {
116 if let Some(parent) = path.parent() {
117 fs::create_dir_all(parent)?;
118 }
119 fs::OpenOptions::new()
120 .write(true)
121 .create(true)
122 .truncate(false)
123 .open(path)
124 .map(|_| ())
125 }
126
127 fn rename(&self, source: &Path, destination: &Path) -> io::Result<()> {
128 fs::rename(source, destination)
129 }
130
131 fn copy_file(&self, source: &Path, destination: &Path) -> io::Result<()> {
132 fs::copy(source, destination).map(|_| ())
133 }
134
135 fn copy_dir(&self, source: &Path, destination: &Path) -> io::Result<()> {
136 crate::fs_util::copy_dir_all(source, destination)
137 }
138
139 fn remove_file(&self, path: &Path) -> io::Result<()> {
140 fs::remove_file(path)
141 }
142
143 fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
144 fs::remove_dir_all(path)
145 }
146
147 fn exists(&self, path: &Path) -> bool {
148 path.exists()
149 }
150
151 fn is_dir(&self, path: &Path) -> bool {
152 path.is_dir()
153 }
154
155 fn disk_usage(&self, path: &Path) -> Option<DiskUsage> {
156 crate::fs_util::disk_usage(path)
157 }
158
159 fn dir_size(&self, path: &Path) -> (u64, bool) {
160 crate::fs_util::dir_size(path)
161 }
162
163 fn item_count(&self, path: &Path) -> Option<usize> {
164 fs::read_dir(path).ok().map(|items| items.flatten().count())
165 }
166
167 fn is_local(&self) -> bool {
168 true
169 }
170}
171
172#[cfg(feature = "trash")]
180#[derive(Debug, Clone, Copy, Default)]
181pub struct TrashFileSystem;
182
183#[cfg(feature = "trash")]
184impl FileSystem for TrashFileSystem {
185 fn read_dir(&self, path: &Path) -> io::Result<Vec<FsEntry>> {
186 StdFileSystem.read_dir(path)
187 }
188 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
189 StdFileSystem.create_dir_all(path)
190 }
191 fn create_file(&self, path: &Path) -> io::Result<()> {
192 StdFileSystem.create_file(path)
193 }
194 fn rename(&self, source: &Path, destination: &Path) -> io::Result<()> {
195 StdFileSystem.rename(source, destination)
196 }
197 fn copy_file(&self, source: &Path, destination: &Path) -> io::Result<()> {
198 StdFileSystem.copy_file(source, destination)
199 }
200 fn copy_dir(&self, source: &Path, destination: &Path) -> io::Result<()> {
201 StdFileSystem.copy_dir(source, destination)
202 }
203 fn remove_file(&self, path: &Path) -> io::Result<()> {
204 trash::delete(path).map_err(io::Error::other)
205 }
206 fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
207 trash::delete(path).map_err(io::Error::other)
208 }
209 fn exists(&self, path: &Path) -> bool {
210 StdFileSystem.exists(path)
211 }
212 fn is_dir(&self, path: &Path) -> bool {
213 StdFileSystem.is_dir(path)
214 }
215 fn disk_usage(&self, path: &Path) -> Option<DiskUsage> {
216 StdFileSystem.disk_usage(path)
217 }
218 fn dir_size(&self, path: &Path) -> (u64, bool) {
219 StdFileSystem.dir_size(path)
220 }
221 fn item_count(&self, path: &Path) -> Option<usize> {
222 StdFileSystem.item_count(path)
223 }
224 fn is_local(&self) -> bool {
225 true
226 }
227
228 #[cfg(any(
229 target_os = "windows",
230 all(
231 unix,
232 not(target_os = "macos"),
233 not(target_os = "ios"),
234 not(target_os = "android")
235 )
236 ))]
237 fn restore_deleted(&self, path: &Path) -> io::Result<()> {
238 let item = trash::os_limited::list()
240 .map_err(io::Error::other)?
241 .into_iter()
242 .filter(|i| i.original_path() == path)
243 .max_by_key(|i| i.time_deleted)
244 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "not found in the trash"))?;
245 trash::os_limited::restore_all([item]).map_err(io::Error::other)
246 }
247}
248
249pub const TRASH_SUPPORTED: bool = cfg!(feature = "trash");
251
252#[must_use]
255pub fn local_filesystem(use_trash: bool) -> SharedFileSystem {
256 #[cfg(feature = "trash")]
257 if use_trash {
258 return Arc::new(TrashFileSystem);
259 }
260 let _ = use_trash;
261 Arc::new(StdFileSystem)
262}
263
264#[must_use]
266pub fn std_filesystem() -> SharedFileSystem {
267 Arc::new(StdFileSystem)
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct VirtualEntry {
273 pub name: String,
275 pub path: PathBuf,
277 pub is_dir: bool,
279 pub size: Option<u64>,
281 pub item_count: Option<usize>,
283 pub extension: String,
285}
286
287impl From<VirtualEntry> for FsEntry {
288 fn from(entry: VirtualEntry) -> Self {
289 Self {
290 name: entry.name,
291 path: entry.path,
292 is_dir: entry.is_dir,
293 size: entry.size,
294 item_count: entry.item_count,
295 extension: entry.extension,
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::FileExplorer;
304
305 #[derive(Debug)]
306 struct MemoryFileSystem;
307
308 impl FileSystem for MemoryFileSystem {
309 fn read_dir(&self, path: &Path) -> io::Result<Vec<FsEntry>> {
310 Ok(vec![
311 VirtualEntry {
312 name: "virtual.txt".into(),
313 path: path.join("virtual.txt"),
314 is_dir: false,
315 size: Some(7),
316 item_count: None,
317 extension: "txt".into(),
318 }
319 .into(),
320 ])
321 }
322
323 fn create_dir_all(&self, _: &Path) -> io::Result<()> {
324 Ok(())
325 }
326 fn create_file(&self, _: &Path) -> io::Result<()> {
327 Ok(())
328 }
329 fn rename(&self, _: &Path, _: &Path) -> io::Result<()> {
330 Ok(())
331 }
332 fn copy_file(&self, _: &Path, _: &Path) -> io::Result<()> {
333 Ok(())
334 }
335 fn copy_dir(&self, _: &Path, _: &Path) -> io::Result<()> {
336 Ok(())
337 }
338 fn remove_file(&self, _: &Path) -> io::Result<()> {
339 Ok(())
340 }
341 fn remove_dir_all(&self, _: &Path) -> io::Result<()> {
342 Ok(())
343 }
344 fn exists(&self, _: &Path) -> bool {
345 true
346 }
347 fn is_dir(&self, _: &Path) -> bool {
348 false
349 }
350 fn disk_usage(&self, _: &Path) -> Option<DiskUsage> {
351 None
352 }
353 fn dir_size(&self, _: &Path) -> (u64, bool) {
354 (0, false)
355 }
356 }
357
358 #[test]
359 fn explorer_reads_from_custom_filesystem() {
360 let explorer = FileExplorer::builder(PathBuf::from("virtual:/"))
361 .filesystem(Arc::new(MemoryFileSystem))
362 .try_build()
363 .unwrap();
364 assert_eq!(explorer.entries.len(), 1);
365 assert_eq!(explorer.entries[0].name, "virtual.txt");
366 }
367}