Skip to content

Come lavorare con diversi filesystem

Node.js espone molte funzionalità dei filesystem. Ma non tutti i filesystem sono uguali. Di seguito sono riportate le migliori pratiche suggerite per mantenere il codice semplice e sicuro quando si lavora con diversi filesystem.

Comportamento del filesystem

Prima di poter lavorare con un filesystem, è necessario conoscere il suo comportamento. Diversi filesystem si comportano in modo diverso e hanno più o meno funzionalità rispetto ad altri: distinzione tra maiuscole e minuscole, insensibilità alle maiuscole e minuscole, preservazione delle maiuscole e minuscole, preservazione della forma Unicode, risoluzione dei timestamp, attributi estesi, inode, permessi Unix, flussi di dati alternativi, ecc.

Fate attenzione a dedurre il comportamento del filesystem da process.platform. Ad esempio, non date per scontato che, poiché il vostro programma è in esecuzione su Darwin, state quindi lavorando su un filesystem insensibile alle maiuscole e minuscole (HFS+), poiché l'utente potrebbe utilizzare un filesystem sensibile alle maiuscole e minuscole (HFSX). Allo stesso modo, non date per scontato che, poiché il vostro programma è in esecuzione su Linux, state quindi lavorando su un filesystem che supporta i permessi Unix e gli inode, poiché potreste trovarvi su una particolare unità esterna, un'unità USB o un'unità di rete che non li supporta.

Il sistema operativo potrebbe non rendere facile dedurre il comportamento del filesystem, ma non tutto è perduto. Invece di tenere un elenco di ogni filesystem e comportamento noto (che sarà sempre incompleto), è possibile sondare il filesystem per vedere come si comporta effettivamente. La presenza o l'assenza di determinate funzionalità che sono facili da sondare, sono spesso sufficienti per dedurre il comportamento di altre funzionalità che sono più difficili da sondare.

Ricordate che alcuni utenti potrebbero avere diversi filesystem montati in vari percorsi nell'albero di lavoro.

Evitare un approccio al minimo comune denominatore

Potreste essere tentati di far sì che il vostro programma si comporti come un filesystem al minimo comune denominatore, normalizzando tutti i nomi dei file in maiuscolo, normalizzando tutti i nomi dei file in forma Unicode NFC e normalizzando tutti i timestamp dei file a, diciamo, una risoluzione di 1 secondo. Questo sarebbe l'approccio al minimo comune denominatore.

Non fatelo. Potreste interagire in modo sicuro solo con un filesystem che ha le stesse caratteristiche del minimo comune denominatore in ogni aspetto. Non sareste in grado di lavorare con filesystem più avanzati nel modo in cui gli utenti si aspettano e si verificherebbero collisioni di nomi di file o timestamp. Perdereste e corrompereste sicuramente i dati dell'utente attraverso una serie di eventi dipendenti complicati e creereste bug che sarebbero difficili, se non impossibili, da risolvere.

Cosa succede quando in seguito è necessario supportare un filesystem che ha solo una risoluzione del timestamp di 2 secondi o 24 ore? Cosa succede quando lo standard Unicode avanza per includere un algoritmo di normalizzazione leggermente diverso (come è successo in passato)?

Un approccio al minimo comune denominatore tenderebbe a cercare di creare un programma portatile utilizzando solo chiamate di sistema "portabili". Questo porta a programmi che perdono dati e che non sono di fatto portabili.

Adottare un Approccio Superset

Ottimizzate al massimo ogni piattaforma supportata adottando un approccio superset. Ad esempio, un programma di backup portatile dovrebbe sincronizzare correttamente i btimes (il tempo di creazione di un file o di una cartella) tra sistemi Windows e non dovrebbe distruggere o alterare i btimes, anche se i btimes non sono supportati sui sistemi Linux. Lo stesso programma di backup portatile dovrebbe sincronizzare correttamente le autorizzazioni Unix tra sistemi Linux e non dovrebbe distruggere o alterare le autorizzazioni Unix, anche se le autorizzazioni Unix non sono supportate sui sistemi Windows.

Gestisci diversi filesystem facendo in modo che il tuo programma si comporti come un filesystem più avanzato. Supporta un superset di tutte le funzionalità possibili: distinzione tra maiuscole e minuscole, conservazione della maiuscola/minuscola, sensibilità alla forma Unicode, conservazione della forma Unicode, autorizzazioni Unix, timestamp ad alta risoluzione in nanosecondi, attributi estesi, ecc.

Una volta che hai la conservazione della maiuscola/minuscola nel tuo programma, puoi sempre implementare l'insensibilità alla maiuscola/minuscola se hai bisogno di interagire con un filesystem insensibile alla maiuscola/minuscola. Ma se rinunci alla conservazione della maiuscola/minuscola nel tuo programma, non puoi interagire in modo sicuro con un filesystem che la preserva. Lo stesso vale per la conservazione della forma Unicode e la conservazione della risoluzione del timestamp.

Se un filesystem ti fornisce un nome file in una combinazione di maiuscole e minuscole, conserva il nome file nel caso esatto fornito. Se un filesystem ti fornisce un nome file in forma Unicode mista o NFC o NFD (o NFKC o NFKD), conserva il nome file nella sequenza di byte esatta fornita. Se un filesystem ti fornisce un timestamp in millisecondi, conserva il timestamp con una risoluzione in millisecondi.

Quando lavori con un filesystem meno potente, puoi sempre effettuare un downsampling appropriato, con funzioni di confronto come richiesto dal comportamento del filesystem su cui è in esecuzione il tuo programma. Se sai che il filesystem non supporta le autorizzazioni Unix, non dovresti aspettarti di leggere le stesse autorizzazioni Unix che scrivi. Se sai che il filesystem non preserva la distinzione tra maiuscole e minuscole, dovresti essere preparato a vedere ABC in un elenco di directory quando il tuo programma crea abc. Ma se sai che il filesystem preserva la distinzione tra maiuscole e minuscole, dovresti considerare ABC come un nome file diverso da abc, quando rilevi rinominazioni di file o se il filesystem è sensibile alla distinzione tra maiuscole e minuscole.

Conservazione della Case

Potresti creare una directory chiamata test /abc e sorprenderti nel vedere a volte che fs.readdir('test') restituisce ['ABC']. Questo non è un bug di Node. Node restituisce il nome del file così come è memorizzato dal filesystem, e non tutti i filesystem supportano la conservazione della case. Alcuni filesystem convertono tutti i nomi dei file in maiuscolo (o minuscolo).

Conservazione della Forma Unicode

La conservazione della case e la conservazione della forma Unicode sono concetti simili. Per capire perché la forma Unicode dovrebbe essere preservata, assicurati di capire prima perché la case dovrebbe essere preservata. La conservazione della forma Unicode è altrettanto semplice quando si comprende correttamente. Unicode può codificare gli stessi caratteri usando diverse sequenze di byte. Diverse stringhe possono apparire uguali, ma avere sequenze di byte diverse. Quando si lavora con stringhe UTF-8, fai attenzione che le tue aspettative siano in linea con il funzionamento di Unicode. Proprio come non ti aspetteresti che tutti i caratteri UTF-8 vengano codificati in un singolo byte, non dovresti aspettarti che diverse stringhe UTF-8 che appaiono uguali all'occhio umano abbiano la stessa rappresentazione in byte. Questa potrebbe essere un'aspettativa che puoi avere di ASCII, ma non di UTF-8.

Potresti creare una directory chiamata test/ café (forma Unicode NFC con sequenza di byte <63 61 66 c3 a9> e string.length ===5) e sorprenderti nel vedere a volte che fs.readdir('test') restituisce ['café'] (forma Unicode NFD con sequenza di byte <63 61 66 65 cc 81> e string.length ===6). Questo non è un bug di Node. Node.js restituisce il nome del file così come è memorizzato dal filesystem, e non tutti i filesystem supportano la conservazione della forma Unicode. HFS+, per esempio, normalizzerà tutti i nomi dei file in una forma quasi sempre uguale alla forma NFD. Non aspettarti che HFS+ si comporti allo stesso modo di NTFS o EXT 4 e viceversa. Non cercare di modificare i dati in modo permanente tramite la normalizzazione come astrazione leaky per risolvere le differenze Unicode tra i filesystem. Questo creerebbe problemi senza risolverne nessuno. Piuttosto, preserva la forma Unicode e usa la normalizzazione solo come funzione di confronto.

Insensibilità alla Forma Unicode

L'insensibilità alla forma Unicode e la preservazione della forma Unicode sono due comportamenti diversi del filesystem spesso confusi tra loro. Proprio come l'insensibilità al maiuscolo/minuscolo è stata a volte implementata erroneamente normalizzando permanentemente i nomi dei file in maiuscolo durante l'archiviazione e la trasmissione dei nomi dei file, così l'insensibilità alla forma Unicode è stata a volte implementata erroneamente normalizzando permanentemente i nomi dei file in una certa forma Unicode (NFD nel caso di HFS+) durante l'archiviazione e la trasmissione dei nomi dei file. È possibile e molto meglio implementare l'insensibilità alla forma Unicode senza sacrificare la preservazione della forma Unicode, utilizzando la normalizzazione Unicode solo per il confronto.

Confronto di Diverse Forme Unicode

Node.js fornisce string.normalize ('NFC' / 'NFD') che puoi usare per normalizzare una stringa UTF-8 in NFC o NFD. Non dovresti mai memorizzare l'output di questa funzione, ma usarla solo come parte di una funzione di confronto per verificare se due stringhe UTF-8 apparirebbero uguali all'utente. Puoi usare string1.normalize('NFC')=== string2.normalize('NFC') o string1.normalize('NFD')=== string2.normalize('NFD') come funzione di confronto. La forma che usi non importa.

La normalizzazione è veloce, ma potresti voler usare una cache come input alla tua funzione di confronto per evitare di normalizzare la stessa stringa molte volte. Se la stringa non è presente nella cache, normalizzala e memorizzala nella cache. Fai attenzione a non memorizzare o persistere la cache, usala solo come cache.

Si noti che l'utilizzo di normalize () richiede che la tua versione di Node.js includa ICU (altrimenti normalize () restituirà semplicemente la stringa originale). Se scarichi l'ultima versione di Node.js dal sito web, includerà ICU.

Risoluzione Timestamp

Potresti impostare l'mtime (il tempo modificato) di un file su 1444291759414 (risoluzione in millisecondi) e sorprenderti nel vedere a volte che fs.stat restituisce il nuovo mtime come 1444291759000 (risoluzione di 1 secondo) o 1444291758000 (risoluzione di 2 secondi). Questo non è un bug in Node. Node.js restituisce il timestamp come lo memorizza il filesystem, e non tutti i filesystem supportano la risoluzione del timestamp in nanosecondi, millisecondi o 1 secondo. Alcuni filesystem hanno persino una risoluzione molto grossolana per il timestamp atime in particolare, ad esempio 24 ore per alcuni filesystem FAT.

Non danneggiare nomi file e timestamp tramite normalizzazione

I nomi file e i timestamp sono dati utente. Proprio come non riscriveresti mai automaticamente i dati dei file utente per mettere i dati in maiuscolo o normalizzare le terminazioni di riga CRLF in LF, così non dovresti mai cambiare, interferire o danneggiare i nomi file o i timestamp tramite normalizzazione di maiuscole/minuscole, forma Unicode/risoluzione timestamp. La normalizzazione dovrebbe essere utilizzata solo per il confronto, mai per alterare i dati.

La normalizzazione è effettivamente un codice hash con perdita di informazioni. Puoi usarla per verificare determinati tipi di equivalenza (ad esempio, diverse stringhe sembrano uguali anche se hanno sequenze di byte diverse), ma non puoi mai usarla come sostituto dei dati effettivi. Il tuo programma dovrebbe passare i dati di nome file e timestamp così come sono.

Il tuo programma può creare nuovi dati in NFC (o in qualsiasi combinazione di forme Unicode che preferisce) o con un nome file in minuscolo o maiuscolo, o con un timestamp a risoluzione di 2 secondi, ma il tuo programma non dovrebbe danneggiare i dati utente esistenti imponendo la normalizzazione di maiuscole/minuscole, forma Unicode/risoluzione timestamp. Piuttosto, adotta un approccio superset e conserva la distinzione tra maiuscole e minuscole, la forma Unicode e la risoluzione del timestamp nel tuo programma. In questo modo, sarai in grado di interagire in modo sicuro con i filesystem che fanno lo stesso.

Utilizzare correttamente le funzioni di confronto di normalizzazione

Assicurati di utilizzare correttamente le funzioni di confronto di maiuscole/minuscole, forma Unicode/risoluzione timestamp. Non utilizzare una funzione di confronto del nome file non sensibile alle maiuscole se si sta lavorando su un filesystem sensibile alle maiuscole. Non utilizzare una funzione di confronto non sensibile alla forma Unicode se si sta lavorando su un filesystem sensibile alla forma Unicode (ad esempio, NTFS e la maggior parte dei filesystem Linux che preservano sia NFC che NFD o forme Unicode miste). Non confrontare i timestamp a risoluzione di 2 secondi se si sta lavorando su un filesystem a risoluzione di timestamp di nanosecondi.

Essere preparati a lievi differenze nelle funzioni di confronto

Fai attenzione che le tue funzioni di confronto corrispondano a quelle del filesystem (o sonda il filesystem se possibile per vedere come confronterebbe effettivamente). La non sensibilità alle maiuscole, ad esempio, è più complessa di un semplice confronto toLowerCase(). Infatti, toUpperCase() è di solito migliore di toLowerCase() (poiché gestisce in modo diverso alcuni caratteri di lingue straniere). Ma sarebbe ancora meglio sondare il filesystem poiché ogni filesystem ha la propria tabella di confronto delle maiuscole integrata.

Ad esempio, HFS+ di Apple normalizza i nomi file in forma NFD, ma questa forma NFD è in realtà una versione precedente dell'attuale forma NFD e a volte può essere leggermente diversa dalla forma NFD dello standard Unicode più recente. Non aspettarti che HFS+ NFD sia sempre esattamente uguale a Unicode NFD.