diff --git a/README.md b/README.md index f18fdad..7277090 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,19 @@ # Backup Script CLI (English) -A TypeScript command-line utility (CLI) to quickly create ZIP backup archives of your project, allowing granular selection of files and folders. +A TypeScript command-line utility (CLI) to quickly create and extract ZIP backup archives of your project, allowing granular selection of files and folders and secure encryption. ## 🚀 Features +- **Compression & Decompression**: Choose whether to create a new backup or extract an existing one from a simple initial menu. - **Interactive Selection**: Choose which files and folders from the root to include in the archive. - **Automatic Encryption**: Generates a secure 14-character password to protect the ZIP archive. +- **Secure Extraction**: Allows extracting encrypted ZIP files by providing the correct password. - **Smart Exclusions**: Automatically excludes heavy or unnecessary folders like `node_modules`, `dist`, `.git`, and other existing backup files. - **Optimized Compression**: Uses `archiver` with compression level 9 (Zlib). - **Customizable Naming**: Suggests a name based on today's date (`backup_YYYY-MM-DD.zip`), but allows customization. - **Overwrite Protection**: Asks for confirmation before overwriting an existing file. -- **Real-time Feedback**: Displays progress percentage and the size of processed data. +- **Real-time Feedback**: Displays progress percentage and the size of processed data during compression. - **Code Quality**: Linting with ESLint and formatting with Prettier. - **Automated Testing**: Test suite with Jest to ensure logic correctness. - **Continuous Integration**: GitHub Actions configured for automatic lint, build, and test on every push. @@ -83,12 +85,6 @@ After the build, you can run the standard version: npm start ``` -Or the minified version: - -```bash -npm run start:minify -``` - ### Quality and Testing To lint the code: @@ -111,7 +107,7 @@ npm test ## 🏗️ Project Structure -- `src/main.ts`: Main entry point of the script. +- `src/main.ts`: Main entry point of the script (manages the main menu). - `src/utils/utils.ts`: Utility functions extracted for testability. - `tests/`: Automated test suite. - `dist/`: Contains bundles generated after the build. @@ -123,6 +119,7 @@ npm test - [archiver](https://www.npmjs.com/package/archiver): ZIP archive generation. - [archiver-zip-encryptable](https://www.npmjs.com/package/archiver-zip-encryptable): Password encryption support. +- [adm-zip](https://www.npmjs.com/package/adm-zip): Estrazione robusta di file ZIP con supporto password. - [inquirer](https://www.npmjs.com/package/inquirer): Interactive CLI interface. - [glob](https://www.npmjs.com/package/glob): Pattern matching for files. - [esbuild](https://esbuild.github.io/): Ultra-fast bundler for compilation. @@ -145,17 +142,19 @@ _Created to simplify local backup management during development._ # Backup Script CLI (Italiano) -Un'utilità a riga di comando (CLI) in TypeScript per creare rapidamente archivi ZIP di backup del progetto, permettendo la selezione granulare dei file e delle cartelle. +Un'utilità a riga di comando (CLI) in TypeScript per creare ed estrarre rapidamente archivi ZIP di backup del progetto, permettendo la selezione granulare dei file e delle cartelle e la cifratura sicura. ## 🚀 Caratteristiche +- **Compressione e Decompressione**: Scegli se creare un nuovo backup o estrarne uno esistente tramite un semplice menu iniziale. - **Selezione Interattiva**: Scegli quali file e cartelle della root includere nell'archivio. - **Cifratura Automatica**: Genera una password sicura a 14 caratteri per proteggere l'archivio ZIP. +- **Estrazione Sicura**: Permette di decomprimere file ZIP cifrati fornendo la password corretta. - **Esclusioni Intelligenti**: Esclude automaticamente cartelle pesanti o inutili come `node_modules`, `dist`, `.git` e altri file di backup esistenti. - **Compressione Ottimizzata**: Utilizza `archiver` con livello di compressione 9 (Zlib). - **Naming Personalizzabile**: Suggerisce un nome basato sulla data odierna (`backup_YYYY-MM-DD.zip`), ma permette la personalizzazione. - **Protezione Sovrascrittura**: Chiede conferma prima di sovrascrivere un file esistente. -- **Feedback in Tempo Reale**: Mostra la percentuale di avanzamento e la dimensione dei dati processati. +- **Feedback in Tempo Reale**: Mostra la percentuale di avanzamento e la dimensione dei dati processati durante la compressione. - **Qualità del Codice**: Linting con ESLint e formattazione con Prettier. - **Test Automatizzati**: Suite di test con Jest per garantire la correttezza della logica. - **Continuous Integration**: GitHub Actions configurate per lint, build e test automatici ad ogni push. @@ -220,12 +219,6 @@ Dopo la build, puoi eseguire la versione standard: npm start ``` -Oppure la versione minificata: - -```bash -npm run start:minify -``` - ### Qualità e Test Per eseguire il linting del codice: @@ -248,7 +241,7 @@ npm test ## 🏗️ Struttura del Progetto -- `src/main.ts`: Punto di ingresso principale dello script. +- `src/main.ts`: Punto di ingresso principale dello script (gestisce il menu principale). - `src/utils/utils.ts`: Funzioni di utilità estratte per testabilità. - `tests/`: Suite di test automatizzati. - `dist/`: Contiene i bundle generati dopo la build. @@ -260,6 +253,7 @@ npm test - [archiver](https://www.npmjs.com/package/archiver): Generazione di archivi ZIP. - [archiver-zip-encryptable](https://www.npmjs.com/package/archiver-zip-encryptable): Supporto alla cifratura con password. +- [adm-zip](https://www.npmjs.com/package/adm-zip): Estrazione robusta di file ZIP con supporto password. - [inquirer](https://www.npmjs.com/package/inquirer): Interfaccia interattiva CLI. - [glob](https://www.npmjs.com/package/glob): Corrispondenza di pattern per i file. - [esbuild](https://esbuild.github.io/): Bundler ultra-veloce per la compilazione. diff --git a/package.json b/package.json index 2f6cb28..f2bdf7f 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,14 @@ }, "license": "ISC", "dependencies": { + "adm-zip": "^0.5.17", "archiver": "^7.0.1", "archiver-zip-encryptable": "^1.0.10", "glob": "^13.0.6", "inquirer": "^8.2.7" }, "devDependencies": { + "@types/adm-zip": "^0.5.8", "@types/archiver": "^7.0.0", "@types/glob": "^9.0.0", "@types/inquirer": "^8.2.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e869068..26f4136 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + adm-zip: + specifier: ^0.5.17 + version: 0.5.17 archiver: specifier: ^7.0.1 version: 7.0.1 @@ -21,6 +24,9 @@ importers: specifier: ^8.2.7 version: 8.2.7(@types/node@25.6.0) devDependencies: + '@types/adm-zip': + specifier: ^0.5.8 + version: 0.5.8 '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -612,6 +618,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/adm-zip@0.5.8': + resolution: {integrity: sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==} + '@types/archiver@7.0.0': resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} @@ -856,6 +865,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + engines: {node: '>=12.0'} + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} @@ -2891,6 +2904,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/adm-zip@0.5.8': + dependencies: + '@types/node': 25.6.0 + '@types/archiver@7.0.0': dependencies: '@types/readdir-glob': 1.1.5 @@ -3132,6 +3149,8 @@ snapshots: acorn@8.16.0: {} + adm-zip@0.5.17: {} + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 diff --git a/src/main.ts b/src/main.ts index e6f56eb..3106bca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ const path = require("path"); const archiver = require("archiver"); const { glob } = require("glob"); const inquirer = require("inquirer"); +const AdmZip = require("adm-zip"); const { formatFileName, validateSelection, @@ -10,24 +11,18 @@ const { generatePassword, formatProgress, prepareChoices, + filterZipFiles, } = require("./utils/utils"); // Registra il formato zip-encryptable per archiver archiver.registerFormat("zip-encryptable", require("archiver-zip-encryptable")); /** - * Crea un file zip di backup di tutti i file nella directory genitore della cartella src, - * escludendo le cartelle node_modules, dist e altri file di backup. + * Crea un file zip di backup di tutti i file nella directory root del progetto. */ -async function createBackup(): Promise { - console.log("=== Backup Script CLI ==="); - console.log("Questo strumento creerà un archivio ZIP del progetto."); - console.log("Puoi scegliere quali file e cartelle della root includere.\n"); - - // 1. Identifica la root del progetto (genitore di src) - const projectRoot: string = __dirname; +async function createBackup(projectRoot: string): Promise { + console.log("\n--- Modalità Compressione ---"); - // 2. Legge il contenuto della root per permettere la selezione const rootContent = fs.readdirSync(projectRoot); const excludeList = [ "node_modules", @@ -55,11 +50,9 @@ async function createBackup(): Promise { const selectedItems = selectionAnswer.selectedItems; - // 3. Genera il nome di default del file di backup - const today: string = new Date().toISOString().slice(0, 10); // Formato YYYY-MM-DD + const today: string = new Date().toISOString().slice(0, 10); const defaultFileName: string = `backup_${today}.zip`; - // 4. Chiede all'utente il nome del file const nameAnswer = await inquirer.prompt([ { type: "input", @@ -74,7 +67,6 @@ async function createBackup(): Promise { const outputFileName: string = nameAnswer.fileName; const outputFilePath: string = path.join(projectRoot, outputFileName); - // 5. Gestione conferme if (fs.existsSync(outputFilePath)) { const overwriteAnswer = await inquirer.prompt([ { @@ -91,20 +83,6 @@ async function createBackup(): Promise { ); return; } - } else { - const confirmAnswer = await inquirer.prompt([ - { - type: "confirm", - name: "confirm", - message: `Confermi l'esecuzione del backup come '${outputFileName}'?`, - default: true, - }, - ]); - - if (!confirmAnswer.confirm) { - console.log("Operazione annullata dall'utente."); - return; - } } console.log(`\nInizio del processo di backup: ${outputFileName}`); @@ -112,7 +90,7 @@ async function createBackup(): Promise { const password = generatePassword(); const output = fs.createWriteStream(outputFilePath); const archive = archiver("zip-encryptable", { - zlib: { level: 9 }, // Imposta il livello di compressione + zlib: { level: 9 }, password: password, }); @@ -131,7 +109,6 @@ async function createBackup(): Promise { }, ); - // 6. Trova i file basandosi sulla selezione dell'utente console.log("Scansione dei file in corso..."); for (const item of selectedItems) { @@ -139,7 +116,6 @@ async function createBackup(): Promise { const isDirectory = fs.statSync(fullPath).isDirectory(); if (isDirectory) { - // Se è una cartella, usa glob per trovare tutti i file al suo interno const files: string[] = await glob(`${item}/**/*`, { cwd: projectRoot, nodir: true, @@ -151,7 +127,6 @@ async function createBackup(): Promise { archive.file(path.join(projectRoot, file), { name: file }); }); } else { - // Se è un file singolo archive.file(fullPath, { name: item }); } } @@ -179,8 +154,100 @@ async function createBackup(): Promise { await closePromise; } +/** + * Decomprime un file zip selezionato. + */ +async function extractBackup(projectRoot: string): Promise { + console.log("\n--- Modalità Decompressione ---"); + + const rootContent = fs.readdirSync(projectRoot); + const zipFiles = filterZipFiles(rootContent); + + if (zipFiles.length === 0) { + console.log("Nessun file .zip trovato nella cartella corrente."); + return; + } + + const selectionAnswer = await inquirer.prompt([ + { + type: "list", + name: "selectedZip", + message: "Seleziona il file .zip da decomprimere:", + choices: zipFiles, + }, + ]); + + const selectedZip = selectionAnswer.selectedZip; + const zipPath = path.join(projectRoot, selectedZip); + + const passwordAnswer = await inquirer.prompt([ + { + type: "input", + name: "password", + message: "Inserisci la password per il file zip:", + }, + ]); + + const password = passwordAnswer.password; + + const folderName = `extracted_${selectedZip.replace(".zip", "")}_${Date.now()}`; + const extractPath = path.join(projectRoot, folderName); + + console.log(`\nInizio estrazione in: ${folderName}...`); + + try { + const zip = new AdmZip(zipPath); + // Nota: extractAllTo richiede la password se i file sono cifrati + zip.extractAllTo(extractPath, true, false, password); + console.log("Estrazione completata con successo!"); + } catch (err: unknown) { + if (err instanceof Error) { + console.error( + "\nErrore durante l'estrazione:", + (err as Error).message || err, + ); + } else { + console.error("\nErrore sconosciuto durante l'estrazione:", err); + } + if ( + fs.existsSync(extractPath) && + fs.readdirSync(extractPath).length === 0 + ) { + fs.rmdirSync(extractPath); + } + } +} + +/** + * Funzione principale che gestisce il menu iniziale. + */ +async function main(): Promise { + console.log("=== Backup Script CLI ==="); + + // Utilizziamo process.cwd() per operare nella cartella dove viene lanciato il comando + const projectRoot: string = process.cwd(); + + const modeAnswer = await inquirer.prompt([ + { + type: "list", + name: "mode", + message: "Cosa desideri fare?", + choices: [ + { name: "Comprimere (Crea un backup ZIP)", value: "compress" }, + { name: "Decomprimere (Estrai un backup ZIP)", value: "decompress" }, + ], + }, + ]); + + if (modeAnswer.mode === "compress") { + await createBackup(projectRoot); + } else { + await extractBackup(projectRoot); + } +} + if (require.main === module) { - createBackup().catch((err: Error) => { - console.error("Errore durante la creazione del backup:", err); + main().catch((err: Error) => { + console.error("Errore fatale:", err); }); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 65cae75..c391e1f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -74,6 +74,13 @@ function prepareChoices( .sort((a, b) => a.name.localeCompare(b.name)); } +/** + * Filtra solo i file con estensione .zip. + */ +function filterZipFiles(content: string[]): string[] { + return content.filter((item) => item.toLowerCase().endsWith(".zip")); +} + module.exports = { formatFileName, validateSelection, @@ -81,4 +88,5 @@ module.exports = { generatePassword, formatProgress, prepareChoices, + filterZipFiles, }; diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 080f663..bab429b 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -5,6 +5,7 @@ const { filterRootContent, formatProgress, prepareChoices, + filterZipFiles, } = require("../src/utils/utils"); describe("Utility Functions", () => { @@ -109,4 +110,20 @@ describe("Utility Functions", () => { expect(choices[1].value).toBe("z.txt"); }); }); + + describe("filterZipFiles", () => { + test("should return only .zip files", () => { + const content = ["backup.zip", "data.txt", "archive.ZIP", "folder"]; + const result = filterZipFiles(content); + expect(result).toHaveLength(2); + expect(result).toContain("backup.zip"); + expect(result).toContain("archive.ZIP"); + }); + + test("should return empty array if no zip files are present", () => { + const content = ["data.txt", "folder"]; + const result = filterZipFiles(content); + expect(result).toEqual([]); + }); + }); });