I’ve been trying not to use Google Products. Two of the products are Google Drive and Google Photos. Admittedly, I regret trying to not use Google Photos in particular. Google Photos is just an awesome product! Sorting, applied photo machine learning, photo history, and numerous other features of Google Photos are fantastic. However to complete my exercise of trying to not use Google products, I had to try something to replace Google Photos.

I came across Resilio Sync which seemed like a great candidate for the internal workings of something similar to cloud storage. Resilio Sync backs up file data over an encrypted P2P network to a personal computer. Great! However, there would be a lot of manual work sorting through pictures. So I asked, “How can I sort through my photos automatically?” I quickly answered, “I’m a programmer, I can make something!”

I started programming something in NodeJS. Then I thought I could make a photo electron application. As I was programming in NodeJS, I wanted to duplicate the terminal tool in Rust, because it makes sense to have a photo sorter with a language more low level. Plus, I could use Rust a bit more. I’ll show the code for the Rust project over the NodeJS project, because I find that more interesting.

The main() of the program parses the command line arguments and passes them along to the photo_library module with the make_photo_library() function. The main() function also determines if the number of parameters are right and if the directory passed is indeed a directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn main() {
    let args: Vec<String> = env::args().collect();
    let photos_dir_str: &str = &args[1];

    if args.len() != 2 {
        println!("Did not input the right amount of arguments!  Just two please.");
        process::exit(1);
    }

    let photos_dir_path = Path::new(photos_dir_str);

    if photos_dir_path.is_dir() {
        make_photo_library(photos_dir_str);
    }
}

If I were to do additional programming with the main() function, I would consider a more utilitarian approach to receiving command line arguments. In fact, I might even do that really soon, because it seems like a good idea.

I had no idea how to pull out the metadata of a photo. Then image Exif came into play! Exif is just a image format that can show the metadata of a photo! Great, I can use that. Knowing that, I used the glob module to gather all the JPEG files in the given directory and subdirectories. Then I looped through the returned data, and that loop would handle each file centrally.

Here is the module file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub fn make_photo_library(photos_dir_str: &str) {
    let white_list_file_types: Vec<&str> = vec!["jpeg", "jpg", "JPEG", "JPG"];

    for file_type in &white_list_file_types {
        let glob_path = photos_dir_str.to_owned() + "/*." + file_type.to_owned();

        for entry in glob(&glob_path).expect("Failed to read glob pattern") {
            match entry {
                Ok(path) => {
                    let image_path: Display = path.display();
                    let image_path_str: &str = &image_path.to_string();

                    let date_data: String = read_exif_date_data(image_path_str);
                    let made_dir: String = make_dir(&date_data);
                    
                    match path.file_name() {
                        Some(data) => move_image(data.to_str(), made_dir, image_path_str),
                        _ => (),
                    }
                }
                Err(e) => println!("{:?}", e),
            }
        }
    }
}

This function I actually did remake a couple times, and I finally settled on this function handle all the metadata, folder creation, and image renaming. It just made more sense to have the core logic be more centralized.

There are three further functions with in that glob loop. The first of those functions is called read_exif_date_data. Writing this function was fairly straight forward. Read file data through a file buffer. Then that buffer would be piped to the exif buffer reader. After the exif buffer reader, then I grab the appropriate property in a match statement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn read_exif_date_data(image_path_str: &str) -> String {
    let path = Path::new(image_path_str);

    let file = File::open(path).unwrap();
    let reader = exif::Reader::new(&mut std::io::BufReader::new(&file)).unwrap();

    let date_data: String = match reader.get_field(exif::Tag::DateTime, false) {
        Some(data) => data.value.display_as(data.tag).to_string(),
        None => String::from("false"),
    };

    return date_data;
}

That match statment was a little tricky; I made sure I was using the right enum and display the returned data as a string. Otherwise it was completely easy!

The second function is called make_dir. This function just takes a date as a string, and it gets split up so that each portion of the date becomes a folder. The folders are then created in a mkdir -p fashion. Simple enough.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
fn make_dir(date_time: &str) -> String {
    let mut split_date_time_spaces = date_time.split_whitespace();

    match split_date_time_spaces.next() {
        Some(e) => {
            let replace_date_hyphens = str::replace(e, "-", "/");
            let dir_to_create = "./photos/".to_owned() + &replace_date_hyphens;

            match create_dir_all(&dir_to_create) {
                Ok(_e) => (),
                Err(_) => println!("{:?} could not be created!", &dir_to_create),
            }

            return dir_to_create;
        }
        None => println!("{:?}", "No dates exist."),
    };

    return String::from("Somehow no directories were made!");
}

The third and last function is called move_image. Hopefully, the file data was taken in and processed, and the new folder location would have been created fine. Really, it was all fine, or else I would have known because of the error handling that I had in the previous functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn move_image(file_name: Option<&str>, made_dir: String, image_path_str: &str) {
    match file_name {
        Some(file_name) => {
            let new_file_name = made_dir + "/" + file_name;

            println!("{:?}", new_file_name);
            println!("{:?}", image_path_str);

            match rename(image_path_str, new_file_name) {
                Ok(_e) => println!("File relocated!"),
                Err(_) => println!("File not relocated"),
            };
        },
        _ => ()
    }
}

Programming the Rust duplicate of this terminal tool was fun. I’ll grab any opportunity to code in something else other than JS, so that I can keep my skills up. Learning Rust could be very important for web technology I think! Looking at you, WebAssembly. Plus, I can learn something new like Exif data. Learning is hopefully always welcomed!