From fff0d1b0dddafb5867904b3023ea0343c273ee65 Mon Sep 17 00:00:00 2001 From: rmanach Date: Fri, 24 Oct 2025 10:08:44 +0200 Subject: [PATCH] add missing file mod + add lib.rs --- src/file.rs | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/main.rs | 4 +- 3 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 src/file.rs create mode 100644 src/lib.rs diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..1725270 --- /dev/null +++ b/src/file.rs @@ -0,0 +1,318 @@ +use chrono::{DateTime, Utc}; +use mime_guess::from_path; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use walkdir::WalkDir; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FileImgMimetype { + Jpeg, + Png, +} + +impl FileImgMimetype { + pub fn value(&self) -> &'static str { + match self { + FileImgMimetype::Jpeg => "image/jpeg", + FileImgMimetype::Png => "image/png", + } + } +} + +/// Categorized files by their size in megabytes. +/// +/// * TINY: [0,1[ Mb +/// * MEDIUM: [1,2[ Mb +/// * LARGE: [2,5[ Mb +/// * FAT: [5,inf[ Mb +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum FileSizeRange { + Tiny, + Medium, + Large, + Fat, +} + +impl FileSizeRange { + pub fn from_size(size: f64) -> Self { + if size < 1.0 { + return FileSizeRange::Tiny; + } + + if size < 2.0 { + return FileSizeRange::Medium; + } + + if size < 5.0 { + return FileSizeRange::Large; + } + + FileSizeRange::Fat + } + + pub fn value(&self) -> &'static str { + match self { + FileSizeRange::Tiny => "Tiny", + FileSizeRange::Medium => "Medium", + FileSizeRange::Fat => "Fat", + FileSizeRange::Large => "Large", + } + } +} + +/// Handle file main attributes. +/// +/// Example: +/// ```rs +/// file = File.from_directory("dir-path", "my-file-name.png") +/// ``` +#[derive(Debug, Clone, PartialEq)] +pub struct File { + pub directory: String, + pub name: String, + pub path: String, + pub mimetype: String, + pub size: f64, + pub modified: DateTime, +} + +impl File { + pub fn from_directory(directory: &str, name: &str) -> Result { + let path = Path::new(directory) + .join(name) + .to_string_lossy() + .into_owned(); + + let mimetype = from_path(&path) + .first() + .map_or("application/octet-stream".to_string(), |m| m.to_string()); + + let size = fs::metadata(&path)?.len() as f64 / 1_048_576.0; + let modified = fs::metadata(&path)?.modified()?.into(); + + Ok(File { + directory: directory.to_string(), + name: name.to_string(), + path, + mimetype, + size, + modified, + }) + } +} + +/// Group a bunch of `File`. That's all. +/// Only useful to provide number of file and the whole size in Mb quickly. +#[derive(Debug, Clone)] +pub struct FileGroup { + files: HashMap, + size: f64, + nb_files: usize, +} + +impl Default for FileGroup { + fn default() -> Self { + Self::new() + } +} + +impl FileGroup { + pub fn new() -> Self { + FileGroup { + files: HashMap::new(), + size: 0.0, + nb_files: 0, + } + } + + pub fn add(&mut self, file: File) { + if self.files.get(&file.path).is_none() { + self.files.insert(file.path.clone(), file.clone()); + self.nb_files += 1; + self.size += file.size; + } + } + + pub fn len(&self) -> usize { + self.nb_files + } + + pub fn join(&mut self, right: &FileGroup) { + for (filepath, file) in &right.files { + if self.files.get(filepath).is_none() { + self.files.insert(filepath.clone(), file.clone()); + self.size += file.size; + self.nb_files += 1; + } + } + } + + pub fn get_size(&self) -> f64 { + self.size + } + + pub fn format_size(size: f64) -> String { + if size < 1000.0 { + return format!("{:.2} Mb", size); + } + format!("{:.2} Gb", size / 1024.0) + } + + pub fn get_size_formatted(&self) -> String { + Self::format_size(self.size) + } + + pub fn get_files(&self) -> Vec { + self.files.values().cloned().collect() + } +} + +/// Represents a directory path grouping files by mimetype and size range. +/// +/// Example: +/// ```rs +/// directory = Directory.from_path("my-path") +/// fg = directory.get_file_group() # collect all files +/// +/// // collect all tiny files of the directory +/// fg_tiny = directory.get_file_group(None, FileSizeRange::TINY) +/// +/// // collect all JPEG files +/// fg_jpeg = directory.get_file_group(FileImgMimetype::JPEG, None) +/// ``` +#[derive(Debug, Clone)] +pub struct Directory { + pub path: String, + pub nb_files: usize, + pub details: HashMap>, +} + +impl Directory { + pub fn from_path(path: &str) -> Result> { + if !std::path::Path::new(path).is_dir() { + return Err(format!("Directory path: {} must be a directory", path).into()); + } + + let mut nb_files = 0; + let mut details: HashMap> = HashMap::new(); + + for entry in WalkDir::new(path).into_iter().filter_map(Result::ok) { + if entry.file_type().is_file() { + let file_path = entry.path().to_string_lossy().into_owned(); + + let file = match File::from_directory( + entry.path().parent().unwrap().to_string_lossy().as_ref(), + entry.file_name().to_string_lossy().as_ref(), + ) { + Ok(file) => file, + Err(e) => { + log::error!("error accessing {}, err: {}", file_path, e); + continue; + } + }; + + let mimetype = file.mimetype.clone(); + let size_range = FileSizeRange::from_size(file.size); + + details + .entry(mimetype.clone()) + .or_default() + .entry(size_range) + .or_default() + .add(file); + + nb_files += 1; + } + } + + Ok(Directory { + path: path.to_string(), + nb_files, + details, + }) + } + + pub fn len(&self) -> usize { + self.nb_files + } + + pub fn show(&self) { + let mut data = vec![format!("directory ({}) details:", self.path)]; + + for (mimetype, group) in &self.details { + let mut nb_files = 0; + let mut size = 0.0; + let mut to_display = vec![format!("* {}", mimetype)]; + + for file_range in group.keys() { + let file_group = &group[file_range]; + to_display.push(format!( + "\t{:<8}{:<8}{}", + file_range.value(), + file_group.len(), + file_group.get_size_formatted() + )); + nb_files += file_group.len(); + size += file_group.get_size(); + } + + to_display[0] = format!( + "* {} ({} files, {})", + mimetype, + nb_files, + FileGroup::format_size(size) + ); + data.push(to_display.join("\n")); + } + + println!("{}", data.join("\n")); + } + + pub fn get_file_group( + &self, + mimetype: Option<&FileImgMimetype>, + size_range: Option<&FileSizeRange>, + ) -> FileGroup { + let mut file_group = FileGroup::new(); + + match (mimetype, size_range) { + (None, None) => { + return self.get_all(); + } + (Some(mime), None) => { + if let Some(dict_file_range) = self.details.get(mime.value()) { + for fg in dict_file_range.values() { + file_group.join(fg); + } + } + } + (None, Some(size_range)) => { + for dict_file_range in self.details.values() { + if let Some(fg) = dict_file_range.get(size_range) { + file_group.join(fg); + } + } + } + (Some(mime), Some(size_range)) => { + if let Some(dict_file_range) = self.details.get(mime.value()) + && let Some(fg) = dict_file_range.get(size_range) + { + file_group.join(fg); + } + } + } + + file_group + } + + pub fn get_all(&self) -> FileGroup { + let mut file_group = FileGroup::new(); + for details in self.details.values() { + for fg in details.values() { + file_group.join(fg); + } + } + file_group + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..29aa5b4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +mod file; + +pub use file::Directory; diff --git a/src/main.rs b/src/main.rs index 80aacee..fbfdb40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ use clap::Parser; use env_logger::Env; -mod file; - -use file::Directory; +use rs_optimg::Directory; #[derive(Parser)] struct Args {