├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmrc ├── .travis.yml ├── .vscode └── settings.json ├── License.md ├── README.md ├── appveyor.yml ├── assets └── demo.gif ├── dist ├── cli.js ├── formats.js └── helpers.js ├── gulpfile.js ├── package.json ├── src ├── cli.js ├── formats.js └── helpers.js ├── test └── testOrganize.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "plugins": [] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-console": "off", 5 | "strict": "off", 6 | "prefer-const": "off", 7 | "no-restricted-syntax": "off", 8 | "comma-dangle": "off", 9 | "no-unused-vars": "off", 10 | "import/no-extraneous-dependencies": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | 1. Please check whether another person has raised a [pull request](https://github.com/manrajgrover/organize-cli/pulls) for same issue before creating one. 4 | 2. Please check [issues](https://github.com/manrajgrover/organize-cli/issues) created before requesting for a feature. 5 | 3. Open a pull request explaining what changes it brings. 6 | 4. Add references where applicable. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | ## Description 13 | 14 | 15 | ### Versions 16 | 17 | - NodeJS version (if applicable): 18 | - Organize CLI version: 19 | 20 | ### Error 21 | 22 | 23 | ### Expected behaviour 24 | 25 | 26 | ## Steps to recreate 27 | 28 | 29 | ## People to notify 30 | 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description of new feature, or changes 10 | 11 | 12 | ## Checklist 13 | 14 | - [ ] Your branch is up-to-date with the base branch 15 | - [ ] You've included at least one test if this is a new feature 16 | - [ ] All tests are passing 17 | 18 | ## Related Issues and Discussions 19 | 20 | 21 | 22 | ## People to notify 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /testing 3 | 4 | 5 | .nyc_output 6 | package-lock.json 7 | 8 | npm-debug.log 9 | yarn-error.log 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7.4 4 | install: 5 | - npm install 6 | script: 7 | - npm link 8 | - npm run lint 9 | - npm run test 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.formatOnSave": false 4 | }, 5 | "editor.formatOnSave": true, 6 | "eslint.autoFixOnSave": true 7 | } 8 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Manraj Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # organize-cli 2 | [![Build Status](https://travis-ci.org/manrajgrover/organize-cli.svg?branch=master)](https://travis-ci.org/manrajgrover/organize-cli) [![Build status](https://ci.appveyor.com/api/projects/status/ynou4s6geylqsmo1?svg=true)](https://ci.appveyor.com/project/manrajgrover/organize-cli) 3 | [![npm](https://img.shields.io/npm/v/organize-cli.svg?maxAge=2592000?style=flat-square)](https://www.npmjs.com/package/organize-cli) [![npm](https://img.shields.io/npm/dt/organize-cli.svg?maxAge=2592000?style=flat-square)](https://www.npmjs.com/package/organize-cli) ![awesome](https://img.shields.io/badge/awesome-yes-green.svg) 4 | 5 | > Organize files based on file types, formally known as `organizeit` 6 | 7 | 8 | 9 | ## Installation 10 | 11 | ``` 12 | $ npm install -g organize-cli 13 | ``` 14 | 15 | ## Usage 16 | 17 | ``` 18 | Usage: organize [options] 19 | 20 | Options: 21 | -o, --output Output directory - Creates one if doesn't exist [string] 22 | -d, --date Organize files by dates [boolean] 23 | -s, --source Source directory to organize [string] [required] 24 | -t, --type Specific types to organize - strings of file extensions [array] 25 | -f, --folder Specific folder to move specific files to [string] 26 | -h, --help Show help [boolean] 27 | 28 | Examples: 29 | organize -s ~/Downloads -o . -t mp3 wav -f "Songs" 30 | ``` 31 | 32 | ## Development 33 | 34 | Run: 35 | 36 | ```sh 37 | $ git clone https://github.com/manrajgrover/organize-cli.git 38 | $ cd organize-cli 39 | $ npm link 40 | ``` 41 | 42 | This will setup a symbolic link to the CLI. Any changes in source files will now be reflected when running the `organize` command. 43 | 44 | To lint your code, run 45 | 46 | ```sh 47 | $ npm run lint 48 | ``` 49 | 50 | ## Like it? 51 | 52 | :star2: this repo to show support. You can also tweet about this project by clicking [here](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fmanrajgrover%2Forganize-cli&via=manrajsgrover&text=Checkout%20this%20command%20line%20tool%20for%20organizing%20your%20files%20in%20a%20better%20way%20on%20%23Github&hashtags=cli%2C%20node). 53 | 54 | ## Related 55 | 56 | * [classifier](https://github.com/bhrigu123/classifier) 57 | 58 | ## License 59 | [MIT](https://github.com/manrajgrover/organize-cli/blob/master/License.md) © Manraj Singh 60 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "7.4" 3 | 4 | install: 5 | - ps: Install-Product node $env:nodejs_version 6 | - npm install 7 | 8 | test_script: 9 | - node --version 10 | - npm --version 11 | - npm link 12 | - npm run lint 13 | - npm run test 14 | 15 | build: off 16 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manrajgrover/organize-cli/47d025fe70ac087d1990f3ba2d39dd378c0482c9/assets/demo.gif -------------------------------------------------------------------------------- /dist/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | 'use strict'; 5 | 6 | var yargs = require('yargs'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var ora = require('ora'); 10 | 11 | /** 12 | * Get helper functions from `helpers` 13 | */ 14 | 15 | var _require = require('./helpers'), 16 | organizeByDefaults = _require.organizeByDefaults, 17 | organizeBySpecificFileTypes = _require.organizeBySpecificFileTypes, 18 | organizeByDates = _require.organizeByDates; 19 | 20 | /** 21 | * Pass all arguments passed using `yargs` 22 | */ 23 | 24 | 25 | var argv = yargs.usage('Usage: $0 [options]').alias('o', 'output').describe('o', "Output directory - Creates one if doesn't exist").string('o').alias('d', 'date').describe('d', 'Organize files by dates').boolean('d').alias('s', 'source').describe('s', 'Source directory to organize').string('s').alias('t', 'type').describe('t', 'Specific types to organize - strings of file extensions').array('t').alias('f', 'folder').describe('f', 'Specific folder to move specific files to').string('f').alias('l', 'list').describe('l', 'List the mv commands that will be executed without actually executing them').boolean('l').demand(['s']).example('$0 -s ~/Downloads -o . -t mp3 wav -f "Songs"').help('h').alias('h', 'help').argv; 26 | 27 | /** 28 | * Spinner initialization 29 | */ 30 | var spinner = ora('Scanning').start(); 31 | 32 | /** 33 | * Get source directory, if provided in arguments 34 | * Defaults to current working directory 35 | */ 36 | var sourceDir = argv.source ? path.resolve(process.cwd(), argv.source) : process.cwd(); 37 | /** 38 | * Get output directory, if provided in arguments 39 | * Defaults to source directory 40 | */ 41 | var outputDir = argv.output ? path.resolve(process.cwd(), argv.output) : sourceDir; 42 | 43 | var names = fs.readdirSync(sourceDir); 44 | var moved = []; 45 | 46 | var listOnly = argv.l; 47 | 48 | // If date flag is passed, organize by dates 49 | if (argv.d) { 50 | moved = organizeByDates(names, sourceDir, outputDir, spinner, listOnly); 51 | } else if (argv.t && argv.f) { 52 | // Organize specific file formats and move to specific folder 53 | var spFormats = argv.t; 54 | var spFolder = argv.f; 55 | 56 | moved = organizeBySpecificFileTypes(spFormats, spFolder, names, sourceDir, outputDir, spinner, listOnly); 57 | } else { 58 | // Defaults to normal behavior 59 | moved = organizeByDefaults(names, sourceDir, outputDir, spinner, listOnly); 60 | } 61 | 62 | /** 63 | * Resolves all promises and catches any error 64 | * while moving a file 65 | */ 66 | Promise.all(moved.map(function (p) { 67 | return p.catch(function (e) { 68 | return e; 69 | }); 70 | })).then(function (messages) { 71 | var isError = false; 72 | 73 | // Check if any promise failed 74 | var _iteratorNormalCompletion = true; 75 | var _didIteratorError = false; 76 | var _iteratorError = undefined; 77 | 78 | try { 79 | for (var _iterator = messages[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 80 | var message = _step.value; 81 | 82 | if (message instanceof Error) { 83 | spinner.fail("Couldn't move all files!"); 84 | isError = true; 85 | break; 86 | } 87 | } 88 | } catch (err) { 89 | _didIteratorError = true; 90 | _iteratorError = err; 91 | } finally { 92 | try { 93 | if (!_iteratorNormalCompletion && _iterator.return) { 94 | _iterator.return(); 95 | } 96 | } finally { 97 | if (_didIteratorError) { 98 | throw _iteratorError; 99 | } 100 | } 101 | } 102 | 103 | if (!listOnly && !isError) { 104 | spinner.succeed('Moved all files!'); 105 | } 106 | }).catch(function (err) { 107 | return spinner.fail('An error occured!'); 108 | }); -------------------------------------------------------------------------------- /dist/formats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Music = ['MP3', 'WAV', 'WMA', 'MKA', 'AAC', 'MID', 'RA', 'RAM', 'RM', 'OGG']; 4 | var Codes = ['CPP', 'RB', 'PY', 'HTML', 'CSS', 'JS', 'PHP']; 5 | var Compressed = ['RAR', 'JAR', 'ZIP', 'TAR', 'MAR', 'ISO', 'LZ', '7ZIP', 'TGZ', 'GZ', 'BZ2']; 6 | var Documents = ['DOC', 'DOCX', 'PPT', 'PPTX', 'PAGES', 'PDF', 'ODT', 'ODP', 'XLSX', 'XLS', 'ODS', 'TXT', 'IN', 'OUT', 'MD']; 7 | var Images = ['JPG', 'JPEG', 'GIF', 'PNG', 'SVG']; 8 | var Executables = ['DEB', 'EXE', 'SH', 'BUNDLE']; 9 | var Video = ['FLV', 'WMV', 'MOV', 'MP4', 'MPEG', '3GP', 'MKV']; 10 | 11 | module.exports = { 12 | Music: Music, 13 | Codes: Codes, 14 | Compressed: Compressed, 15 | Documents: Documents, 16 | Images: Images, 17 | Video: Video, 18 | Executables: Executables 19 | }; -------------------------------------------------------------------------------- /dist/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mv = require('mv'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var dateformat = require('dateformat'); 7 | var formats = require('./formats'); 8 | 9 | /** 10 | * Check if file is valid 11 | * 12 | * @param {string} name Name of file 13 | * @param {string} dir File directory 14 | */ 15 | var isValidFile = function isValidFile(name, dir) { 16 | return name.indexOf('.') !== 0 && !fs.statSync(path.join(dir, name)).isDirectory(); 17 | }; 18 | 19 | /** 20 | * Create a directory if it does not exist 21 | * 22 | * @param {string} folderPath Path of folder to be created 23 | */ 24 | var mkdir = function mkdir(folderPath) { 25 | try { 26 | fs.mkdirSync(folderPath); 27 | } catch (err) { 28 | if (err.code !== 'EEXIST') { 29 | throw new Error('Error occurred while creating a new directory'); 30 | } 31 | } 32 | }; 33 | 34 | /** 35 | * Get extension of a file 36 | * 37 | * @param {string} fileName File name 38 | */ 39 | var getFileExtension = function getFileExtension(fileName) { 40 | var i = fileName.lastIndexOf('.'); 41 | return i < 0 ? '' : fileName.substr(i + 1); 42 | }; 43 | 44 | /** 45 | * Returns a promise for movement of file to specific directory; 46 | * Also creates the output directory if not existing 47 | * 48 | * @param {Object} spinner Ora spinner instance 49 | * @param {string} source Source directory name 50 | * @param {string} output Output directory name 51 | * @param {string} fileName File name 52 | * @param {string} type File type 53 | * @param {boolean} listOnly Only list the commands which will be executed for movement 54 | */ 55 | var organize = function organize(spinner, source, output, fileName, type, listOnly) { 56 | var typeDir = path.resolve(output, type); 57 | 58 | // Create the directory only if listOnly is not set 59 | if (!listOnly) { 60 | mkdir(output); 61 | mkdir(typeDir); 62 | } 63 | 64 | // Return promise for moving a specific file to specific directory 65 | return new Promise(function (resolve, reject) { 66 | // If listOnly is set, output the command that will be executed without 67 | // moving the file 68 | if (listOnly) { 69 | var listMessage = 'mv ' + path.resolve(source, fileName) + ' ' + path.resolve(typeDir, fileName); 70 | spinner.info(listMessage); 71 | resolve(listMessage); 72 | } else { 73 | // Move the file 74 | mv(path.resolve(source, fileName), path.resolve(typeDir, fileName), function (err) { 75 | if (err) { 76 | var errorMessage = 'Couldn\'t move ' + fileName + ' because of following error: ' + err; 77 | spinner.warn(errorMessage); 78 | reject(new Error(errorMessage)); 79 | } else { 80 | var successMessage = 'Moved ' + fileName + ' to ' + type + ' folder'; 81 | spinner.info(successMessage); 82 | resolve(successMessage); 83 | } 84 | }); 85 | } 86 | }); 87 | }; 88 | 89 | /** 90 | * Organizes files using pre-configured formats and file extensions 91 | * 92 | * @param {Array} files File names 93 | * @param {string} sourceDir Source directory name 94 | * @param {string} outputDir Output directory name 95 | * @param {Object} spinner Ora spinner instance 96 | * @param {boolean} listOnly Only list the commands which will be executed for movement 97 | */ 98 | var organizeByDefaults = function organizeByDefaults(files, sourceDir, outputDir, spinner, listOnly) { 99 | var moved = []; 100 | 101 | var _iteratorNormalCompletion = true; 102 | var _didIteratorError = false; 103 | var _iteratorError = undefined; 104 | 105 | try { 106 | for (var _iterator = files[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 107 | var file = _step.value; 108 | 109 | // Check if file is valid 110 | if (isValidFile(file, sourceDir)) { 111 | // Get file extension 112 | var extension = getFileExtension(file).toUpperCase(); 113 | var isMoved = false; 114 | 115 | // Iterating over format types 116 | var _iteratorNormalCompletion2 = true; 117 | var _didIteratorError2 = false; 118 | var _iteratorError2 = undefined; 119 | 120 | try { 121 | for (var _iterator2 = Object.keys(formats)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 122 | var type = _step2.value; 123 | 124 | if (formats[type].indexOf(extension) >= 0) { 125 | // Output to spinner that this file will be moved 126 | spinner.info('Moving file ' + file + ' to ' + type); 127 | 128 | // Move the file to format directory 129 | var pOrganize = organize(spinner, sourceDir, outputDir, file, type, listOnly); 130 | 131 | // Push the promise to array 132 | moved.push(pOrganize); 133 | isMoved = true; 134 | break; 135 | } 136 | } 137 | 138 | // If file extension does not exist in config, 139 | // move the file to Miscellaneous folder 140 | } catch (err) { 141 | _didIteratorError2 = true; 142 | _iteratorError2 = err; 143 | } finally { 144 | try { 145 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 146 | _iterator2.return(); 147 | } 148 | } finally { 149 | if (_didIteratorError2) { 150 | throw _iteratorError2; 151 | } 152 | } 153 | } 154 | 155 | if (!isMoved) { 156 | // Output to spinner that this file will be moved 157 | spinner.info('Moving file ' + file + ' to Miscellaneous'); 158 | 159 | // Push the promise to array 160 | moved.push(organize(spinner, sourceDir, outputDir, file, 'Miscellaneous', listOnly)); 161 | } 162 | } 163 | } 164 | } catch (err) { 165 | _didIteratorError = true; 166 | _iteratorError = err; 167 | } finally { 168 | try { 169 | if (!_iteratorNormalCompletion && _iterator.return) { 170 | _iterator.return(); 171 | } 172 | } finally { 173 | if (_didIteratorError) { 174 | throw _iteratorError; 175 | } 176 | } 177 | } 178 | 179 | return moved; 180 | }; 181 | 182 | /** 183 | * Organize specific file types 184 | * 185 | * @param {Array} spFormats Organize only specific formats 186 | * @param {string} spFolder Move specific files to this folder name 187 | * @param {Array} files File names 188 | * @param {string} sourceDir Source directory name 189 | * @param {string} outputDir Output directory name 190 | * @param {Object} spinner Ora spinner instance 191 | * @param {boolean} listOnly Only list the commands which will be executed for movement 192 | */ 193 | var organizeBySpecificFileTypes = function organizeBySpecificFileTypes(spFormats, spFolder, files, sourceDir, outputDir, spinner, listOnly) { 194 | // Filter file names on specific formats 195 | var names = files.filter(function (name) { 196 | if (!isValidFile(name, sourceDir)) { 197 | return false; 198 | } 199 | 200 | var extension = getFileExtension(name); 201 | return spFormats.indexOf(extension) !== -1; 202 | }); 203 | 204 | var moved = []; 205 | 206 | var _iteratorNormalCompletion3 = true; 207 | var _didIteratorError3 = false; 208 | var _iteratorError3 = undefined; 209 | 210 | try { 211 | for (var _iterator3 = names[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 212 | var name = _step3.value; 213 | 214 | // Output to spinner that this file will be moved 215 | spinner.info('Moving file ' + name + ' to ' + spFolder); 216 | 217 | // Move the file to output directory 218 | var pOrganize = organize(spinner, sourceDir, outputDir, name, spFolder, listOnly); 219 | 220 | // Push the promise to array 221 | moved.push(pOrganize); 222 | } 223 | } catch (err) { 224 | _didIteratorError3 = true; 225 | _iteratorError3 = err; 226 | } finally { 227 | try { 228 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 229 | _iterator3.return(); 230 | } 231 | } finally { 232 | if (_didIteratorError3) { 233 | throw _iteratorError3; 234 | } 235 | } 236 | } 237 | 238 | return moved; 239 | }; 240 | 241 | /** 242 | * Organizes the files by creation date 243 | * 244 | * @param {Array} files Files to be organized 245 | * @param {string} sourceDir Source directory name 246 | * @param {string} outputDir Output directory name 247 | * @param {object} spinner Ora spinner instance 248 | * @param {boolean} listOnly Only list the commands which will be executed for movement 249 | */ 250 | var organizeByDates = function organizeByDates(files, sourceDir, outputDir, spinner, listOnly) { 251 | var moved = []; 252 | 253 | var _iteratorNormalCompletion4 = true; 254 | var _didIteratorError4 = false; 255 | var _iteratorError4 = undefined; 256 | 257 | try { 258 | for (var _iterator4 = files[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { 259 | var file = _step4.value; 260 | 261 | // Get date when the file was created 262 | var date = fs.statSync(path.join(sourceDir, file)); 263 | date = dateformat(new Date(date.mtime), 'yyyy-mm-dd'); 264 | 265 | // Output to spinner that this file will be moved 266 | spinner.info('Moving file ' + file + ' to ' + date + ' folder'); 267 | 268 | // Move the file to output directory 269 | var pOrganize = organize(spinner, sourceDir, outputDir, file, date, listOnly); 270 | 271 | // Push the promise to array 272 | moved.push(pOrganize); 273 | } 274 | } catch (err) { 275 | _didIteratorError4 = true; 276 | _iteratorError4 = err; 277 | } finally { 278 | try { 279 | if (!_iteratorNormalCompletion4 && _iterator4.return) { 280 | _iterator4.return(); 281 | } 282 | } finally { 283 | if (_didIteratorError4) { 284 | throw _iteratorError4; 285 | } 286 | } 287 | } 288 | 289 | return moved; 290 | }; 291 | 292 | module.exports = { 293 | mkdir: mkdir, 294 | getFileExtension: getFileExtension, 295 | organize: organize, 296 | organizeByDefaults: organizeByDefaults, 297 | organizeBySpecificFileTypes: organizeBySpecificFileTypes, 298 | organizeByDates: organizeByDates 299 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | 4 | gulp.task('es6', () => { 5 | gulp.src('src/*.js') 6 | .pipe(babel({ 7 | presets: ['env'], 8 | })) 9 | .pipe(gulp.dest('./dist')); 10 | }); 11 | 12 | gulp.task('watch', () => { 13 | gulp.watch(['./src/*.js'], ['es6']); 14 | }); 15 | 16 | gulp.task('default', ['es6']); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "organize-cli", 3 | "version": "0.5.8", 4 | "description": "Organize your files and folders in just seconds", 5 | "scripts": { 6 | "lint": "eslint ./src/cli.js ./src/formats.js ./src/helpers.js", 7 | "test": "npm link && mocha --recursive --timeout 10000", 8 | "report": "nyc mocha --recursive", 9 | "all": "npm run test && npm run lint" 10 | }, 11 | "bin": { 12 | "organize": "./dist/cli.js" 13 | }, 14 | "preferGlobal": true, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/manrajgrover/organize-cli.git" 18 | }, 19 | "keywords": [ 20 | "organize", 21 | "cli", 22 | "classify", 23 | "files", 24 | "folders", 25 | "npm", 26 | "organize-cli" 27 | ], 28 | "maintainers": [ 29 | { 30 | "name": "manrajgrover", 31 | "email": "manrajsinghgrover@gmail.com" 32 | } 33 | ], 34 | "author": "Manraj Singh (http://manrajsingh.in)", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/manrajgrover/organize-cli/issues" 38 | }, 39 | "homepage": "https://github.com/manrajgrover/organize-cli#readme", 40 | "dependencies": { 41 | "dateformat": "^2.0.0", 42 | "mv": "^2.1.1", 43 | "ora": "^1.3.0", 44 | "path": "^0.12.7", 45 | "yargs": "^4.8.1" 46 | }, 47 | "devDependencies": { 48 | "babel-preset-env": "^1.7.0", 49 | "command-exists": "^1.2.6", 50 | "eslint": "^5.3.0", 51 | "eslint-config-airbnb": "17.1.0", 52 | "eslint-config-prettier": "^3.1.0", 53 | "eslint-plugin-import": "^2.14.0", 54 | "eslint-plugin-jsx-a11y": "^6.1.1", 55 | "eslint-plugin-prettier": "^3.0.0", 56 | "eslint-plugin-react": "^7.11.0", 57 | "fs-extra": "^3.0.1", 58 | "gulp": "^3.9.1", 59 | "gulp-babel": "^6.1.2", 60 | "mocha": "^5.2.0", 61 | "nyc": "^11.0.3", 62 | "prettier": "^1.14.3", 63 | "sync-exec": "^0.6.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const yargs = require('yargs'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const ora = require('ora'); 9 | 10 | /** 11 | * Get helper functions from `helpers` 12 | */ 13 | const { 14 | organizeByDefaults, 15 | organizeBySpecificFileTypes, 16 | organizeByDates 17 | } = require('./helpers'); 18 | 19 | 20 | /** 21 | * Pass all arguments passed using `yargs` 22 | */ 23 | // eslint-disable-next-line prefer-destructuring 24 | const argv = yargs 25 | .usage('Usage: $0 [options]') 26 | .alias('o', 'output') 27 | .describe('o', "Output directory - Creates one if doesn't exist") 28 | .string('o') 29 | .alias('d', 'date') 30 | .describe('d', 'Organize files by dates') 31 | .boolean('d') 32 | .alias('s', 'source') 33 | .describe('s', 'Source directory to organize') 34 | .string('s') 35 | .alias('t', 'type') 36 | .describe('t', 'Specific types to organize - strings of file extensions') 37 | .array('t') 38 | .alias('f', 'folder') 39 | .describe('f', 'Specific folder to move specific files to') 40 | .string('f') 41 | .alias('l', 'list') 42 | .describe('l', 'List the mv commands that will be executed without actually executing them') 43 | .boolean('l') 44 | .demand(['s']) 45 | .example('$0 -s ~/Downloads -o . -t mp3 wav -f "Songs"') 46 | .help('h') 47 | .alias('h', 'help') 48 | .argv; 49 | 50 | /** 51 | * Spinner initialization 52 | */ 53 | let spinner = ora('Scanning').start(); 54 | 55 | /** 56 | * Get source directory, if provided in arguments 57 | * Defaults to current working directory 58 | */ 59 | const sourceDir = argv.source ? path.resolve( 60 | process.cwd(), argv.source 61 | ) : process.cwd(); 62 | /** 63 | * Get output directory, if provided in arguments 64 | * Defaults to source directory 65 | */ 66 | const outputDir = argv.output ? path.resolve( 67 | process.cwd(), argv.output 68 | ) : sourceDir; 69 | 70 | let names = fs.readdirSync(sourceDir); 71 | let moved = []; 72 | 73 | let listOnly = argv.l; 74 | 75 | // If date flag is passed, organize by dates 76 | if (argv.d) { 77 | moved = organizeByDates(names, sourceDir, outputDir, spinner, listOnly); 78 | } else if (argv.t && argv.f) { 79 | // Organize specific file formats and move to specific folder 80 | const spFormats = argv.t; 81 | const spFolder = argv.f; 82 | 83 | moved = organizeBySpecificFileTypes( 84 | spFormats, spFolder, names, sourceDir, outputDir, spinner, listOnly 85 | ); 86 | } else { 87 | // Defaults to normal behavior 88 | moved = organizeByDefaults(names, sourceDir, outputDir, spinner, listOnly); 89 | } 90 | 91 | /** 92 | * Resolves all promises and catches any error 93 | * while moving a file 94 | */ 95 | Promise.all(moved.map(p => p.catch(e => e))) 96 | .then((messages) => { 97 | let isError = false; 98 | 99 | // Check if any promise failed 100 | for (let message of messages) { 101 | if (message instanceof Error) { 102 | spinner.fail("Couldn't move all files!"); 103 | isError = true; 104 | break; 105 | } 106 | } 107 | 108 | if (!listOnly && !isError) { 109 | spinner.succeed('Moved all files!'); 110 | } 111 | }) 112 | .catch(err => spinner.fail('An error occured!')); 113 | -------------------------------------------------------------------------------- /src/formats.js: -------------------------------------------------------------------------------- 1 | const Music = ['MP3', 'WAV', 'WMA', 'MKA', 'AAC', 'MID', 'RA', 'RAM', 'RM', 'OGG']; 2 | const Codes = ['CPP', 'RB', 'PY', 'HTML', 'CSS', 'JS', 'PHP']; 3 | const Compressed = ['RAR', 'JAR', 'ZIP', 'TAR', 'MAR', 'ISO', 'LZ', '7ZIP', 'TGZ', 'GZ', 'BZ2']; 4 | const Documents = ['DOC', 'DOCX', 'PPT', 'PPTX', 'PAGES', 'PDF', 'ODT', 'ODP', 'XLSX', 'XLS', 'ODS', 'TXT', 'IN', 'OUT', 'MD']; 5 | const Images = ['JPG', 'JPEG', 'GIF', 'PNG', 'SVG']; 6 | const Executables = ['DEB', 'EXE', 'SH', 'BUNDLE']; 7 | const Video = ['FLV', 'WMV', 'MOV', 'MP4', 'MPEG', '3GP', 'MKV']; 8 | 9 | module.exports = { 10 | Music, 11 | Codes, 12 | Compressed, 13 | Documents, 14 | Images, 15 | Video, 16 | Executables, 17 | }; 18 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mv = require('mv'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const dateformat = require('dateformat'); 7 | const formats = require('./formats'); 8 | 9 | /** 10 | * Check if file is valid 11 | * 12 | * @param {string} name Name of file 13 | * @param {string} dir File directory 14 | */ 15 | const isValidFile = (name, dir) => (name.indexOf('.') !== 0 16 | && !fs.statSync(path.join(dir, name)).isDirectory()); 17 | 18 | /** 19 | * Create a directory if it does not exist 20 | * 21 | * @param {string} folderPath Path of folder to be created 22 | */ 23 | const mkdir = (folderPath) => { 24 | try { 25 | fs.mkdirSync(folderPath); 26 | } catch (err) { 27 | if (err.code !== 'EEXIST') { 28 | throw new Error('Error occurred while creating a new directory'); 29 | } 30 | } 31 | }; 32 | 33 | /** 34 | * Get extension of a file 35 | * 36 | * @param {string} fileName File name 37 | */ 38 | const getFileExtension = (fileName) => { 39 | const i = fileName.lastIndexOf('.'); 40 | return (i < 0) ? '' : fileName.substr(i + 1); 41 | }; 42 | 43 | /** 44 | * Returns a promise for movement of file to specific directory; 45 | * Also creates the output directory if not existing 46 | * 47 | * @param {Object} spinner Ora spinner instance 48 | * @param {string} source Source directory name 49 | * @param {string} output Output directory name 50 | * @param {string} fileName File name 51 | * @param {string} type File type 52 | * @param {boolean} listOnly Only list the commands which will be executed for movement 53 | */ 54 | const organize = (spinner, source, output, fileName, type, listOnly) => { 55 | const typeDir = path.resolve(output, type); 56 | 57 | // Create the directory only if listOnly is not set 58 | if (!listOnly) { 59 | mkdir(output); 60 | mkdir(typeDir); 61 | } 62 | 63 | // Return promise for moving a specific file to specific directory 64 | return new Promise((resolve, reject) => { 65 | // If listOnly is set, output the command that will be executed without 66 | // moving the file 67 | if (listOnly) { 68 | const listMessage = `mv ${path.resolve(source, fileName)} ${path.resolve(typeDir, fileName)}`; 69 | spinner.info(listMessage); 70 | resolve(listMessage); 71 | } else { 72 | // Move the file 73 | mv(path.resolve(source, fileName), path.resolve(typeDir, fileName), (err) => { 74 | if (err) { 75 | const errorMessage = `Couldn't move ${fileName} because of following error: ${err}`; 76 | spinner.warn(errorMessage); 77 | reject(new Error(errorMessage)); 78 | } else { 79 | const successMessage = `Moved ${fileName} to ${type} folder`; 80 | spinner.info(successMessage); 81 | resolve(successMessage); 82 | } 83 | }); 84 | } 85 | }); 86 | }; 87 | 88 | /** 89 | * Organizes files using pre-configured formats and file extensions 90 | * 91 | * @param {Array} files File names 92 | * @param {string} sourceDir Source directory name 93 | * @param {string} outputDir Output directory name 94 | * @param {Object} spinner Ora spinner instance 95 | * @param {boolean} listOnly Only list the commands which will be executed for movement 96 | */ 97 | const organizeByDefaults = (files, sourceDir, outputDir, spinner, listOnly) => { 98 | const moved = []; 99 | 100 | for (let file of files) { 101 | // Check if file is valid 102 | if (isValidFile(file, sourceDir)) { 103 | // Get file extension 104 | const extension = getFileExtension(file).toUpperCase(); 105 | let isMoved = false; 106 | 107 | // Iterating over format types 108 | for (let type of Object.keys(formats)) { 109 | if (formats[type].indexOf(extension) >= 0) { 110 | // Output to spinner that this file will be moved 111 | spinner.info(`Moving file ${file} to ${type}`); 112 | 113 | // Move the file to format directory 114 | const pOrganize = organize(spinner, sourceDir, outputDir, file, type, listOnly); 115 | 116 | // Push the promise to array 117 | moved.push(pOrganize); 118 | isMoved = true; 119 | break; 120 | } 121 | } 122 | 123 | // If file extension does not exist in config, 124 | // move the file to Miscellaneous folder 125 | if (!isMoved) { 126 | // Output to spinner that this file will be moved 127 | spinner.info(`Moving file ${file} to Miscellaneous`); 128 | 129 | // Push the promise to array 130 | moved.push( 131 | organize(spinner, sourceDir, outputDir, file, 'Miscellaneous', listOnly) 132 | ); 133 | } 134 | } 135 | } 136 | 137 | return moved; 138 | }; 139 | 140 | /** 141 | * Organize specific file types 142 | * 143 | * @param {Array} spFormats Organize only specific formats 144 | * @param {string} spFolder Move specific files to this folder name 145 | * @param {Array} files File names 146 | * @param {string} sourceDir Source directory name 147 | * @param {string} outputDir Output directory name 148 | * @param {Object} spinner Ora spinner instance 149 | * @param {boolean} listOnly Only list the commands which will be executed for movement 150 | */ 151 | const organizeBySpecificFileTypes = ( 152 | spFormats, spFolder, files, sourceDir, outputDir, spinner, listOnly 153 | ) => { 154 | // Filter file names on specific formats 155 | const names = files.filter((name) => { 156 | if (!isValidFile(name, sourceDir)) { 157 | return false; 158 | } 159 | 160 | const extension = getFileExtension(name); 161 | return spFormats.indexOf(extension) !== -1; 162 | }); 163 | 164 | const moved = []; 165 | 166 | for (let name of names) { 167 | // Output to spinner that this file will be moved 168 | spinner.info(`Moving file ${name} to ${spFolder}`); 169 | 170 | // Move the file to output directory 171 | const pOrganize = organize(spinner, sourceDir, outputDir, name, spFolder, listOnly); 172 | 173 | // Push the promise to array 174 | moved.push(pOrganize); 175 | } 176 | 177 | return moved; 178 | }; 179 | 180 | /** 181 | * Organizes the files by creation date 182 | * 183 | * @param {Array} files Files to be organized 184 | * @param {string} sourceDir Source directory name 185 | * @param {string} outputDir Output directory name 186 | * @param {object} spinner Ora spinner instance 187 | * @param {boolean} listOnly Only list the commands which will be executed for movement 188 | */ 189 | const organizeByDates = (files, sourceDir, outputDir, spinner, listOnly) => { 190 | const moved = []; 191 | 192 | for (let file of files) { 193 | // Get date when the file was created 194 | let date = fs.statSync(path.join(sourceDir, file)); 195 | date = dateformat(new Date(date.mtime), 'yyyy-mm-dd'); 196 | 197 | // Output to spinner that this file will be moved 198 | spinner.info(`Moving file ${file} to ${date} folder`); 199 | 200 | // Move the file to output directory 201 | const pOrganize = organize(spinner, sourceDir, outputDir, file, date, listOnly); 202 | 203 | // Push the promise to array 204 | moved.push(pOrganize); 205 | } 206 | 207 | return moved; 208 | }; 209 | 210 | module.exports = { 211 | mkdir, 212 | getFileExtension, 213 | organize, 214 | organizeByDefaults, 215 | organizeBySpecificFileTypes, 216 | organizeByDates 217 | }; 218 | -------------------------------------------------------------------------------- /test/testOrganize.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | const fse = require('fs-extra'); 5 | const path = require('path'); 6 | const syncExec = require('sync-exec'); 7 | const commandExistsSync = require('command-exists').sync; 8 | 9 | const { mkdir } = require('../src/helpers'); 10 | const formats = require('../src/formats'); 11 | 12 | const TESTING_FOLDER = path.join(__dirname, '..', 'testing'); 13 | 14 | const SOURCE_FOLDER = path.join(TESTING_FOLDER, 'source'); 15 | const OUTPUT_FOLDER = path.join(TESTING_FOLDER, 'output'); 16 | 17 | const removeDirsFromFolder = (dirName) => { 18 | let files; 19 | try { 20 | files = fs.readdirSync(dirName); 21 | } catch (e) { 22 | console.log(`Couldn't read directory because of error: ${e}`); 23 | } 24 | 25 | if (files.length > 0) { 26 | for (let i = 0; i < files.length; i += 1) { 27 | let filePath = path.join(dirName, files[i]); 28 | if (fs.statSync(filePath).isDirectory()) { 29 | fse.removeSync(filePath); 30 | } 31 | } 32 | } 33 | }; 34 | 35 | describe('Organize Files', () => { 36 | beforeEach(() => { 37 | mkdir(TESTING_FOLDER); 38 | removeDirsFromFolder(TESTING_FOLDER); 39 | 40 | mkdir(SOURCE_FOLDER); 41 | mkdir(OUTPUT_FOLDER); 42 | 43 | for (let folderType of Object.keys(formats)) { 44 | for (let fileType of formats[folderType]) { 45 | fileType = fileType.toLowerCase(); 46 | const FILE_NAME = `test.${fileType}`; 47 | fs.writeFileSync(path.join(SOURCE_FOLDER, FILE_NAME), ''); 48 | } 49 | } 50 | 51 | // Some Miscellaneous files in source 52 | fs.writeFileSync(path.join(SOURCE_FOLDER, 'test'), ''); 53 | fs.writeFileSync(path.join(SOURCE_FOLDER, 'test.apib'), ''); 54 | fs.writeFileSync(path.join(SOURCE_FOLDER, 'test.ai'), ''); 55 | fs.writeFileSync(path.join(SOURCE_FOLDER, 'test.log'), ''); 56 | 57 | // Some Miscellaneous files in source 58 | fs.utimesSync(path.join(SOURCE_FOLDER, 'test'), '1499599912', '1499599912'); 59 | fs.utimesSync(path.join(SOURCE_FOLDER, 'test.apib'), '1499699912', '1499699912'); 60 | fs.utimesSync(path.join(SOURCE_FOLDER, 'test.ai'), '1499299912', '1499299912'); 61 | fs.utimesSync(path.join(SOURCE_FOLDER, 'test.log'), '1499399912', '1499399912'); 62 | 63 | if (!commandExistsSync('organize')) { 64 | throw new Error('Command "organize" command not found'); 65 | } 66 | }); 67 | 68 | it('should throw error for missing args', () => { 69 | const { stderr } = syncExec('organize'); 70 | 71 | assert.notEqual(stderr, ''); 72 | assert(stderr.includes('Missing required argument: s')); 73 | }); 74 | 75 | it('should organize files with source', () => { 76 | syncExec(`organize -s ${SOURCE_FOLDER}`); 77 | 78 | for (let folderType of Object.keys(formats)) { 79 | for (let fileType of formats[folderType]) { 80 | fileType = fileType.toLowerCase(); 81 | assert(fs.existsSync(path.join(SOURCE_FOLDER, folderType, `test.${fileType}`))); 82 | } 83 | } 84 | }); 85 | 86 | it('should organize files with source and output folder', () => { 87 | syncExec(`organize -s ${SOURCE_FOLDER} -o ${OUTPUT_FOLDER}`); 88 | 89 | for (let folderType of Object.keys(formats)) { 90 | for (let fileType of formats[folderType]) { 91 | fileType = fileType.toLowerCase(); 92 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, folderType, `test.${fileType}`))); 93 | } 94 | } 95 | }); 96 | 97 | it('should only list moving commands and not actually move the files', () => { 98 | syncExec(`organize -s ${SOURCE_FOLDER} -l`); 99 | 100 | for (let folderType of Object.keys(formats)) { 101 | for (let fileType of formats[folderType]) { 102 | fileType = fileType.toLowerCase(); 103 | assert(!fs.existsSync(path.join(OUTPUT_FOLDER, folderType, `test.${fileType}`))); 104 | } 105 | } 106 | }); 107 | 108 | it('should organize files with specific file type', () => { 109 | syncExec(`organize -s ${SOURCE_FOLDER} -o ${OUTPUT_FOLDER} -t ai 3gp -f misc`); 110 | 111 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, 'misc', 'test.ai'))); 112 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, 'misc', 'test.3gp'))); 113 | }); 114 | 115 | it('should organize files by dates', () => { 116 | syncExec(`organize -s ${SOURCE_FOLDER} -o ${OUTPUT_FOLDER} -d`); 117 | 118 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, '2017-07-06', 'test.ai'))); 119 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, '2017-07-07', 'test.log'))); 120 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, '2017-07-09', 'test'))); 121 | assert(fs.existsSync(path.join(OUTPUT_FOLDER, '2017-07-10', 'test.apib'))); 122 | }); 123 | }); 124 | --------------------------------------------------------------------------------