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
Output with errors
Output after running on 4,000+ images
Images organized into subdirectories based on 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!