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 not1
. - 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!
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.