Wie man mit verschiedenen Dateisystemen arbeitet
Node.js stellt viele Funktionen der Dateisysteme bereit. Aber nicht alle Dateisysteme sind gleich. Die folgenden sind empfohlene Best Practices, um Ihren Code einfach und sicher zu halten, wenn Sie mit verschiedenen Dateisystemen arbeiten.
Verhalten von Dateisystemen
Bevor Sie mit einem Dateisystem arbeiten können, müssen Sie wissen, wie es sich verhält. Unterschiedliche Dateisysteme verhalten sich unterschiedlich und haben mehr oder weniger Funktionen als andere: Groß-/Kleinschreibungsempfindlichkeit, Groß-/Kleinschreibungsunempfindlichkeit, Beibehaltung der Groß-/Kleinschreibung, Beibehaltung der Unicode-Form, Zeitstempelauflösung, erweiterte Attribute, Inodes, Unix-Berechtigungen, alternative Datenströme usw.
Hüten Sie sich davor, das Verhalten des Dateisystems von process.platform
abzuleiten. Nehmen Sie zum Beispiel nicht an, dass Sie, weil Ihr Programm auf Darwin läuft, daher an einem nicht-case-sensitiven Dateisystem (HFS+) arbeiten, da der Benutzer möglicherweise ein case-sensitives Dateisystem (HFSX) verwendet. Nehmen Sie auch nicht an, dass Sie, weil Ihr Programm unter Linux läuft, daher mit einem Dateisystem arbeiten, das Unix-Berechtigungen und Inodes unterstützt, da Sie sich möglicherweise auf einem bestimmten externen Laufwerk, USB- oder Netzlaufwerk befinden, das dies nicht tut.
Das Betriebssystem macht es möglicherweise nicht einfach, das Verhalten des Dateisystems abzuleiten, aber es ist nicht alles verloren. Anstatt eine Liste aller bekannten Dateisysteme und Verhaltensweisen zu führen (die immer unvollständig sein wird), können Sie das Dateisystem untersuchen, um zu sehen, wie es sich tatsächlich verhält. Das Vorhandensein oder Fehlen bestimmter Funktionen, die leicht zu untersuchen sind, reicht oft aus, um das Verhalten anderer Funktionen abzuleiten, die schwieriger zu untersuchen sind.
Denken Sie daran, dass einige Benutzer unterschiedliche Dateisysteme unter verschiedenen Pfaden im Arbeitsverzeichnis eingebunden haben können.
Vermeiden Sie einen Ansatz des kleinsten gemeinsamen Nenners
Sie könnten versucht sein, Ihr Programm wie ein Dateisystem des kleinsten gemeinsamen Nenners zu verhalten, indem Sie alle Dateinamen in Großbuchstaben normalisieren, alle Dateinamen in NFC-Unicode-Form normalisieren und alle Dateizeitstempel auf eine Auflösung von z. B. 1 Sekunde normalisieren. Dies wäre der Ansatz des kleinsten gemeinsamen Nenners.
Tun Sie dies nicht. Sie könnten nur sicher mit einem Dateisystem interagieren, das in jeder Hinsicht genau die gleichen Merkmale des kleinsten gemeinsamen Nenners aufweist. Sie wären nicht in der Lage, mit fortgeschritteneren Dateisystemen so zu arbeiten, wie es Benutzer erwarten, und Sie würden auf Dateinamen- oder Zeitstempelkollisionen stoßen. Sie würden mit Sicherheit Benutzerdaten durch eine Reihe komplizierter, abhängiger Ereignisse verlieren und beschädigen, und Sie würden Fehler erzeugen, die schwierig, wenn nicht gar unmöglich zu lösen wären.
Was passiert, wenn Sie später ein Dateisystem unterstützen müssen, das nur eine 2-Sekunden- oder 24-Stunden-Zeitstempelauflösung hat? Was passiert, wenn der Unicode-Standard fortschreitet, um einen etwas anderen Normalisierungsalgorithmus einzuschließen (wie es in der Vergangenheit geschehen ist)?
Ein Ansatz des kleinsten gemeinsamen Nenners würde tendenziell versuchen, ein portables Programm zu erstellen, indem nur "portable" Systemaufrufe verwendet werden. Dies führt zu Programmen, die lückenhaft und nicht wirklich portabel sind.
Einen Superset-Ansatz verfolgen
Nutzen Sie jede von Ihnen unterstützte Plattform optimal, indem Sie einen Superset-Ansatz verfolgen. Beispielsweise sollte ein portables Sicherungsprogramm die Btimes (Erstellungszeit einer Datei oder eines Ordners) zwischen Windows-Systemen korrekt synchronisieren und Btimes weder zerstören noch ändern, auch wenn Btimes auf Linux-Systemen nicht unterstützt werden. Dasselbe portable Sicherungsprogramm sollte Unix-Berechtigungen zwischen Linux-Systemen korrekt synchronisieren und Unix-Berechtigungen weder zerstören noch ändern, auch wenn Unix-Berechtigungen auf Windows-Systemen nicht unterstützt werden.
Behandeln Sie verschiedene Dateisysteme, indem Sie Ihr Programm wie ein fortgeschritteneres Dateisystem agieren lassen. Unterstützen Sie einen Superset aller möglichen Funktionen: Groß-/Kleinschreibung, Beibehaltung der Groß-/Kleinschreibung, Unicode-Formempfindlichkeit, Beibehaltung der Unicode-Form, Unix-Berechtigungen, hochauflösende Nanosekunden-Zeitstempel, erweiterte Attribute usw.
Sobald Sie die Beibehaltung der Groß-/Kleinschreibung in Ihrem Programm implementiert haben, können Sie jederzeit die Unempfindlichkeit gegenüber der Groß-/Kleinschreibung implementieren, wenn Sie mit einem Dateisystem interagieren müssen, das nicht zwischen Groß- und Kleinschreibung unterscheidet. Wenn Sie jedoch die Beibehaltung der Groß-/Kleinschreibung in Ihrem Programm aufgeben, können Sie nicht sicher mit einem Dateisystem interagieren, das die Groß-/Kleinschreibung beibehält. Das Gleiche gilt für die Beibehaltung der Unicode-Form und die Beibehaltung der Zeitstempelauflösung.
Wenn Ihnen ein Dateisystem einen Dateinamen in einer Mischung aus Klein- und Großbuchstaben bereitstellt, behalten Sie den Dateinamen in der exakt angegebenen Schreibweise bei. Wenn Ihnen ein Dateisystem einen Dateinamen in gemischter Unicode-Form oder NFC oder NFD (oder NFKC oder NFKD) bereitstellt, behalten Sie den Dateinamen in der exakten angegebenen Byte-Sequenz bei. Wenn Ihnen ein Dateisystem einen Millisekunden-Zeitstempel bereitstellt, behalten Sie den Zeitstempel in Millisekunden-Auflösung bei.
Wenn Sie mit einem geringeren Dateisystem arbeiten, können Sie jederzeit eine geeignete Downsampling durchführen, wobei Vergleichsfunktionen gemäß dem Verhalten des Dateisystems verwendet werden, auf dem Ihr Programm ausgeführt wird. Wenn Sie wissen, dass das Dateisystem keine Unix-Berechtigungen unterstützt, sollten Sie nicht erwarten, dieselben Unix-Berechtigungen zu lesen, die Sie schreiben. Wenn Sie wissen, dass das Dateisystem die Groß-/Kleinschreibung nicht beibehält, sollten Sie darauf vorbereitet sein, ABC
in einer Verzeichnisauflistung zu sehen, wenn Ihr Programm abc
erstellt. Wenn Sie jedoch wissen, dass das Dateisystem die Groß-/Kleinschreibung beibehält, sollten Sie ABC
als einen anderen Dateinamen als abc
betrachten, wenn Sie Dateiumbenennungen erkennen oder wenn das Dateisystem zwischen Groß- und Kleinschreibung unterscheidet.
Groß-/Kleinschreibungserhaltung
Sie können ein Verzeichnis namens test /abc
erstellen und überrascht sein, dass fs.readdir('test')
manchmal ['ABC']
zurückgibt. Dies ist kein Fehler in Node. Node gibt den Dateinamen so zurück, wie er vom Dateisystem gespeichert wird, und nicht alle Dateisysteme unterstützen die Erhaltung der Groß-/Kleinschreibung. Einige Dateisysteme konvertieren alle Dateinamen in Großbuchstaben (oder Kleinbuchstaben).
Erhaltung der Unicode-Form
Die Erhaltung der Groß-/Kleinschreibung und die Erhaltung der Unicode-Form sind ähnliche Konzepte. Um zu verstehen, warum die Unicode-Form erhalten bleiben sollte, stellen Sie sicher, dass Sie zuerst verstehen, warum die Groß-/Kleinschreibung erhalten bleiben sollte. Die Erhaltung der Unicode-Form ist genauso einfach, wenn sie richtig verstanden wird. Unicode kann dieselben Zeichen mit verschiedenen Byte-Sequenzen kodieren. Mehrere Zeichenketten können gleich aussehen, aber unterschiedliche Byte-Sequenzen haben. Wenn Sie mit UTF-8-Zeichenketten arbeiten, achten Sie darauf, dass Ihre Erwartungen mit der Funktionsweise von Unicode übereinstimmen. So wie Sie nicht erwarten würden, dass alle UTF-8-Zeichen in ein einzelnes Byte kodiert werden, sollten Sie nicht erwarten, dass mehrere UTF-8-Zeichenketten, die für das menschliche Auge gleich aussehen, die gleiche Byte-Darstellung haben. Dies kann eine Erwartung sein, die Sie von ASCII haben können, aber nicht von UTF-8.
Sie können ein Verzeichnis namens test/ café
erstellen (NFC-Unicode-Form mit Byte-Sequenz <63 61 66 c3 a9>
und string.length ===5
) und überrascht sein, dass fs.readdir('test')
manchmal ['café']
zurückgibt (NFD-Unicode-Form mit Byte-Sequenz <63 61 66 65 cc 81>
und string.length ===6
). Dies ist kein Fehler in Node. Node.js gibt den Dateinamen so zurück, wie er vom Dateisystem gespeichert wird, und nicht alle Dateisysteme unterstützen die Erhaltung der Unicode-Form. HFS+ zum Beispiel normalisiert alle Dateinamen in eine Form, die fast immer der NFD-Form entspricht. Erwarten Sie nicht, dass sich HFS+ genauso verhält wie NTFS oder EXT 4 und umgekehrt. Versuchen Sie nicht, Daten dauerhaft durch Normalisierung zu ändern, da dies eine undichte Abstraktion ist, um Unicode-Unterschiede zwischen Dateisystemen zu überdecken. Dies würde Probleme schaffen, ohne sie zu lösen. Erhalten Sie stattdessen die Unicode-Form und verwenden Sie die Normalisierung nur als Vergleichsfunktion.
Unicode-Formularunempfindlichkeit
Unicode-Formularunempfindlichkeit und Unicode-Formerhaltung sind zwei unterschiedliche Dateisystemverhalten, die oft miteinander verwechselt werden. So wie die Groß-/Kleinschreibungssensitivität manchmal fälschlicherweise implementiert wurde, indem Dateinamen beim Speichern und Übertragen von Dateinamen dauerhaft in Großbuchstaben normalisiert wurden, so wurde die Unicode-Formularunempfindlichkeit manchmal fälschlicherweise implementiert, indem Dateinamen beim Speichern und Übertragen von Dateinamen dauerhaft in eine bestimmte Unicode-Form (NFD im Fall von HFS+) normalisiert wurden. Es ist möglich und viel besser, die Unicode-Formularunempfindlichkeit zu implementieren, ohne die Unicode-Formerhaltung zu opfern, indem die Unicode-Normalisierung nur für den Vergleich verwendet wird.
Vergleichen verschiedener Unicode-Formen
Node.js bietet string.normalize ('NFC' / 'NFD')
, mit dem Sie eine UTF-8-Zeichenkette entweder in NFC oder NFD normalisieren können. Sie sollten die Ausgabe dieser Funktion niemals speichern, sondern sie nur als Teil einer Vergleichsfunktion verwenden, um zu testen, ob zwei UTF-8-Zeichenketten für den Benutzer gleich aussehen würden. Sie können string1.normalize('NFC')=== string2.normalize('NFC')
oder string1.normalize('NFD')=== string2.normalize('NFD')
als Vergleichsfunktion verwenden. Welche Form Sie verwenden, spielt keine Rolle.
Die Normalisierung ist schnell, aber Sie sollten einen Cache als Eingabe für Ihre Vergleichsfunktion verwenden, um zu vermeiden, dass dieselbe Zeichenkette viele Male normalisiert wird. Wenn die Zeichenkette nicht im Cache vorhanden ist, normalisieren Sie sie und speichern Sie sie im Cache. Achten Sie darauf, den Cache nicht zu speichern oder zu persistieren, verwenden Sie ihn nur als Cache.
Beachten Sie, dass die Verwendung von normalize()
voraussetzt, dass Ihre Version von Node.js ICU enthält (ansonsten gibt normalize()
nur die Originalzeichenkette zurück). Wenn Sie die neueste Version von Node.js von der Website herunterladen, ist ICU enthalten.
Zeitstempelauflösung
Sie können die mtime (die Änderungszeit) einer Datei auf 1444291759414 (Millisekundenauflösung) setzen und überrascht sein, manchmal zu sehen, dass fs.stat
die neue mtime als 1444291759000 (1-Sekunden-Auflösung) oder 1444291758000 (2-Sekunden-Auflösung) zurückgibt. Dies ist kein Fehler in Node. Node.js gibt den Zeitstempel so zurück, wie er vom Dateisystem gespeichert wird, und nicht alle Dateisysteme unterstützen Nanosekunden-, Millisekunden- oder 1-Sekunden-Zeitstempelauflösung. Einige Dateisysteme haben insbesondere für den atime-Zeitstempel eine sehr grobe Auflösung, z. B. 24 Stunden für einige FAT-Dateisysteme.
Dateinamen und Zeitstempel nicht durch Normalisierung beschädigen
Dateinamen und Zeitstempel sind Benutzerdaten. So wie Sie niemals automatisch Benutzerdateidaten in Großbuchstaben umschreiben oder CRLF-Zeilenenden in LF-Zeilenenden normalisieren würden, sollten Sie niemals Dateinamen oder Zeitstempel durch Groß-/Kleinschreibung, Unicode-Form- oder Zeitstempelnormalisierung ändern, stören oder beschädigen. Normalisierung sollte nur zum Vergleich verwendet werden, niemals zur Änderung von Daten.
Normalisierung ist effektiv ein verlustbehafteter Hash-Code. Sie können ihn verwenden, um auf bestimmte Arten von Äquivalenz zu testen (z. B. ob mehrere Zeichenketten gleich aussehen, obwohl sie unterschiedliche Byte-Sequenzen haben), aber Sie können ihn niemals als Ersatz für die tatsächlichen Daten verwenden. Ihr Programm sollte Dateinamen- und Zeitstempeldaten unverändert weitergeben.
Ihr Programm kann neue Daten in NFC (oder in einer beliebigen Kombination von Unicode-Formen, die es bevorzugt) oder mit einem Klein- oder Großbuchstabendateinamen oder mit einem Zeitstempel mit 2-Sekunden-Auflösung erstellen, aber Ihr Programm sollte vorhandene Benutzerdaten nicht durch die Erhebung von Groß-/Kleinschreibungs-, Unicode-Form- oder Zeitstempelnormalisierung beschädigen. Verwenden Sie stattdessen einen Obermenge-Ansatz und bewahren Sie die Groß-/Kleinschreibung, die Unicode-Form und die Zeitstempelauflösung in Ihrem Programm. Auf diese Weise können Sie sicher mit Dateisystemen interagieren, die dasselbe tun.
Normalisierungsvergleichsfunktionen angemessen verwenden
Stellen Sie sicher, dass Sie Funktionen zum Vergleich von Groß-/Kleinschreibung, Unicode-Form und Zeitstempel angemessen verwenden. Verwenden Sie keine Funktion zum Vergleich von Dateinamen, die die Groß-/Kleinschreibung nicht berücksichtigt, wenn Sie auf einem Dateisystem arbeiten, das die Groß-/Kleinschreibung beachtet. Verwenden Sie keine Vergleichsfunktion, die die Unicode-Form nicht berücksichtigt, wenn Sie auf einem Dateisystem arbeiten, das die Unicode-Form berücksichtigt (z. B. NTFS und die meisten Linux-Dateisysteme, die sowohl NFC- als auch NFD- oder gemischte Unicode-Formen beibehalten). Vergleichen Sie keine Zeitstempel mit einer 2-Sekunden-Auflösung, wenn Sie auf einem Dateisystem mit einer Nanosekunden-Zeitstempelauflösung arbeiten.
Seien Sie auf geringfügige Unterschiede in den Vergleichsfunktionen vorbereitet
Achten Sie darauf, dass Ihre Vergleichsfunktionen mit denen des Dateisystems übereinstimmen (oder fragen Sie das Dateisystem gegebenenfalls ab, um zu sehen, wie es tatsächlich vergleichen würde). Die Nichtbeachtung der Groß-/Kleinschreibung ist beispielsweise komplexer als ein einfacher toLowerCase()
-Vergleich. Tatsächlich ist toUpperCase()
normalerweise besser als toLowerCase()
(da es bestimmte fremdsprachliche Zeichen anders behandelt). Aber besser wäre es immer noch, das Dateisystem abzufragen, da jedes Dateisystem seine eigene Vergleichstabelle für die Groß-/Kleinschreibung enthält.
Als Beispiel normalisiert Apples HFS+ Dateinamen in die NFD-Form, aber diese NFD-Form ist eigentlich eine ältere Version der aktuellen NFD-Form und kann manchmal geringfügig von der NFD-Form des neuesten Unicode-Standards abweichen. Erwarten Sie nicht, dass HFS+ NFD immer genau dasselbe ist wie Unicode NFD.