use crate::models::FileInfo;
use crate::models::SimpleFileKind;
use eyre::Context;
use eyre::Report;
use eyre::Result;
use std::collections::HashSet;
use std::fs::{self, DirEntry, Metadata};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
use std::path::Path;
#[derive(Debug, Default)]
pub struct DirListing {
pub entries: Vec<FileInfo>,
pub errors: Vec<Report>,
}
pub trait FileSystem {
fn current_directory(&self) -> Result<FileInfo> {
self.directory_from_current::<&str>(None)
}
fn directory_from_current<P: AsRef<Path>>(&self, path: Option<P>) -> Result<FileInfo>;
fn list_files(&self, file: &FileInfo) -> Result<DirListing>;
fn file_size(&self, file: &FileInfo) -> Result<u64>;
fn device_id(&self, _file: &FileInfo) -> Option<u64> {
None
}
}
pub trait FileSystemClean {
fn remove_file(&self, file: &FileInfo) -> Result<()>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct RealFileSystem;
impl FileSystem for RealFileSystem {
fn directory_from_current<P: AsRef<Path>>(&self, path: Option<P>) -> Result<FileInfo> {
let mut path_buf = std::env::current_dir()?;
if let Some(p) = path {
path_buf.push(p);
}
Ok(FileInfo::new(
path_buf,
"".into(),
SimpleFileKind::Directory,
))
}
fn list_files(&self, file: &FileInfo) -> Result<DirListing> {
let read_dir = fs::read_dir(&file.path)
.with_context(|| format!("cannot read directory {}", file.path.display()))?;
Ok(read_dir.fold(DirListing::default(), |mut listing, entry| {
match entry
.context("failed to read dir entry")
.and_then(|entry| describe_entry(&entry))
{
Ok(info) => listing.entries.push(info),
Err(report) => listing.errors.push(report),
}
listing
}))
}
fn file_size(&self, file: &FileInfo) -> Result<u64> {
RealFileSystem::reclaimable_size(&file.path)
}
#[cfg(unix)]
fn device_id(&self, file: &FileInfo) -> Option<u64> {
fs::symlink_metadata(&file.path).ok().map(|m| m.dev())
}
}
impl RealFileSystem {
pub fn reclaimable_size<P>(path: P) -> Result<u64>
where
P: AsRef<Path>,
{
let mut seen_inodes = HashSet::new();
let mut pending = vec![path.as_ref().to_path_buf()];
let mut total = 0;
while let Some(current) = pending.pop() {
let metadata = fs::symlink_metadata(¤t)
.with_context(|| format!("cannot stat {}", current.display()))?;
if metadata.is_dir() {
let read_dir = fs::read_dir(¤t)
.with_context(|| format!("cannot read directory {}", current.display()))?;
for entry in read_dir {
pending.push(entry?.path());
}
}
if counts_towards_total(&metadata, &mut seen_inodes) {
total += allocated_bytes(&metadata);
}
}
Ok(total)
}
}
fn describe_entry(entry: &DirEntry) -> Result<FileInfo> {
let file_type = entry
.file_type()
.with_context(|| format!("cannot determine type of {}", entry.path().display()))?;
let kind = if file_type.is_symlink() {
SimpleFileKind::Symlink
} else if file_type.is_dir() {
SimpleFileKind::Directory
} else {
SimpleFileKind::File
};
Ok(FileInfo::new(
entry.path(),
entry.file_name().to_string_lossy().into_owned(),
kind,
))
}
#[cfg(unix)]
fn allocated_bytes(metadata: &Metadata) -> u64 {
metadata.blocks() * 512
}
#[cfg(not(unix))]
fn allocated_bytes(metadata: &Metadata) -> u64 {
metadata.len()
}
#[cfg(unix)]
fn counts_towards_total(metadata: &Metadata, seen_inodes: &mut HashSet<(u64, u64)>) -> bool {
metadata.nlink() <= 1 || seen_inodes.insert((metadata.dev(), metadata.ino()))
}
#[cfg(not(unix))]
fn counts_towards_total(_metadata: &Metadata, _seen_inodes: &mut HashSet<(u64, u64)>) -> bool {
true
}
impl FileSystemClean for RealFileSystem {
fn remove_file(&self, file: &FileInfo) -> Result<()> {
match file.kind {
SimpleFileKind::Directory => fs::remove_dir_all(&file.path)
.with_context(|| format!("cannot remove directory {}", file.path.display())),
SimpleFileKind::File => fs::remove_file(&file.path)
.with_context(|| format!("cannot remove {}", file.path.display())),
SimpleFileKind::Symlink => remove_symlink(&file.path)
.with_context(|| format!("cannot remove link {}", file.path.display())),
}
}
}
#[cfg(not(windows))]
fn remove_symlink(path: &Path) -> std::io::Result<()> {
fs::remove_file(path)
}
#[cfg(windows)]
fn remove_symlink(path: &Path) -> std::io::Result<()> {
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
if fs::symlink_metadata(path)?.file_attributes() & FILE_ATTRIBUTE_DIRECTORY == 0 {
fs::remove_file(path)
} else {
fs::remove_dir(path)
}
}
#[cfg(test)]
mod tests;