Skip to content

Instantly share code, notes, and snippets.

@se1983
Last active December 30, 2023 13:30
Show Gist options
  • Save se1983/e9e1520cbd1da770a27659665c34aa64 to your computer and use it in GitHub Desktop.
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/
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