Sort iCloud images using Rust

Author: Kalin

2024-05-20 20:43:00 +0200 CEST

I have a naming convention for my images. It goes like that:

images\20240223
images\20240224
images\20240225

So, I went into my Photos app and exported unmofied versions of all my photos to a directory called "images". That's great but now I have one directory with images inside. They are not sorted in subdirectories the way I like them to be. They look like that:

IMG_3734.HEIC
IMG_3735.HEIC
IMG_3736.HEIC
IMG_3737.HEIC
IMG_3738.HEIC
IMG_3739.HEIC
IMG_3740.HEIC
IMG_3741.HEIC
IMG_3742.HEIC
IMG_3743.HEIC
IMG_3744.HEIC
IMG_3745.HEIC
IMG_3746.HEIC
IMG_3748.HEIC
IMG_3749.HEIC
IMG_3750.HEIC

So, let's write a simple Rust application to move all files from "images" to multiple directories under "sorted_images".

We'll need just a bit of dependencies, here's the Cargo.toml file:

[package]
name = "image_sorter"
version = "0.1.0"
edition = "2021"

[dependencies]
chrono = "0.4"
glob = "0.3"
filetime = "0.2"

And here's the Rust application itself:

use chrono::{NaiveDateTime, DateTime, Utc}; // Import necessary types
use filetime::FileTime;
use glob::glob;
use std::fs;
use std::io;
use std::path::Path;

fn main() -> io::Result<()> {
    let source_dir = "./images"; // Source directory containing images
    let destination_base = "./sorted_images"; // Base directory for sorted images

    // Ensure the destination base directory exists
    fs::create_dir_all(destination_base)?;

    // Iterate over all image files in the source directory
    for entry in glob(&format!("{}/*.*", source_dir)).expect("Failed to read glob pattern") {
        match entry {
            Ok(path) => {
                if let Ok(metadata) = fs::metadata(&path) {
                    if let Some(created_time) = FileTime::from_creation_time(&metadata) {
                        let created_naive = NaiveDateTime::from_timestamp(created_time.unix_seconds(), created_time.nanoseconds() as u32);
                        let datetime: DateTime<Utc> = DateTime::<Utc>::from_utc(created_naive, Utc);
                        let date = datetime.date();
                        let target_dir = format!("{}/{}", destination_base, date.format("%Y%m%d"));
                        fs::create_dir_all(&target_dir)?;

                        let filename = path.file_name().unwrap();
                        let destination = Path::new(&target_dir).join(filename);

                        fs::rename(&path, &destination)?;
                        println!("Moved {} to {}", path.display(), destination.display());
                    } else {
                        println!("Could not extract creation time from {}", path.display());
                    }
                } else {
                    println!("Could not get metadata for {}", path.display());
                }
            }
            Err(e) => println!("{:?}", e),
        }
    }
    Ok(())
}

Here's what's going on:

  1. The glob is taking all files under the source directory "images" and looping over them.
  2. We read the metadata of each file to find the created time.
  3. We move the file to a directory within the destination directory "sorted_images/< date >"

The date is created using:

date.format("%Y%m%d"))

which gives the YYYYMMDD time format. And that's all there is to it!

Now we have a nice sorted list of directories with photos. I usually (when I find time) go over them to review them and add a bit of context like:

20240101_new_year_party
20240624_beach

Can I automate that? Hmm.. a bit of AI to recognize what images are about would go a long way here... but maybe next time.