Travailler avec différents systèmes de fichiers
Node.js expose de nombreuses fonctionnalités des systèmes de fichiers. Mais tous les systèmes de fichiers ne sont pas identiques. Voici les meilleures pratiques suggérées pour garder votre code simple et sûr lorsque vous travaillez avec différents systèmes de fichiers.
Comportement du système de fichiers
Avant de pouvoir travailler avec un système de fichiers, vous devez connaître son comportement. Les différents systèmes de fichiers se comportent différemment et ont plus ou moins de fonctionnalités que d'autres : sensibilité à la casse, insensibilité à la casse, préservation de la casse, préservation de la forme Unicode, résolution des horodatages, attributs étendus, inodes, permissions Unix, flux de données alternatifs, etc.
Méfiez-vous de l'inférence du comportement du système de fichiers à partir de process.platform
. Par exemple, ne supposez pas que parce que votre programme s'exécute sur Darwin, vous travaillez donc sur un système de fichiers insensible à la casse (HFS+), car l'utilisateur peut utiliser un système de fichiers sensible à la casse (HFSX). De même, ne supposez pas que parce que votre programme s'exécute sur Linux, vous travaillez donc sur un système de fichiers qui prend en charge les permissions Unix et les inodes, car vous pouvez être sur un disque dur externe, une clé USB ou un lecteur réseau particulier qui ne le fait pas.
Le système d'exploitation peut ne pas faciliter l'inférence du comportement du système de fichiers, mais tout n'est pas perdu. Au lieu de garder une liste de chaque système de fichiers et comportement connu (ce qui sera toujours incomplet), vous pouvez sonder le système de fichiers pour voir comment il se comporte réellement. La présence ou l'absence de certaines fonctionnalités faciles à sonder suffit souvent à inférer le comportement d'autres fonctionnalités plus difficiles à sonder.
N'oubliez pas que certains utilisateurs peuvent avoir différents systèmes de fichiers montés à différents chemins dans l'arborescence de travail.
Éviter une approche du plus petit dénominateur commun
Vous pourriez être tenté de faire en sorte que votre programme se comporte comme un système de fichiers du plus petit dénominateur commun, en normalisant tous les noms de fichiers en majuscules, en normalisant tous les noms de fichiers au format Unicode NFC et en normalisant tous les horodatages des fichiers à une résolution de 1 seconde par exemple. Ce serait l'approche du plus petit dénominateur commun.
Ne faites pas ça. Vous ne seriez en mesure d'interagir en toute sécurité qu'avec un système de fichiers ayant exactement les mêmes caractéristiques du plus petit dénominateur commun à tous égards. Vous ne seriez pas en mesure de travailler avec des systèmes de fichiers plus avancés comme les utilisateurs le souhaitent, et vous rencontreriez des collisions de noms de fichiers ou d'horodatages. Vous perdriez et corrompriez certainement les données de l'utilisateur via une série d'événements dépendants complexes, et vous créeriez des bogues qui seraient difficiles, voire impossibles à résoudre.
Que se passe-t-il lorsque vous devez plus tard prendre en charge un système de fichiers qui n'a qu'une résolution d'horodatage de 2 secondes ou de 24 heures ? Que se passe-t-il lorsque la norme Unicode évolue pour inclure un algorithme de normalisation légèrement différent (comme cela s'est produit par le passé) ?
Une approche du plus petit dénominateur commun tendrait à essayer de créer un programme portable en utilisant uniquement des appels système « portables ». Cela conduit à des programmes qui fuient et qui ne sont en fait pas portables.
Adopter une approche sur-ensemble
Tirez le meilleur parti de chaque plateforme que vous supportez en adoptant une approche sur-ensemble. Par exemple, un programme de sauvegarde portable doit synchroniser correctement les btimes (l'heure de création d'un fichier ou d'un dossier) entre les systèmes Windows, et ne doit pas détruire ou modifier les btimes, même si les btimes ne sont pas supportés sur les systèmes Linux. Le même programme de sauvegarde portable doit synchroniser correctement les permissions Unix entre les systèmes Linux, et ne doit pas détruire ou modifier les permissions Unix, même si les permissions Unix ne sont pas supportées sur les systèmes Windows.
Gérez les différents systèmes de fichiers en faisant en sorte que votre programme agisse comme un système de fichiers plus avancé. Supportez un sur-ensemble de toutes les fonctionnalités possibles : sensibilité à la casse, préservation de la casse, sensibilité à la forme Unicode, préservation de la forme Unicode, permissions Unix, horodatages nanosecondes haute résolution, attributs étendus, etc.
Une fois que vous avez la préservation de la casse dans votre programme, vous pouvez toujours implémenter l'insensibilité à la casse si vous devez interagir avec un système de fichiers insensible à la casse. Mais si vous renoncez à la préservation de la casse dans votre programme, vous ne pouvez pas interagir en toute sécurité avec un système de fichiers préservant la casse. Il en va de même pour la préservation de la forme Unicode et la préservation de la résolution des horodatages.
Si un système de fichiers vous fournit un nom de fichier dans un mélange de minuscules et de majuscules, conservez le nom de fichier dans la casse exacte donnée. Si un système de fichiers vous fournit un nom de fichier dans une forme Unicode mixte ou NFC ou NFD (ou NFKC ou NFKD), conservez le nom de fichier dans la séquence d'octets exacte donnée. Si un système de fichiers vous fournit un horodatage en millisecondes, conservez l'horodatage en résolution milliseconde.
Lorsque vous travaillez avec un système de fichiers moins performant, vous pouvez toujours effectuer un sous-échantillonnage approprié, avec des fonctions de comparaison comme requis par le comportement du système de fichiers sur lequel votre programme s'exécute. Si vous savez que le système de fichiers ne prend pas en charge les permissions Unix, vous ne devez pas vous attendre à lire les mêmes permissions Unix que vous écrivez. Si vous savez que le système de fichiers ne préserve pas la casse, vous devez vous préparer à voir ABC
dans une liste de répertoires lorsque votre programme crée abc
. Mais si vous savez que le système de fichiers préserve la casse, vous devez considérer ABC
comme un nom de fichier différent de abc
, lors de la détection de renommages de fichiers ou si le système de fichiers est sensible à la casse.
Préservation de la casse
Vous pouvez créer un répertoire appelé test /abc
et être surpris de voir parfois que fs.readdir('test')
retourne ['ABC']
. Ce n'est pas un bug dans Node. Node renvoie le nom de fichier tel qu'il est stocké par le système de fichiers, et tous les systèmes de fichiers ne prennent pas en charge la préservation de la casse. Certains systèmes de fichiers convertissent tous les noms de fichiers en majuscules (ou en minuscules).
Préservation de la forme Unicode
La préservation de la casse et la préservation de la forme Unicode sont des concepts similaires. Pour comprendre pourquoi la forme Unicode doit être préservée, assurez-vous d'abord de comprendre pourquoi la casse doit être préservée. La préservation de la forme Unicode est tout aussi simple lorsqu'elle est correctement comprise. Unicode peut encoder les mêmes caractères en utilisant plusieurs séquences d'octets différentes. Plusieurs chaînes peuvent sembler identiques, mais avoir des séquences d'octets différentes. Lorsque vous travaillez avec des chaînes UTF-8, assurez-vous que vos attentes correspondent à la manière dont fonctionne Unicode. Tout comme vous ne vous attendriez pas à ce que tous les caractères UTF-8 soient codés sur un seul octet, vous ne devriez pas vous attendre à ce que plusieurs chaînes UTF-8 qui semblent identiques à l'œil humain aient la même représentation en octets. Ceci peut être une attente que vous pouvez avoir d'ASCII, mais pas d'UTF-8.
Vous pouvez créer un répertoire appelé test/ café
(forme Unicode NFC avec la séquence d'octets <63 61 66 c3 a9>
et string.length ===5
) et être surpris de voir parfois que fs.readdir('test')
retourne ['café']
(forme Unicode NFD avec la séquence d'octets <63 61 66 65 cc 81>
et string.length ===6
). Ce n'est pas un bug dans Node. Node.js renvoie le nom de fichier tel qu'il est stocké par le système de fichiers, et tous les systèmes de fichiers ne prennent pas en charge la préservation de la forme Unicode. HFS+, par exemple, normalisera tous les noms de fichiers sous une forme presque toujours identique à la forme NFD. Ne vous attendez pas à ce que HFS+ se comporte de la même manière que NTFS ou EXT 4 et vice-versa. N'essayez pas de modifier les données de manière permanente via la normalisation comme une abstraction défaillante pour masquer les différences Unicode entre les systèmes de fichiers. Cela créerait des problèmes sans en résoudre aucun. Au contraire, préservez la forme Unicode et utilisez la normalisation uniquement comme fonction de comparaison.
Insensibilité à la forme Unicode
L'insensibilité à la forme Unicode et la préservation de la forme Unicode sont deux comportements de système de fichiers différents souvent confondus. Tout comme l'insensibilité à la casse a parfois été mal implémentée en normalisant en permanence les noms de fichiers en majuscules lors du stockage et de la transmission des noms de fichiers, l'insensibilité à la forme Unicode a parfois été mal implémentée en normalisant en permanence les noms de fichiers à une certaine forme Unicode (NFD dans le cas de HFS+) lors du stockage et de la transmission des noms de fichiers. Il est possible et bien meilleur d'implémenter l'insensibilité à la forme Unicode sans sacrifier la préservation de la forme Unicode, en utilisant la normalisation Unicode uniquement pour la comparaison.
Comparaison de différentes formes Unicode
Node.js fournit string.normalize ('NFC' / 'NFD')
que vous pouvez utiliser pour normaliser une chaîne UTF-8 en NFC ou NFD. Vous ne devez jamais stocker la sortie de cette fonction, mais l'utiliser uniquement dans le cadre d'une fonction de comparaison pour tester si deux chaînes UTF-8 seraient identiques pour l'utilisateur. Vous pouvez utiliser string1.normalize('NFC')=== string2.normalize('NFC')
ou string1.normalize('NFD')=== string2.normalize('NFD')
comme fonction de comparaison. La forme que vous utilisez n'a pas d'importance.
La normalisation est rapide, mais vous pouvez utiliser un cache en entrée de votre fonction de comparaison pour éviter de normaliser plusieurs fois la même chaîne. Si la chaîne n'est pas présente dans le cache, normalisez-la et mettez-la en cache. Veillez à ne pas stocker ou persister le cache, utilisez-le uniquement comme cache.
Notez que l'utilisation de normalize ()
nécessite que votre version de Node.js inclue ICU (sinon normalize ()
renverra simplement la chaîne originale). Si vous téléchargez la dernière version de Node.js depuis le site Web, elle inclura ICU.
Résolution des horodatages
Vous pouvez définir le mtime (le temps de modification) d'un fichier sur 1444291759414 (résolution en millisecondes) et être surpris de constater parfois que fs.stat
renvoie le nouveau mtime comme étant 1444291759000 (résolution en 1 seconde) ou 1444291758000 (résolution en 2 secondes). Ce n'est pas un bug dans Node. Node.js renvoie l'horodatage tel que le système de fichiers le stocke, et tous les systèmes de fichiers ne prennent pas en charge la résolution des horodatages en nanosecondes, en millisecondes ou en 1 seconde. Certains systèmes de fichiers ont même une résolution très grossière pour l'horodatage atime en particulier, par exemple 24 heures pour certains systèmes de fichiers FAT.
Ne pas corrompre les noms de fichiers et les horodatages par la normalisation
Les noms de fichiers et les horodatages sont des données utilisateur. Tout comme vous ne réécririez jamais automatiquement les données de fichier utilisateur pour mettre les données en majuscules ou normaliser les terminaisons de ligne CRLF en LF, vous ne devez jamais modifier, interférer ou corrompre les noms de fichiers ou les horodatages par la normalisation de la casse/de la forme Unicode/de l'horodatage. La normalisation ne doit être utilisée que pour la comparaison, jamais pour modifier les données.
La normalisation est en effet un code de hachage avec perte d'information. Vous pouvez l'utiliser pour tester certains types d'équivalence (par exemple, plusieurs chaînes semblent-elles identiques même si elles ont des séquences d'octets différentes), mais vous ne pouvez jamais l'utiliser en remplacement des données réelles. Votre programme doit transmettre les données de nom de fichier et d'horodatage telles quelles.
Votre programme peut créer de nouvelles données en NFC (ou dans toute combinaison de formes Unicode qu'il préfère) ou avec un nom de fichier en minuscules ou en majuscules, ou avec un horodatage à la résolution de 2 secondes, mais votre programme ne doit pas corrompre les données utilisateur existantes en imposant une normalisation de la casse/de la forme Unicode/de l'horodatage. Adoptez plutôt une approche sur-ensemble et préservez la casse, la forme Unicode et la résolution de l'horodatage dans votre programme. De cette façon, vous pourrez interagir en toute sécurité avec les systèmes de fichiers qui font de même.
Utiliser correctement les fonctions de comparaison de normalisation
Assurez-vous d'utiliser correctement les fonctions de comparaison de casse/de forme Unicode/d'horodatage. N'utilisez pas une fonction de comparaison de nom de fichier insensible à la casse si vous travaillez sur un système de fichiers sensible à la casse. N'utilisez pas une fonction de comparaison insensible à la forme Unicode si vous travaillez sur un système de fichiers sensible à la forme Unicode (par exemple, NTFS et la plupart des systèmes de fichiers Linux qui conservent à la fois les formes NFC et NFD ou les formes Unicode mixtes). Ne comparez pas les horodatages à une résolution de 2 secondes si vous travaillez sur un système de fichiers à résolution d'horodatage nanoseconde.
Être préparé aux légères différences dans les fonctions de comparaison
Veillez à ce que vos fonctions de comparaison correspondent à celles du système de fichiers (ou interrogez le système de fichiers si possible pour voir comment il comparerait réellement). L'insensibilité à la casse, par exemple, est plus complexe qu'une simple comparaison toLowerCase()
. En fait, toUpperCase()
est généralement meilleur que toLowerCase()
(car il gère différemment certains caractères de langues étrangères). Mais il serait encore mieux d'interroger le système de fichiers, car chaque système de fichiers a sa propre table de comparaison de casse intégrée.
Par exemple, HFS+ d'Apple normalise les noms de fichiers au format NFD, mais ce format NFD est en fait une ancienne version du format NFD actuel et peut parfois être légèrement différent du format NFD de la dernière norme Unicode. Ne vous attendez pas à ce que le NFD HFS+ soit exactement le même que le NFD Unicode tout le temps.