1use crate::models::FileInfo;
2use crate::models::SimpleFileKind;
3use eyre::Context;
4use eyre::Report;
5use eyre::Result;
6use std::collections::HashSet;
7use std::fs::{self, DirEntry, Metadata};
8#[cfg(unix)]
9use std::os::unix::fs::MetadataExt;
10#[cfg(windows)]
11use std::os::windows::fs::MetadataExt;
12use std::path::Path;
13
14#[derive(Debug, Default)]
25pub struct DirListing {
26 pub entries: Vec<FileInfo>,
27 pub errors: Vec<Report>,
28}
29
30pub trait FileSystem {
31 fn current_directory(&self) -> Result<FileInfo> {
32 self.directory_from_current::<&str>(None)
33 }
34
35 fn directory_from_current<P: AsRef<Path>>(&self, path: Option<P>) -> Result<FileInfo>;
36
37 fn list_files(&self, file: &FileInfo) -> Result<DirListing>;
38
39 fn file_size(&self, file: &FileInfo) -> Result<u64>;
40
41 fn device_id(&self, _file: &FileInfo) -> Option<u64> {
46 None
47 }
48}
49
50pub trait FileSystemClean {
51 fn remove_file(&self, file: &FileInfo) -> Result<()>;
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
55pub struct RealFileSystem;
56
57impl FileSystem for RealFileSystem {
58 fn directory_from_current<P: AsRef<Path>>(&self, path: Option<P>) -> Result<FileInfo> {
59 let mut path_buf = std::env::current_dir()?;
60 if let Some(p) = path {
61 path_buf.push(p);
62 }
63
64 Ok(FileInfo::new(
65 path_buf,
66 "".into(),
67 SimpleFileKind::Directory,
68 ))
69 }
70
71 fn list_files(&self, file: &FileInfo) -> Result<DirListing> {
72 let read_dir = fs::read_dir(&file.path)
73 .with_context(|| format!("cannot read directory {}", file.path.display()))?;
74
75 Ok(read_dir.fold(DirListing::default(), |mut listing, entry| {
76 match entry
77 .context("failed to read dir entry")
78 .and_then(|entry| describe_entry(&entry))
79 {
80 Ok(info) => listing.entries.push(info),
81 Err(report) => listing.errors.push(report),
82 }
83 listing
84 }))
85 }
86
87 fn file_size(&self, file: &FileInfo) -> Result<u64> {
88 RealFileSystem::reclaimable_size(&file.path)
89 }
90
91 #[cfg(unix)]
92 fn device_id(&self, file: &FileInfo) -> Option<u64> {
93 fs::symlink_metadata(&file.path).ok().map(|m| m.dev())
94 }
95}
96
97impl RealFileSystem {
98 pub fn reclaimable_size<P>(path: P) -> Result<u64>
109 where
110 P: AsRef<Path>,
111 {
112 let mut seen_inodes = HashSet::new();
113 let mut pending = vec![path.as_ref().to_path_buf()];
114 let mut total = 0;
115
116 while let Some(current) = pending.pop() {
117 let metadata = fs::symlink_metadata(¤t)
118 .with_context(|| format!("cannot stat {}", current.display()))?;
119
120 if metadata.is_dir() {
121 let read_dir = fs::read_dir(¤t)
122 .with_context(|| format!("cannot read directory {}", current.display()))?;
123 for entry in read_dir {
124 pending.push(entry?.path());
125 }
126 }
127
128 if counts_towards_total(&metadata, &mut seen_inodes) {
129 total += allocated_bytes(&metadata);
130 }
131 }
132
133 Ok(total)
134 }
135}
136
137fn describe_entry(entry: &DirEntry) -> Result<FileInfo> {
143 let file_type = entry
144 .file_type()
145 .with_context(|| format!("cannot determine type of {}", entry.path().display()))?;
146
147 let kind = if file_type.is_symlink() {
148 SimpleFileKind::Symlink
149 } else if file_type.is_dir() {
150 SimpleFileKind::Directory
151 } else {
152 SimpleFileKind::File
153 };
154
155 Ok(FileInfo::new(
156 entry.path(),
157 entry.file_name().to_string_lossy().into_owned(),
158 kind,
159 ))
160}
161
162#[cfg(unix)]
163fn allocated_bytes(metadata: &Metadata) -> u64 {
164 metadata.blocks() * 512
165}
166
167#[cfg(not(unix))]
168fn allocated_bytes(metadata: &Metadata) -> u64 {
169 metadata.len()
170}
171
172#[cfg(unix)]
174fn counts_towards_total(metadata: &Metadata, seen_inodes: &mut HashSet<(u64, u64)>) -> bool {
175 metadata.nlink() <= 1 || seen_inodes.insert((metadata.dev(), metadata.ino()))
176}
177
178#[cfg(not(unix))]
179fn counts_towards_total(_metadata: &Metadata, _seen_inodes: &mut HashSet<(u64, u64)>) -> bool {
180 true
181}
182
183impl FileSystemClean for RealFileSystem {
184 fn remove_file(&self, file: &FileInfo) -> Result<()> {
185 match file.kind {
186 SimpleFileKind::Directory => fs::remove_dir_all(&file.path)
187 .with_context(|| format!("cannot remove directory {}", file.path.display())),
188 SimpleFileKind::File => fs::remove_file(&file.path)
189 .with_context(|| format!("cannot remove {}", file.path.display())),
190 SimpleFileKind::Symlink => remove_symlink(&file.path)
191 .with_context(|| format!("cannot remove link {}", file.path.display())),
192 }
193 }
194}
195
196#[cfg(not(windows))]
198fn remove_symlink(path: &Path) -> std::io::Result<()> {
199 fs::remove_file(path)
200}
201
202#[cfg(windows)]
209fn remove_symlink(path: &Path) -> std::io::Result<()> {
210 const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
211
212 if fs::symlink_metadata(path)?.file_attributes() & FILE_ATTRIBUTE_DIRECTORY == 0 {
213 fs::remove_file(path)
214 } else {
215 fs::remove_dir(path)
216 }
217}
218
219#[cfg(test)]
220mod tests;