Organize Files into Directories Recursively using Node.js (Advanced)
Ever wondered how you could merge contents of folders based on when the files were created? This post will cover how I used Node.js to take a manual tedious work and automate the work for me.
In my previous post on how I programmed a small files organizer using Node.js, I realized that sometimes I come across other situations where I need to "merge" directories that share the samename but contain different photos.
Mac OS X does not allow merging directories...
As you can see from the screenshot above, because Mac OS X doesn't allow merging directories sharing the same name. So they'll end up replacing the content of the destination directory altogether, ouch!
Revisiting our Node.js script
If you aren't familiar with the first iteration of the script, you might want to check my original post here.
In my first iteration of that script.js, I was mostly expecting that all my files/photos will be flat in my source directory, and would end up getting organized according to their creation date.
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;
}But with my current scenario that wouldn't work, that's because I want to traverse all the folders and subfolders in order to move the files over to an existing/or new folder structure.
Restructuring the script
Here's the content of the script.js file:
const fs = require('fs')
const organizeFile = require('./organizeFile')
console.log('\n------------------------------------')
console.log('🗂 Welcome to KB\'s File Organizer 🗂')
console.log('------------------------------------\n')
const argv = require('yargs')
.usage('Usage:')
.options('from', {
alias: 'sourceDirectory',
default: './photos',
describe: 'The directory containing photos/folders that need to be organized.'
})
.options('to', {
alias: 'destinationDirectory',
default: './output',
describe: 'Root directory to store the organized photos.'
})
.options('incSub', {
alias: 'includeSubdirectories',
default: true,
describe: 'Whether to organize subdirectories within directories.'
})
.help()
.argv
function traverse (path, outputDir, includeDirectories) {
fs.readdir(path, (err, files) => {
if (err) {
console.log(`Error getting directory information ${path}: ${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(`📂 ${path} (${totalFiles})`)
// 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 filePath = `${path}/${file}`
const stats = fs.statSync(filePath)
// if that's a directory let's recurse.
if (stats.isDirectory()) {
if (includeDirectories === true) {
traverse(filePath, outputDir, includeDirectories)
}
} else {
// organize that file and see if we got the file back
const result = organizeFile(filePath, file, outputDir)
if (result.error) {
errorFiles.push(result.file)
}
}
})
console.log(`🗂 ${path} (${i}/${totalFiles}) ✔️\n`)
if (errorFiles.length) {
console.log(`⚠️ 📂 ERRORS ${path} (${errorFiles.length}/${totalFiles}) ⚠️`)
errorFiles.forEach((val, index) => {
console.log(val)
})
console.log('\n')
}
})
}
traverse(argv.from, argv.to, argv.incSub === 'true')The first big change is that I introduced yargs as a new npm module, so that I can enhance how the parameters are passed to the script. Where you can do something like:
node script.js --from ./test --to ./test-out --incSub true
Notice how the —-from and —-to can point to a source and destination folders. Don't forget to run npm i in terminal in the root folder to install that new package!
The other change is that I now check if I have a directory/folder, and if permitted (i.e.: —-incSub true) go ahead and call that same method recursively to visit that folder.
const fs = require('fs')
function pad(num, size = 2) {
var s = num + '';
while (s.length < size) s = '0' + s;
return s;
}
module.exports = function organizeFile (filePath, file, outputDir) {
//console.log(`📄 ${filePath}`)
const stats = fs.statSync(filePath)
// 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 = dayDir
let error
try {
fs.renameSync(filePath, `${dayDir}/${file}`)
console.log('📄', filePath, `→ ${moveStatus} ✔️`);
} catch (err) {
console.log('❌', filePath, moveStatus, `ERROR: ${err}`);
error = err
}
return {
file,
error
}
}
}The last change was to pull out all the specific file organization part into its own file and include it at the top organizeFile.js.
That's pretty much it, now you can run the script and enjoy recursively moving folders to where they belong! 🥳
Usage example to move files non recursively
Moving a file without recursing over subdirectories.
Running the script with no subdirectory recursion
Notice how that single file was moved to the new destination based on its creation date.
Usage example to move files recursively
Moving files by recursing over subdirectories.
Running the script with subdirectory recursion
Notice how all the files got moved starting with the first one (underneath folder 09), then the other 2 files (underneath 2019_09_28) and finally the 3 files.
I hope you found this post useful 🍻