Organize Files Into Directories by Creation Date using a Node.js script

Every 6 months or so, I move photos taken on my phone to my Mac. The way I like to organize photos is by having them divided into subdirectories based on when the photo was taken.

For example, if I have a photo taken today (May 10th, 2020) the photo should end up in that directory structure:

.../photos/2020/05/2020_05_10/photo.jpg

I used to like Aperture app by Apple, as it has the "Import" feature which does exactly that. But Apple decided to drop support for it, hence the app became buggy and would crash often. On top of that, it takes a REALLY LONG time to process files, and many times it would end up putting 80% of my photos under a this directory:

.../photos/0000/00/0000_00_00/photo.jpg

Clearly that indicates that something went wrong 😖 - I tried to look for other alternatives like perhaps another app, I couldn't find what I was looking for.

 

I ended up writing a script that does exactly what I need, plus it's really fast! Aperture would take about 5-10min for 1,000 images, but my script takes about 1-2s for 5,000 images.

 

Using Node.js

I opted into using node and JavaScript as I have it setup on my machine, and it's pretty easy when it comes to using fs module. Here's the full code, which you can copy/paste into a new file and call it script.js:

let root = './photos'
const outputDir = './output'
const fs = require('fs')

console.log('\n------------------------------------')
console.log('🗂  Welcome to KB\'s File Organizer 🗂')
console.log('------------------------------------\n')

// pull the arguments (by skipping the first 2 being the path and the script name)
const args = process.argv.slice(2)
// args.forEach((val, index) => {
//   console.log(`${index}: ${val}`)
// })

// check if we have the proper argument for the source director
// and make sure it does exist.
if (args[0] && fs.existsSync(args[0])) {
  root = args[0]
}

console.log(`> PATH "${root}"   -->   "${outputDir}"`)

fs.readdir(root, (err, files) => {
  if (err) {
    console.log('Error getting directory information: ', JSON.stringify(err, null, 2))
    return
  }

  // remove all hidden files
  files = files.filter(item => !(/(^|\/)\.[^\/\.]/g).test(item))

  // mention how many files total
  const totalFiles = files.length
  console.log(`> TOTAL FILES (${totalFiles})\n`)

  // create the output directory
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir)
  }

  let i = 0
  // keep track of the files ones
  let errorFiles = []

  files.forEach(file => {
    // increase the current file count
    ++i

    const stats = fs.statSync(`${root}/${file}`)

    // if that's a directory let's skip it.
    if (!stats.isDirectory()) {
      const birthtime = stats.birthtime

      let month = pad(birthtime.getMonth() + 1)
      let day = pad(birthtime.getDate())
      let year = birthtime.getFullYear()

      // check if we don't have the year folder created, to create it
      const yearDir = `${outputDir}/${year}`
      if (!fs.existsSync(yearDir)) {
        fs.mkdirSync(yearDir)
      }
      // same with month
      const monthDir = `${yearDir}/${month}`
      if (!fs.existsSync(monthDir)) {
        fs.mkdirSync(monthDir)
      }
      // same with day
      const dayDir = `${monthDir}/${year}_${month}_${day}`
      if (!fs.existsSync(dayDir)) {
        fs.mkdirSync(dayDir)
      }

      // move the file
      const moveStatus = `@ ${year}/${month}/${day} ${parseInt((i / totalFiles) * 100)}%`
      try {
        fs.renameSync(`${root}/${file}`, `${dayDir}/${file}`)
        console.log('✔️ ', file, moveStatus);
      } catch (err) {
        console.log('❌ ', file, moveStatus, `ERROR: ${err}`);
        errorFiles.push(file)
      }
    }
  });

  console.log(`\n🎉 ALL DONE (${i}/${totalFiles}) 🎉\n`)
  if (errorFiles.length) {
    console.log(`⚠️  ERRORS (${errorFiles.length}/${totalFiles}) ⚠️`)
    errorFiles.forEach((val, index) => {
      console.log(val)
    })
    console.log('\n')
  }
});

function pad(num, size = 2) {
  var s = num + '';
  while (s.length < size) s = '0' + s;
  return s;
}

Script Explanation

The script is pretty well documented to explain what's going on in every section of it. The overview is as follows:

  • Parse the optional argument sent. If not there, use the default source/root directory.

  • Read all the files in that directory.

  • Then check if we have an error to display it.

  • Get rid of all hidden files like (.DS_Store) etc.

  • Show some logs for the number of files we'll be parsing.

  • Check if the output directory isn't created yet to create it.

  • Then for each file:

    • keep track of the number of files we've processed so far.

    • Get the metadata properties for that file using statSync.

    • Skip all directories.

    • Get the date that file was created.

    • Pad them so that January ends up as 01 and not 1.

    • Then using the file's date, deduce the directory structure: YYYY/MM/YYYY_MM_DD/file.jpg

    • Create the folders if they haven't been created yet.

    • Now move the file to the destination using the renameSync method.

    • Catch any errors on the way to display them at the end.

  • After processing all files, we're pretty much done.

Usage Example

node script.js ~/Downloads/dev/photos

Output with no errors

Successfully moved 3 files.

Output with errors

Failed to move 3 files.

Output after running on 4,000+ images

Successfully moved 4,216 images in under 2 seconds!

Images organized into subdirectories based on date

output directory with images organized by date

I hope you found this post useful, please subscribe for more content 🍻

 

I've revisited that script and enhanced it to allow recursively organizing folders. Check it out here!

KB

👨🏻‍💻 Developer 👨🏻‍💼 Entrepreneur 👨🏻‍🎨 Indie Artist 📷 Photographer

https://karlboghossian.com
Previous
Previous

Debugging Node.js using Visual Studio Code

Next
Next

Using Let's Encrypt SSL Certificate with Auto-Renewal on WordPress Site Hosted on AWS Lightsail