smart-file-organizer
Version:
Intelligent CLI tool for automatically organizing files by type with preview, logging, and undo functionality
523 lines (455 loc) ⢠13.9 kB
JavaScript
import fs from "fs/promises";
import path from "path";
import readline from "readline";
// Default configuration - expandable and customizable
const defaultConfig = {
categories: {
images: [
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".svg",
".webp",
".tiff",
".ico",
".raw",
],
documents: [
".pdf",
".doc",
".docx",
".txt",
".rtf",
".odt",
".pages",
".tex",
],
spreadsheets: [".xls", ".xlsx", ".csv", ".ods", ".numbers"],
presentations: [".ppt", ".pptx", ".odp", ".key"],
audio: [".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a", ".wma"],
video: [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v"],
archives: [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"],
code: [
".js",
".ts",
".html",
".css",
".py",
".java",
".cpp",
".c",
".php",
".rb",
".go",
],
ebooks: [".epub", ".mobi", ".azw", ".fb2", ".pdf"],
fonts: [".ttf", ".otf", ".woff", ".woff2", ".eot"],
},
// Comprehensive list of files that should NEVER be moved
excludeFiles: [
// Package managers
"package.json",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"composer.json",
"composer.lock",
"Gemfile",
"Gemfile.lock",
"requirements.txt",
"poetry.lock",
"Pipfile",
"Pipfile.lock",
// Git files
".gitignore",
".gitattributes",
".gitmodules",
".gitkeep",
// Documentation
"README.md",
"README.txt",
"README.rst",
"CHANGELOG.md",
"CHANGELOG.txt",
"CONTRIBUTING.md",
"AUTHORS.md",
"CONTRIBUTORS.md",
// License files
"LICENSE",
"LICENSE.md",
"LICENSE.txt",
"LICENCE",
"LICENCE.md",
"LICENCE.txt",
// Environment and config files
".env",
".env.example",
".env.local",
".env.production",
".env.development",
"tsconfig.json",
"jsconfig.json",
"webpack.config.js",
"vite.config.js",
"next.config.js",
"nuxt.config.js",
"vue.config.js",
"angular.json",
"tailwind.config.js",
"postcss.config.js",
"babel.config.js",
// Linting and formatting
".eslintrc",
".eslintrc.js",
".eslintrc.json",
".eslintignore",
".prettierrc",
".prettierrc.js",
".prettierrc.json",
".prettierignore",
// Docker
"Dockerfile",
"docker-compose.yml",
"docker-compose.yaml",
".dockerignore",
// CI/CD
"netlify.toml",
"vercel.json",
".travis.yml",
"appveyor.yml",
// Build tools
"Makefile",
"CMakeLists.txt",
"build.gradle",
"pom.xml",
// App-specific files
"file-organizer.log",
"index.js",
"app.js",
// Archives and packages (avoid moving compressed files that might be important)
"*.tgz",
"*.tar.gz",
],
// Folders to completely skip
excludeFolders: [
"node_modules",
".git",
".vscode",
".idea",
"dist",
"build",
"coverage",
".nyc_output",
"target",
"bin",
"obj",
".gradle",
".next",
".nuxt",
"__pycache__",
".pytest_cache",
"venv",
"env",
".svn",
".hg",
"vendor",
],
logFile: "file-organizer.log",
};
class FileOrganizer {
constructor(config = defaultConfig) {
this.config = config;
this.stats = {
moved: 0,
created: [],
errors: [],
skipped: [],
startTime: new Date(),
};
}
async log(message, level = "INFO") {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${level}: ${message}\n`;
try {
await fs.appendFile(this.config.logFile, logEntry);
} catch (error) {
console.warn("ā ļø Could not write to log file:", error.message);
}
}
async getTargetFolder(fileExtension, fileName) {
// Check for important files that should not be moved
const importantPatterns = [
/^(readme|license|licence|changelog|contributing|authors|contributors)/i,
/\.(md|txt|rst)$/i && fileName.toLowerCase().includes("readme"),
/\.(env|config|conf)$/i,
/^(makefile|dockerfile|rakefile|gemfile|podfile)$/i,
];
if (
importantPatterns.some(
(pattern) => pattern.test && pattern.test(fileName)
)
) {
return null; // Don't move this file
}
// Check categories
for (const [category, extensions] of Object.entries(
this.config.categories
)) {
if (extensions.includes(fileExtension.toLowerCase())) {
return category;
}
}
return "others";
}
async createFolderIfNeeded(folderName) {
try {
await fs.access(folderName);
} catch {
await fs.mkdir(folderName, { recursive: true });
this.stats.created.push(folderName);
console.log(`š Created folder: ${folderName}/`);
await this.log(`Created folder: ${folderName}`);
}
}
async organizeFolder(targetPath = ".", preview = false) {
try {
console.log(
`\nš ${preview ? "Preview" : "Organizing"}: ${path.resolve(
targetPath
)}\n`
);
const files = await fs.readdir(targetPath);
const filesToMove = [];
// Analyze files first
for (const file of files) {
const filePath = path.join(targetPath, file);
try {
const stats = await fs.stat(filePath);
// Skip directories, hidden files, excluded files and folders
if (
stats.isDirectory() ||
file.startsWith(".") ||
this.config.excludeFiles.includes(file) ||
this.config.excludeFolders.includes(file) ||
// Skip files without extension (often important files)
(!path.extname(file) && !file.includes("."))
) {
if (stats.isDirectory()) {
this.stats.skipped.push(`${file}/ (directory)`);
} else {
this.stats.skipped.push(`${file} (excluded/important file)`);
}
continue;
}
const fileExtension = path.extname(file);
const targetFolder = await this.getTargetFolder(fileExtension, file);
// Skip files that should not be moved
if (targetFolder === null) {
console.log(`āļø Skipping important file: ${file}`);
this.stats.skipped.push(`${file} (important file)`);
continue;
}
filesToMove.push({
name: file,
path: filePath,
targetFolder,
size: stats.size,
});
} catch (error) {
this.stats.errors.push(`Could not read ${file}: ${error.message}`);
}
}
if (filesToMove.length === 0) {
console.log(
"š Folder is already organized or contains no files to organize!"
);
if (this.stats.skipped.length > 0) {
console.log(
`\nš Skipped ${this.stats.skipped.length} items (directories, config files, etc.)`
);
}
return;
}
// Show overview
const folderSummary = {};
filesToMove.forEach((file) => {
if (!folderSummary[file.targetFolder]) {
folderSummary[file.targetFolder] = [];
}
folderSummary[file.targetFolder].push(file.name);
});
console.log("š Overview of changes:");
for (const [folder, files] of Object.entries(folderSummary)) {
console.log(` š ${folder}/ (${files.length} files)`);
files.slice(0, 3).forEach((file) => console.log(` ⢠${file}`));
if (files.length > 3) {
console.log(` ... and ${files.length - 3} more`);
}
}
if (this.stats.skipped.length > 0) {
console.log(
`\nāļø Skipped ${this.stats.skipped.length} items (config files, directories, etc.)`
);
}
if (preview) {
console.log(
"\n⨠This was just a preview. Run without --preview to execute the changes."
);
return;
}
// Ask for confirmation
const confirmed = await this.askConfirmation(
"\nā Do you want to continue? (y/N): "
);
if (!confirmed) {
console.log("ā Cancelled by user");
return;
}
// Execute the moving
console.log("\nš Starting organization...\n");
for (const file of filesToMove) {
try {
await this.createFolderIfNeeded(file.targetFolder);
const newPath = path.join(file.targetFolder, file.name);
await fs.rename(file.path, newPath);
console.log(`š ${file.name} ā ${file.targetFolder}/`);
await this.log(`Moved: ${file.name} to ${file.targetFolder}/`);
this.stats.moved++;
} catch (error) {
const errorMsg = `Could not move ${file.name}: ${error.message}`;
this.stats.errors.push(errorMsg);
console.error(`ā ${errorMsg}`);
await this.log(errorMsg, "ERROR");
}
}
this.showSummary();
} catch (error) {
console.error("ā Critical error:", error.message);
await this.log(`Critical error: ${error.message}`, "ERROR");
}
}
async askConfirmation(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(["y", "yes"].includes(answer.toLowerCase()));
});
});
}
showSummary() {
const duration = ((new Date() - this.stats.startTime) / 1000).toFixed(2);
console.log("\nā
Organization completed!");
console.log("š Statistics:");
console.log(` ⢠Files moved: ${this.stats.moved}`);
console.log(` ⢠Folders created: ${this.stats.created.length}`);
console.log(` ⢠Errors: ${this.stats.errors.length}`);
console.log(` ⢠Items skipped: ${this.stats.skipped.length}`);
console.log(` ⢠Time taken: ${duration}s`);
if (this.stats.created.length > 0) {
console.log(` ⢠New folders: ${this.stats.created.join(", ")}`);
}
if (this.stats.errors.length > 0) {
console.log("\nā ļø Errors that occurred:");
this.stats.errors
.slice(0, 5)
.forEach((error) => console.log(` ⢠${error}`));
if (this.stats.errors.length > 5) {
console.log(` ... and ${this.stats.errors.length - 5} more errors`);
}
}
console.log(`\nš Details saved to: ${this.config.logFile}`);
}
showHelp() {
console.log(`
šļø Smart File Organizer CLI
USAGE:
organize-files [options] [folder]
file-organizer [options] [folder]
OPTIONS:
--preview, -p Show what will happen without making changes
--help, -h Show this help message
--config, -c Show current configuration
EXAMPLES:
organize-files # Organize current folder
organize-files --preview # Preview changes
organize-files ~/Downloads # Organize Downloads folder
organize-files -p ./test # Preview test folder
ā ļø SAFETY FEATURES:
⢠Always use --preview first to see what will happen
⢠Automatically skips important files (package.json, README.md, etc.)
⢠Best suited for Downloads, Desktop, and similar folders
⢠Avoids moving configuration and project files
š”ļø PROTECTED FILES:
The script automatically skips:
⢠package.json, README.md, LICENSE, etc.
⢠Configuration files (.env, tsconfig.json, etc.)
⢠Git files (.gitignore, etc.)
⢠Folders like node_modules, .git, etc.
SUPPORTED FILE TYPES:
${Object.entries(defaultConfig.categories)
.map(
([cat, exts]) =>
` š ${cat}: ${exts.slice(0, 4).join(", ")}${
exts.length > 4 ? "..." : ""
}`
)
.join("\n")}
`);
}
showConfig() {
console.log("\nāļø Current configuration:");
console.log(JSON.stringify(this.config, null, 2));
}
}
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const options = {
preview: false,
help: false,
config: false,
targetPath: ".",
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--preview" || arg === "-p") {
options.preview = true;
} else if (arg === "--help" || arg === "-h") {
options.help = true;
} else if (arg === "--config" || arg === "-c") {
options.config = true;
} else if (!arg.startsWith("-")) {
options.targetPath = arg;
}
}
return options;
}
// Main function
async function main() {
console.log("šļø Smart File Organizer CLI v1.0.0");
console.log("=====================================\n");
const options = parseArgs();
const organizer = new FileOrganizer();
if (options.help) {
organizer.showHelp();
return;
}
if (options.config) {
organizer.showConfig();
return;
}
await organizer.organizeFolder(options.targetPath, options.preview);
}
// Only run if file is called directly
import { fileURLToPath } from "url";
if (fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch(console.error);
}
export { main };