Last active
December 30, 2023 13:30
-
-
Save se1983/e9e1520cbd1da770a27659665c34aa64 to your computer and use it in GitHub Desktop.
script in rust to rearrange all files in one directory into a folder structure /YYYY/MM/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use chrono::{Datelike, NaiveDate, NaiveDateTime}; | |
use std::error::Error; | |
use std::fs; | |
use std::path::{Path, PathBuf}; | |
use std::string::ToString; | |
use chrono::prelude::{DateTime, Utc}; | |
use console::{style, Emoji}; | |
use regex::Regex; | |
use walkdir::WalkDir; | |
use indicatif::{ProgressBar, ProgressStyle}; | |
use exif::Tag; | |
static LOOKING_GLASS: Emoji<'_, '_> = Emoji("🔍 ", ""); | |
static TRUCK: Emoji<'_, '_> = Emoji("🚚 ", ""); | |
/** Sort my Pictures into Date Directories | |
This traverses a directory SOURCE recursive and sorts all files under DESTINATION in a folder structure like: | |
/YYYY/MM/ | |
Files are copied. | |
Creation dates are extracted by | |
- filename | |
- OR folder structure (/YYYY//MM/) | |
- OR exif data or | |
- OS creation date | |
If no creation date could be found it is sorted into /unknown/ | |
*/ | |
static SOURCE: &str = "/home/sebastian/Nextcloud/Bilder"; | |
static DESTINATION: &str = "/home/sebastian/bilder_neu/"; | |
struct MetadataFile { | |
path: PathBuf, | |
content: String, | |
} | |
struct FileProcessing { | |
from: PathBuf, | |
to: PathBuf, | |
metadata: Option<MetadataFile>, | |
} | |
fn get_creation_date(file_path: &Path) -> Result<Option<NaiveDate>, Box<dyn Error>> { | |
let datestring_rgx = Regex::new("[0-9]{8}_[0-9]{6}")?; | |
let folderdate_rgx = Regex::new("/[1-2][0-9]{3}/[0-1][0-9]/")?; // /2020/11/ | |
if let Some(datestring) = datestring_rgx.find(file_path.file_stem().unwrap().to_str().unwrap()) | |
{ | |
let date = NaiveDateTime::parse_from_str(datestring.as_str(), "%Y%m%d_%H%M%S")?.date(); | |
return Ok(Some(date)); | |
} else if let Some(folderdate) = folderdate_rgx.find(file_path.as_os_str().to_str().unwrap()) { | |
let folderdate = format!("{}01", folderdate.as_str()); | |
let date = NaiveDate::parse_from_str(&folderdate, "/%Y/%m/%d")?; | |
return Ok(Some(date)); | |
} else { | |
let file = std::fs::File::open(file_path)?; | |
let mut bufreader = std::io::BufReader::new(&file); | |
let exifreader = exif::Reader::new(); | |
match exifreader.read_from_container(&mut bufreader) { | |
Ok(exifdata) => { | |
if let Some(f) = exifdata.fields().find(|f| f.tag == Tag::DateTime) { | |
let date = NaiveDateTime::parse_from_str( | |
&f.display_value().to_string(), | |
"%Y-%m-%d %H:%M:%S", | |
)? | |
.date(); | |
return Ok(Some(date)); | |
} | |
} | |
Err(err) => { | |
let metadata = fs::metadata(file_path)?; | |
log::error!("{err}"); | |
let dt: DateTime<Utc> = metadata.created()?.into(); | |
return Ok(Some(dt.date_naive())); | |
} | |
} | |
} | |
Ok(None) | |
} | |
fn collect_files_and_destinations() -> Result<Vec<FileProcessing>, Box<dyn Error>> { | |
let mut file_processings: Vec<FileProcessing> = vec![]; | |
let _spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}") | |
.unwrap() | |
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "); | |
println!( | |
"{} {}Resolving ...", | |
style("[1/2]").bold().dim(), | |
LOOKING_GLASS | |
); | |
let pb = ProgressBar::new(1_000_000); | |
pb.set_style( | |
ProgressStyle::with_template("{spinner:.blue} {msg}") | |
.unwrap() | |
.tick_strings(&[ | |
"▹▹▹▹▹", | |
"▸▹▹▹▹", | |
"▹▸▹▹▹", | |
"▹▹▸▹▹", | |
"▹▹▹▸▹", | |
"▹▹▹▹▸", | |
"▪▪▪▪▪", | |
]), | |
); | |
for entry in WalkDir::new(PathBuf::from(&SOURCE)) | |
.into_iter() | |
.filter_map(Result::ok) | |
.filter(|e| e.file_type().is_file()) | |
{ | |
let path = entry.path(); | |
let filename = path.file_name().unwrap(); | |
pb.inc(1); | |
if let Ok(Some(creation_date)) = get_creation_date(path) { | |
let mut newfile = PathBuf::from(&DESTINATION); | |
newfile.push(creation_date.year().to_string()); | |
newfile.push(format!("{:0>2}", creation_date.month())); | |
fs::create_dir_all(&newfile)?; | |
newfile.push(filename); | |
file_processings.push(FileProcessing { | |
from: path.to_path_buf(), | |
to: newfile.to_path_buf(), | |
metadata: None, | |
}); | |
pb.set_message(format!("{path:?}: {}", creation_date)); | |
} else { | |
let dirpath = PathBuf::from(format!("{DESTINATION}unknown")); | |
let mut newfile = dirpath.clone(); | |
let mut metadatafile = dirpath.clone(); | |
newfile.push(filename); | |
metadatafile.push(format!( | |
"{}.source", | |
path.file_stem().unwrap().to_str().unwrap() | |
)); | |
file_processings.push(FileProcessing { | |
from: path.to_path_buf(), | |
to: newfile.to_path_buf(), | |
metadata: Some(MetadataFile { | |
path: metadatafile.to_path_buf(), | |
content: format!("{path:?}"), | |
}), | |
}); | |
pb.set_message(format!("{path:?}: {}", "unknown")); | |
} | |
} | |
pb.finish_and_clear(); | |
Ok(file_processings) | |
} | |
fn main() -> Result<(), Box<dyn Error>> { | |
let file_processings = collect_files_and_destinations()?; | |
let pb = ProgressBar::new(file_processings.len() as u64); | |
pb.set_style( | |
ProgressStyle::with_template( | |
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] ({eta})", | |
)? | |
.progress_chars("#>-"), | |
); | |
println!("{} {}Copying ...", style("[2/2]").bold().dim(), TRUCK); | |
for f in file_processings { | |
fs::create_dir_all(f.to.parent().unwrap())?; | |
fs::copy(&f.from, &f.to)?; | |
if let Some(metadata) = f.metadata { | |
fs::write(metadata.path, metadata.content)? | |
} | |
pb.inc(1); | |
pb.set_message(format!("{:?} --> {:?}", f.from, f.to)) | |
} | |
pb.finish_and_clear(); | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment