JavaScript è stato a lungo uno dei linguaggi di scripting più popolari, ma per un lungo periodo di tempo non è stato una grande scelta per lo sviluppo di applicazioni backend lato server. Poi è arrivato Node.js, che viene utilizzato per creare applicazioni leggere, guidate dagli eventi e sviluppate lato server utilizzando il linguaggio di programmazione JavaScript.

Node.js è un runtime JavaScript open-source che può essere scaricato e installato gratuitamente su tutti i principali sistemi operativi (Windows, Mac, Linux). Negli ultimi anni è diventato sempre più popolare tra i creatori di app e ha fornito molte nuove opportunità di lavoro per gli sviluppatori JavaScript in cerca di una specializzazione.

In questo articolo scopriremo come gestire il file system utilizzando Node.js. È facile utilizzare le API di Node.js per interagire con il file system ed eseguire operazioni complesse.

Cominciamo!

Prerequisiti per Comprendere il File System di Node.js

Il prerequisito principale è l’installazione di Node.js sul sistema operativo. Node.js non richiede un hardware avanzato, per cui è facile scaricare e installare Node.js sul proprio computer.

Per lavorare sui moduli di Node.js come i file system (noti anche come “FS” o “fs”) è utile avere una conoscenza di base di JavaScript. Una conoscenza di alto livello delle funzioni, delle callback e delle promises di JavaScript aiuterà a capire ancora più velocemente l’argomento.

Modulo File System di Node.js

Lavorare con file e directory è una delle esigenze fondamentali di un’applicazione full-stack. Gli utenti dell’app potrebbero voler caricare immagini, curriculum o altri file su un server. Allo stesso tempo, l’applicazione potrebbe aver bisogno di leggere i file di configurazione, spostare i file o addirittura modificarne i permessi in modo programmatico.

Il modulo del file system di Node.js si occupa di tutti questi aspetti. Fornisce diverse API per interagire con i file system senza problemi. La maggior parte delle API sono personalizzabili con opzioni e flag. È anche possibile utilizzarle per eseguire operazioni sia sincrone che asincrone sui file.

Prima di approfondire il modulo del file system, diamo una sbirciatina al modulo di Node.js.

I Moduli di Node.js

I moduli di Node.js sono un insieme di funzionalità disponibili come API che possono essere utilizzate da un programma utente. Ad esempio, esiste il modulo fs per interagire con il file system. Allo stesso modo, un modulo http utilizza le sue funzioni per creare un server ed eseguire molte altre operazioni. Node.js offre numerosi moduli per astrarre molte funzionalità di basso livello.

È anche possibile creare i propri moduli. Con Node.js versione 14 e successive, potete creare e utilizzare i moduli Node.js in due modi: Moduli CommonJS (CJS) e moduli ESM (MJS). Tutti gli esempi che vedremo in questo articolo sono in stile CJS.

Lavorare con i File in Node.js

Lavorare con i file comporta diverse operazioni con i file e le directory (cartelle). Ora conosceremo ognuna di queste operazioni con un esempio di codice sorgente. Aprite il vostro editor di codice preferito e provatele mentre leggete.

Per prima cosa, importate il modulo fs nel vostro file sorgente per iniziare a lavorare con i metodi del file system. Nello stile CJS, utilizziamo il metodo require per importare un metodo da un modulo. Quindi, per importare e utilizzare i metodi del modulo fs, dovete procedere in questo modo:

const { writeFile } = require('fs/promises');

Si noti che stiamo importando il metodo writeFile dal pacchetto fs/promises. Vogliamo utilizzare i metodi promeses perché sono i più recenti e sono facili da usare con le parole chiave async/await e richiedono meno codice. Alternative sono i metodi sincroni e le funzioni di callback che vedremo più avanti.

Come Creare e Scrivere su un File

Potete creare e scrivere su un file in tre modi:

  1. Con il metodo writeFile
  2. Con il metodo appendFile
  3. Con il metodo openFile

Questi metodi accettano il percorso di un file e i dati da scrivere nel file. Se il file esiste, sostituiscono il contenuto del file. Altrimenti, creano un nuovo file con il contenuto.

1. Il Metodo writeFile

Il frammento di codice che segue mostra come utilizzare il metodo writeFile. Iniziate creando un file chiamato createFile.js e inserite il frammento di codice sottostante:

const { writeFile } = require('fs/promises');
async function writeToFile(fileName, data) {
  try {
    await writeFile(fileName, data);
    console.log(`Wrote data to ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to write the file: ${error.message}`);
  }
}

Nota che stiamo usando la parola chiave await per invocare il metodo che restituisce una promessa JavaScript. Una promessa andata a buon fine creerà/scriverà il file. Abbiamo un blocco try-catch per gestire gli errori nel caso in cui la promessa venga rifiutata.

Ora possiamo invocare la funzione writeToFile:

writeToFile('friends.txt', 'Bob');

Quindi, aprite un prompt dei comandi o un terminale ed eseguite il programma di cui sopra utilizzando il seguente comando:

node createFile.js

Verrà creato un nuovo file chiamato friends.txt con una riga che dice semplicemente:

Bob

2. Il Metodo appendFile

Come dice il nome, questo metodo viene utilizzato principalmente per aggiungere e modificare un file. Tuttavia, potete utilizzare lo stesso metodo anche per creare un file.

Date un’occhiata alla funzione qui sotto. Utilizziamo il metodo appendFile con il flag w per scrivere un file. Il flag predefinito per l’aggiunta a un file è a:

const { appendFile} = require('fs/promises');

async function appendToFile(fileName, data) {
  try {
    await appendFile(fileName, data, { flag: 'w' });
    console.log(`Appended data to ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to append the file: {error.message}`);
  }
}

Ora potete richiamare la funzione di cui sopra in questo modo:

appendToFile('activities.txt', 'Skiing');

Successivamente, potete eseguire il codice nell’ambiente Node.js utilizzando il comando node, come abbiamo visto in precedenza. In questo modo verrà creato un file chiamato activities.txt con il contenuto Skiing.

3. Il Metodo open

L’ultimo metodo che utilizzeremo per creare e scrivere su un file è il metodo open. Potete aprire un file utilizzando il flag w (per “write”):

const { open} = require('fs/promises');

async function openFile(fileName, data) {
  try {
    const file = await open(fileName, 'w');
    await file.write(data);
    console.log(`Opened file ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to open the file: {error.message}`);
  }
}

Ora richiamate la funzione openFile con:

openFile('tasks.txt', 'Do homework');

Quando eseguirete lo script con il comando node, verrà creato un file chiamato tasks.txt con il contenuto iniziale:

Do homework

Come Leggere un File

Ora che sappiamo come creare e scrivere su un file, vediamo come leggerne il contenuto. Per farlo, utilizzeremo il metodo readFile del modulo del file system.

Create un file chiamato readThisFile.js con il seguente codice:

// readThisFile.js
const { readFile } = require('fs/promises');
async function readThisFile(filePath) {
  try {
    const data = await readFile(filePath);
    console.log(data.toString());
  } catch (error) {
    console.error(`Got an error trying to read the file: {error.message}`);
 }
}

Ora leggiamo tutti e tre i file che abbiamo creato invocando la funzione readThisFile:

readThisFile('activities.txt');
readThisFile('friends.txt');
readThisFile('tasks.txt');

Infine, eseguite lo script utilizzando il seguente comando node:

node readThisFile.js

Dovreste vedere il seguente output nella console:

Skiing
Do homework
Bob

Un punto da notare: Il metodo readFile legge il file in modo asincrono. Ciò significa che l’ordine in cui viene letto il file e l’ordine in cui viene stampata la risposta nella console potrebbero non essere gli stessi. Bisogna usare la versione sincrona del metodo readFile per ottenere l’ordine. Lo vedremo tra poco.

Come Rinominare un File

Per rinominare un file, usate il metodo rename del modulo fs. Creiamo un file chiamato rename-me.txt. Rinomineremo questo file in modo programmatico.

Create un file chiamato renameFile.js con il seguente codice:

const { rename } = require('fs/promises');

async function renameFile(from, to) {
  try {
    await rename(from, to);
    console.log(`Renamed ${from} to ${to}`);
  } catch (error) {
    console.error(`Got an error trying to rename the file: ${error.message}`);
  }
}

Come avrete notato, il metodo rename richiede due argomenti. Uno è il file con il nome di origine e l’altro è il nome di destinazione.

Ora invochiamo la funzione di cui sopra per rinominare il file:

const oldName = "rename-me.txt";
const newName = "renamed.txt";
renameFile(oldName, newName);

Come in precedenza, eseguite il file di script utilizzando il comando node per rinominare il file:

node renameFile.js

Come Spostare un File

Spostare un file da una directory all’altra è come rinominarne il percorso. Quindi, possiamo utilizzare il metodo rename per spostare i file.

Creiamo due cartelle, from e to. Poi creeremo un file chiamato move-me.txt all’interno della cartella from.

Successivamente, scriveremo il codice per spostare il file move-me.txt. Create un file chiamato moveFile.js con il seguente frammento:

const { rename } = require('fs/promises');
const { join } = require('path');
async function moveFile(from, to) {
  try {
    await rename(from, to);
    console.log(`Moved ${from} to ${to}`);
  } catch (error) {
    console.error(`Got an error trying to move the file: ${error.message}`);
  }
}

Qui stiamo usando il metodo rename proprio come prima. Ma perché dobbiamo importare il metodo join dal modulo path (sì, il percorso è un altro modulo fondamentale di Node.js)?

Il metodo join viene utilizzato per unire diversi segmenti di percorso specificati per formare un unico percorso. Lo useremo per formare il percorso dei nomi dei file di origine e di destinazione:

const fromPath = join(__dirname, "from", "move-me.txt");
const destPath = join(__dirname, "to", "move-me.txt");
moveFile(fromPath, destPath);

Ed ecco fatto! Se eseguite lo script moveFile.js, vedrete il file move-me.txt spostato nella cartella to.

Come Copiare un File

Utilizziamo il metodo copyFile del modulo fs per copiare un file dall’origine alla destinazione.

Date un’occhiata al frammento di codice qui sotto:

const { copyFile } = require('fs/promises');
const { join } = require('path');
async function copyAFile(from, to) {
  try {
    await copyFile(from, to);
    console.log(`Copied ${from} to ${to}`);
  } catch (err) {
    console.error(`Got an error trying to copy the file: ${err.message}`);
  }
}

Ora potete invocare la funzione di cui sopra con:

copyAFile('friends.txt', 'friends-copy.txt');

Questo copierà il contenuto del file friends.txt nel file friends-copy.txt.

È fantastico, ma come si fa a copiare più file?

Potete utilizzare l’API Promise.all per eseguire più promesse in parallelo:

async function copyAll(fromDir, toDir, filePaths) {
  return Promise.all(filePaths.map(filePath => {
   return copyAFile(join(fromDir, filePath), join(toDir, filePath));
  }));
}

Ora potete fornire tutti i percorsi dei file da copiare da una directory all’altra:

copyFiles('from', 'to', ['copyA.txt', 'copyB.txt']);

Potete seguire questo approccio anche per eseguire altre operazioni come lo spostamento, la scrittura e la lettura di file in parallelo.

Come Eliminare un File

Per eliminare un file utilizziamo il metodo unlink:

const { unlink } = require('fs/promises');
async function deleteFile(filePath) {
  try {
    await unlink(filePath);
    console.log(`Deleted ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to delete the file: ${error.message}`);
  }
}

Ricordate che dovrete fornire il percorso del file per eliminarlo:

deleteFile('delete-me.txt');

Se il percorso è un link simbolico a un altro file, il metodo unlink cancellerà il link simbolico, ma il file originale non verrà toccato. Parleremo più avanti dei link simbolici.

Come Cambiare i Permessi e la Proprietà dei File

A volte potreste voler cambiare i permessi dei file in modo programmatico. Può essere molto utile per rendere un file di sola lettura o completamente accessibile.

Utilizzeremo il metodo chmod per modificare i permessi di un file:

const { chmod } = require('fs/promises');
async function changePermission(filePath, permission) {
  try {
    await chmod(filePath, permission);
    console.log(`Changed permission to ${permission} for ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to change permission: ${error.message}`);
  }
}

Possiamo passare il percorso del file e la bitmask del permesso per modificare il permesso.

Ecco la chiamata alla funzione per cambiare i permessi di un file in sola lettura:

changePermission('permission.txt', 0o400);

Analogamente ai permessi, è possibile anche cambiare la proprietà di un file in modo programmatico. Per farlo utilizziamo il metodo chown:

const { chown } = require('fs/promises');

async function changeOwnership(filePath, userId, groupId) {
  try {
    await chown(filePath, userId, groupId);
    console.log(`Changed ownership to ${userId}:${groupId} for ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to change ownership: ${error.message}`);
  }
}

Quindi invochiamo la funzione con il percorso del file, l’ID utente e l’ID gruppo:

changeOwnership('ownership.txt', 1000, 1010);

Come Creare un Link Simbolico

Il collegamento simbolico (noto anche come symlink) è un concetto del filesystem che permette di creare un collegamento a un file o a una cartella. I link simbolici vengono creati per creare dei collegamenti a un file/cartella di destinazione nel file system. Il modulo Node.js filesystem fornisce il metodo symlink per creare un link simbolico.

Per creare il link simbolico, dobbiamo passare il percorso del file di destinazione, il percorso del file attuale e il tipo:

const { symlink } = require('fs/promises');
const { join } = require('path');
async function createSymlink(target, path, type) {
  try {
    await symlink(target, path, type);
    console.log(`Created symlink to ${target} at ${path}`);
  } catch (error) {
    console.error(`Got an error trying to create the symlink: ${error.message}`);
  }
}

Possiamo invocare la funzione con:

createSymlink('join(__dirname, from, symMe.txt)', 'symToFile', 'file');

Qui abbiamo creato un link simbolico chiamato symToFile.

Come Osservare le Modifiche a un File

Sapevate che è possibile osservare le modifiche apportate a un file? È un ottimo modo per monitorare le modifiche e gli eventi, soprattutto quando non ve li aspettate. Potete catturare e controllare queste modifiche per rivederle in seguito.

Il metodo watch è il modo migliore per osservare le modifiche di un file. Esiste un metodo alternativo chiamato watchFile, ma non è così performante come il metodo watch.

Finora abbiamo utilizzato il metodo del modulo del file system con le parole chiave async/await. Vediamo come usare la funzione di callback con questo esempio.

Il metodo watch accetta come argomenti il percorso del file e una funzione di callback. Ogni volta che viene eseguita un’attività sul file, viene richiamata la funzione callback.

Possiamo sfruttare il parametro event per ottenere maggiori informazioni sulle attività:

const fs = require('fs');
function watchAFile(file) {
  fs.watch(file, (event, filename) => {
    console.log(`${filename} file Changed`);
  });
}

Invocate la funzione passando il nome del file a watch:

watchAFile('friends.txt');

Ora stiamo catturando automaticamente tutte le attività sul file friends.txt.

Lavorare con le Directory (Cartelle) in Node.js

Vediamo ora come eseguire operazioni sulle directory o sulle cartelle. Molte operazioni come rinominare, spostare e copiare sono simili a quelle già viste per i file. Tuttavia, alcuni metodi e operazioni specifiche sono utilizzabili solo sulle directory.

Come Creare una Directory

Per creare una directory utilizziamo il metodo mkdir. È necessario passare il nome della directory come argomento:

const { mkdir } = require('fs/promises');
async function createDirectory(path) {
  try {
    await mkdir(path);
    console.log(`Created directory ${path}`);
  } catch (error) {
    console.error(`Got an error trying to create the directory: ${error.message}`);
  }
}

Ora possiamo invocare la funzione createDirectory con un percorso di directory:

createDirectory('new-directory');

In questo modo verrà creata una directory denominata new-directory.

Come Creare una Directory Temporanea

Le directory temporanee non sono directory normali. Hanno un significato speciale per il sistema operativo. Potete creare una directory temporanea utilizzando il metodo mkdtemp().

Creiamo una cartella temporanea all’interno della directory temporanea del sistema operativo. Otteniamo le informazioni sulla posizione della directory temporanea dal metodo tmpdir() del modulo os:

const { mkdtemp } = require('fs/promises');
const { join } = require('path');
const { tmpdir } = require('os');
async function createTemporaryDirectory(fileName) {
  try {
    const tempDirectory = await mkdtemp(join(tmpdir(), fileName));
    console.log(`Created temporary directory ${tempDirectory}`);
  } catch (error) {
    console.error(`Got an error trying to create the temporary directory: ${error.message}`);
  }
}

Ora invochiamo la funzione con il nome della cartella per crearla:

createTemporaryDirectory('node-temp-file-');

Node.js aggiungerà sei caratteri casuali alla fine del nome della cartella temporanea creata per mantenerlo unico.

Come Eliminare una Cartella

Per rimuovere/eliminare una directory bisogna utilizzare il metodo rmdir():

const { rmdir } = require('fs/promises');
async function deleteDirectory(path) {
  try {
    await rmdir(path);
    console.log(`Deleted directory ${path}`);
  } catch (error) {
    console.error(`Got an error trying to delete the directory: ${error.message}`);
  }
}

Quindi, richiamate la funzione di cui sopra passando il percorso della cartella che volete rimuovere:

deleteDirectory('new-directory-renamed');

API sincrone e asincrone

Finora abbiamo visto molti esempi di metodi per il file system e tutti sono stati utilizzati in modo asincrono. Tuttavia, potreste aver bisogno di gestire alcune operazioni in modo sincrono.

Un esempio di operazione sincrona è la lettura di più file uno dopo l’altro. Il modulo fs ha un metodo chiamato readFileSync() per eseguire questa operazione:

const { readFileSync } = require('fs');
function readFileSynchronously(path) {
  try {
    const data = readFileSync(path);
    console.log(data.toString());
  } catch (error) {
    console.error(error);
  }
}

Il metodo readFileSync() non è richiesto dal pacchetto “fs/promises”. Questo perché il metodo non è asincrono. Quindi, potete chiamare la funzione di cui sopra con:

readFileSynchronously('activities.txt');
readFileSynchronously('friends.txt');
readFileSynchronously('tasks.txt');

In questo caso, tutti i file verranno letti nell’ordine in cui sono state chiamate le funzioni.

Il modulo del file system di Node.js offre un metodo sincrono per altre operazioni come quella di lettura. Utilizzateli con attenzione e solo in caso di necessità. I metodi asincroni sono molto più utili per l’esecuzione parallela.

Gestire gli Errori

Come ogni coder sa bene, bisogna aspettarsi sempre degli errori ed essere pronti a gestirli quando si eseguono operazioni su un file o una directory. Cosa succede se il file non viene trovato o se non avete il permesso di scrivere su un file? Ci possono essere (e probabilmente ci saranno) molti casi in cui potreste imbattervi in un errore.

Dovreste sempre racchiudere le vostre chiamate di metodo in un blocco try-catch. In questo modo, se si verifica un errore, il controllo passerà al blocco catch, dove potrete esaminare e gestire l’errore. Come avrete notato in tutti gli esempi precedenti, abbiamo utilizzato il blocco try-catch per gestire gli errori che abbiamo incontrato.

Riepilogo

Rivediamo i punti chiave che abbiamo trattato in questo tutorial:

  • Il modulo del file system (fs) di Node.js ha molti metodi che aiutano a svolgere molte attività di basso livello.
  • È possibile eseguire diverse operazioni sui file come creare, scrivere, rinominare, copiare, spostare, cancellare e molte altre.
  • È possibile eseguire diverse operazioni sulle directory, come creare, creare una directory temporanea, spostare e molte altre.
  • Tutti i metodi possono essere invocati in modo asincrono utilizzando promesse JavaScript o funzioni di callback.
  • Se necessario, è anche possibile invocare i metodi in modo sincrono.
  • Preferite sempre i metodi asincroni a quelli sincroni.
  • Gestite gli errori con un blocco try-catch ogni volta che interagisci con i metodi.

Ora che abbiamo lavorato un po’ con il file system di Node.js, dovreste avere una buona padronanza delle sue caratteristiche. Se volete migliorare ulteriormente il vostro know-how, potreste approfondire gli stream di Node.js come naturale evoluzione dell’apprendimento dei moduli di Node.js. Gli stream permettono di gestire lo scambio di informazioni, comprese le chiamate di rete, la lettura/scrittura di file e molto altro ancora.

Troverete tutto il codice sorgente utilizzato in questo articolo in questo repository GitHub.

Avete intenzione di utilizzare Node.js per il vostro prossimo progetto? Diteci perché l’hai scelto nella sezione dei commenti qui sotto.