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

KarlBoghossian File Organizer

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!

Ledger Manager Cover

Ledger Manager

Check out my iOS app which facilitates recording crypto transactions (Bitcoin, Ethereum, etc.), simplifies tax reporting & reports on overall net worth!

*Everything is synced privately on your iCloud account.
**Helps getting your crypto tax reporting in order (Form 8949) with smart calculation of cost basis & acquisition dates.

0

Leave a Reply

Your email address will not be published. Required fields are marked *