Arrange photos by their creation dates

3 minute read

I have to confess that I do not like taking photo of my own or others. However, I do posses a big amount of photo in my computer. After every trip that I traveled, it had taken months before I managed to pull out all of photo taken from my IPhone, my wife’s, our Canon camera and labeled (re-arranged) them into corresponding folders. The folders name must ring a bell when we look back later. The goal is that those photo would be arranged programmatically into different directories, preferably starting with Year as root directories and a list of YYYY-MM-DD directories. That would make life easier.

Example:

2015
  2015-01-31
    Img1.jpeg
    Img2.jpeg
  2015-02-16
    Img1.jpeg
    Img2.jpeg
2016
  2016-01-31
    Img1.jpeg
    Img2.jpeg
  2016-02-16
    Img1.jpeg
    Img2.jpeg

These dates are computed from photo’s creation dates. Therefore, this approach heavily relies on the assumption that all device’s dates are correctly set.

The first I had to do is finding the creation date of a given file. That is achieved by utility stat.

stat -f %Sm -t %F Img1.jpeg

According to the manual of ‘stat’, option ‘m’ means the modified date of the file while ‘B’ means the birth date of the file. However, when I tested ‘B’ did not deliver what it was supposed to. I had to reply on an assumption that the file was untouched since it was copied from memory card ( of my camera).

The next challenge is looping through all jpeg files in the current directory and possibly looking into subdirectories. It was achieved by combination of ‘for’ loop and ‘find’.

for thisfile in $(find . -maxdepth 1 -name "$1" \! \( -name "*._* \) -type f | xargs -0 ); do
    echo "Moving..." $thisfile;
    moving $thisfile;
    done

However, as expected, when filenames containing space, the result of find will return broken filenames. For example, if there are 3 files a.jpeg, b.jpeg, and c d.jpeg. The find utility will return the following strings ./a.jpeg, ./b.jpeg, ./c , and lastly d.jpeg. The reason is that the special variable $IFS determines how string should be treated. By default, $IFS is or something like that. Other utilities that deal with strings containing sensitive characters ‘\0’, ‘ ‘, ‘b’ (break), etc, will reply on the value of $IFS to manipulate the string. Therefore, I needed to make sure $IFS not containing something I cannot digest. In this case, it is the space. The strategy is switching on/off the value of $IFS.

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
--> do some good thing with names
IFS=$SAVEIFS

Altogether, here is the final script.

#!/bin/bash
#Author: Thanh Truong, Sweden, July 2016
#Reference: https://gist.github.com/pietrop/880a58088c630c960166
rootdir="/Users/thanhtruong/repos/projects/github/temp"

function moving() {
    img="$1"
    # B means birth of the file
    #creation_date="$(stat -f %SB -t %F $img)"
    # m means modified time of the file
    creation_date="$(stat -f %Sm -t %F $img)"

    imgYearDir=${creation_date:0:4}

    pushd $rootdir
    if [ ! -d "$imgYearDir" ]; then
        mkdir "$imgYearDir"
    fi

    pushd $imgYearDir
    if [ ! -d "$creation_date" ]; then
        mkdir "$creation_date"
    fi
    popd # from imgYearDir
    popd # from rootdir

    imgNameOnly=${img##*/}
    #echo "ImageNameOnly $imgNameOnly"
    $(mv $img $rootdir/$imgYearDir/$creation_date/$imgNameOnly)

    echo "...to $rootdir/$imgYearDir/$creation_date/$imgNameOnly"
 }

function mover()  {
    #The value of IFS are used as token delimiters or separator for each line.
    #By default, IFS is space so processing filenames containing spaces will not correct
    # because the any utility will break a line with spaces into several lines
    SAVEIFS=$IFS
    IFS=$(echo -en "\n\b")

    # Note that, here -print0 is not used. Otherwise, it treats '\0' as 'space'
    for thisfile in $(find . -maxdepth 1 -name "$1" \! \( -name "*._*" \) -type f | xargs -0 ); do
    echo "Moving..." $thisfile;
    moving $thisfile;
    done
    #Restore IFS
    IFS=$SAVEIFS
}

abort()
{
    echo >&2 '
***************
*** ABORTED ***
***************
'
    echo "An error occurred. Exiting..." >&2
    exit 1
}

#----------------------------------------
trap 'abort' 0
set -e
## ===> Guarded script goes here
mover "$1"
# Done!
trap : 0
#----------------------------------------

References