Cómo Trabajar con Diferentes Sistemas de Archivos
Node.js expone muchas características de los sistemas de archivos. Pero no todos los sistemas de archivos son iguales. Las siguientes son las mejores prácticas sugeridas para mantener su código simple y seguro cuando trabaje con diferentes sistemas de archivos.
Comportamiento del Sistema de Archivos
Antes de poder trabajar con un sistema de archivos, necesita saber cómo se comporta. Los diferentes sistemas de archivos se comportan de manera diferente y tienen más o menos características que otros: distinción entre mayúsculas y minúsculas, insensibilidad a mayúsculas y minúsculas, preservación de mayúsculas y minúsculas, preservación de la forma Unicode, resolución de marcas de tiempo, atributos extendidos, inodos, permisos de Unix, flujos de datos alternativos, etc.
Tenga cuidado al inferir el comportamiento del sistema de archivos a partir de process.platform
. Por ejemplo, no asuma que porque su programa se está ejecutando en Darwin, está trabajando en un sistema de archivos que no distingue entre mayúsculas y minúsculas (HFS+), ya que el usuario puede estar usando un sistema de archivos que sí distingue entre mayúsculas y minúsculas (HFSX). De manera similar, no asuma que porque su programa se está ejecutando en Linux, está trabajando en un sistema de archivos que admite permisos e inodos de Unix, ya que podría estar en una unidad externa, USB o unidad de red particular que no los admita.
Es posible que el sistema operativo no facilite la inferencia del comportamiento del sistema de archivos, pero no todo está perdido. En lugar de mantener una lista de todos los sistemas de archivos conocidos y su comportamiento (que siempre estará incompleta), puede sondear el sistema de archivos para ver cómo se comporta realmente. La presencia o ausencia de ciertas características que son fáciles de sondear, a menudo son suficientes para inferir el comportamiento de otras características que son más difíciles de sondear.
Recuerde que algunos usuarios pueden tener diferentes sistemas de archivos montados en varias rutas en el árbol de trabajo.
Evita un Enfoque de Mínimo Común Denominador
Podrías sentirte tentado a hacer que tu programa actúe como un sistema de archivos de mínimo común denominador, normalizando todos los nombres de archivo a mayúsculas, normalizando todos los nombres de archivo a la forma Unicode NFC y normalizando todas las marcas de tiempo de archivos, digamos, a una resolución de 1 segundo. Este sería el enfoque de mínimo común denominador.
No hagas esto. Solo podrías interactuar de forma segura con un sistema de archivos que tenga exactamente las mismas características de mínimo común denominador en todos los aspectos. No podrías trabajar con sistemas de archivos más avanzados de la manera que los usuarios esperan, y te encontrarías con colisiones de nombres de archivo o marcas de tiempo. Lo más probable es que pierdas y corrompas los datos de usuario a través de una serie de eventos dependientes complicados, y crearías errores que serían difíciles, si no imposibles, de resolver.
¿Qué sucede cuando más tarde necesitas admitir un sistema de archivos que solo tiene una resolución de marca de tiempo de 2 segundos o 24 horas? ¿Qué sucede cuando el estándar Unicode avanza para incluir un algoritmo de normalización ligeramente diferente (como ha sucedido en el pasado)?
Un enfoque de mínimo común denominador tendería a intentar crear un programa portátil utilizando solo llamadas al sistema "portátiles". Esto lleva a programas que tienen fugas y que, de hecho, no son portátiles.
Adopta un enfoque de superconjunto
Aprovecha al máximo cada plataforma que admites adoptando un enfoque de superconjunto. Por ejemplo, un programa de copia de seguridad portátil debería sincronizar correctamente los btimes (la hora de creación de un archivo o carpeta) entre los sistemas Windows y no debería destruir ni alterar los btimes, aunque los btimes no se admitan en los sistemas Linux. El mismo programa de copia de seguridad portátil debería sincronizar correctamente los permisos de Unix entre los sistemas Linux y no debería destruir ni alterar los permisos de Unix, aunque los permisos de Unix no se admitan en los sistemas Windows.
Gestiona los diferentes sistemas de archivos haciendo que tu programa actúe como un sistema de archivos más avanzado. Admite un superconjunto de todas las características posibles: distinción entre mayúsculas y minúsculas, conservación de mayúsculas y minúsculas, sensibilidad a la forma Unicode, conservación de la forma Unicode, permisos de Unix, marcas de tiempo de nanosegundos de alta resolución, atributos extendidos, etc.
Una vez que tengas la conservación de mayúsculas y minúsculas en tu programa, siempre puedes implementar la no distinción entre mayúsculas y minúsculas si necesitas interactuar con un sistema de archivos que no distingue entre mayúsculas y minúsculas. Pero si renuncias a la conservación de mayúsculas y minúsculas en tu programa, no puedes interactuar de forma segura con un sistema de archivos que conserva las mayúsculas y minúsculas. Lo mismo ocurre con la conservación de la forma Unicode y la conservación de la resolución de la marca de tiempo.
Si un sistema de archivos te proporciona un nombre de archivo en una mezcla de minúsculas y mayúsculas, mantén el nombre de archivo en el caso exacto dado. Si un sistema de archivos te proporciona un nombre de archivo en forma Unicode mixta o NFC o NFD (o NFKC o NFKD), mantén el nombre de archivo en la secuencia de bytes exacta dada. Si un sistema de archivos te proporciona una marca de tiempo en milisegundos, mantén la marca de tiempo en resolución de milisegundos.
Cuando trabajes con un sistema de archivos inferior, siempre puedes submuestrear de forma adecuada, con las funciones de comparación requeridas por el comportamiento del sistema de archivos en el que se está ejecutando tu programa. Si sabes que el sistema de archivos no admite permisos de Unix, no deberías esperar leer los mismos permisos de Unix que escribes. Si sabes que el sistema de archivos no conserva las mayúsculas y minúsculas, debes estar preparado para ver ABC
en un listado de directorios cuando tu programa cree abc
. Pero si sabes que el sistema de archivos sí conserva las mayúsculas y minúsculas, deberías considerar que ABC
es un nombre de archivo diferente a abc
, al detectar el cambio de nombre de archivos o si el sistema de archivos distingue entre mayúsculas y minúsculas.
Preservación de Mayúsculas/Minúsculas
Puedes crear un directorio llamado test /abc
y sorprenderte al ver que a veces fs.readdir('test')
devuelve ['ABC']
. Esto no es un error en Node. Node devuelve el nombre de archivo tal como lo almacena el sistema de archivos, y no todos los sistemas de archivos admiten la preservación de mayúsculas y minúsculas. Algunos sistemas de archivos convierten todos los nombres de archivo a mayúsculas (o minúsculas).
Preservación de la Forma Unicode
La preservación de mayúsculas/minúsculas y la preservación de la forma Unicode son conceptos similares. Para entender por qué se debe preservar la forma Unicode, asegúrate primero de entender por qué se deben preservar las mayúsculas/minúsculas. La preservación de la forma Unicode es igual de sencilla cuando se entiende correctamente. Unicode puede codificar los mismos caracteres utilizando varias secuencias de bytes diferentes. Varias cadenas pueden verse iguales, pero tienen diferentes secuencias de bytes. Cuando trabajes con cadenas UTF-8, ten cuidado de que tus expectativas estén en línea con cómo funciona Unicode. Así como no esperarías que todos los caracteres UTF-8 se codifiquen en un solo byte, tampoco deberías esperar que varias cadenas UTF-8 que se ven iguales al ojo humano tengan la misma representación de bytes. Esta puede ser una expectativa que puedas tener de ASCll, pero no de UTF-8.
Puedes crear un directorio llamado test/ café
(forma Unicode NFC con secuencia de bytes <63 61 66 c3 a9>
y string.length === 5
) y sorprenderte al ver que a veces fs.readdir('test')
devuelve ['café']
(forma Unicode NFD con secuencia de bytes <63 61 66 65 cc 81>
y string.length === 6
). Esto no es un error en Node. Node.js devuelve el nombre de archivo tal como lo almacena el sistema de archivos, y no todos los sistemas de archivos admiten la preservación de la forma Unicode. HFS+, por ejemplo, normalizará todos los nombres de archivo a una forma casi siempre igual a la forma NFD. No esperes que HFS+ se comporte igual que NTFS o EXT 4 y viceversa. No intentes cambiar los datos permanentemente mediante la normalización como una abstracción con fugas para encubrir las diferencias Unicode entre los sistemas de archivos. Esto crearía problemas sin resolver ninguno. Más bien, preserva la forma Unicode y utiliza la normalización solo como una función de comparación.
Insensibilidad a la Forma Unicode
La insensibilidad a la forma Unicode y la preservación de la forma Unicode son dos comportamientos diferentes del sistema de archivos que a menudo se confunden entre sí. Al igual que la insensibilidad a mayúsculas y minúsculas a veces se ha implementado incorrectamente normalizando permanentemente los nombres de archivo a mayúsculas al almacenar y transmitir nombres de archivo, la insensibilidad a la forma Unicode a veces se ha implementado incorrectamente normalizando permanentemente los nombres de archivo a una cierta forma Unicode (NFD en el caso de HFS+) al almacenar y transmitir nombres de archivo. Es posible y mucho mejor implementar la insensibilidad a la forma Unicode sin sacrificar la preservación de la forma Unicode, utilizando la normalización Unicode solo para la comparación.
Comparación de Diferentes Formas Unicode
Node.js proporciona string.normalize ('NFC' / 'NFD')
que puedes usar para normalizar una cadena UTF-8 a NFC o NFD. Nunca debes almacenar la salida de esta función, sino que solo debes usarla como parte de una función de comparación para probar si dos cadenas UTF-8 se verían iguales para el usuario. Puedes usar string1.normalize('NFC')=== string2.normalize('NFC')
o string1.normalize('NFD')=== string2.normalize('NFD')
como tu función de comparación. No importa qué forma utilices.
La normalización es rápida, pero es posible que desees utilizar una caché como entrada para tu función de comparación para evitar normalizar la misma cadena muchas veces. Si la cadena no está presente en la caché, normalízala y almacénala en la caché. Ten cuidado de no almacenar ni persistir la caché, úsala solo como una caché.
Ten en cuenta que el uso de normalize()
requiere que tu versión de Node.js incluya ICU (de lo contrario, normalize()
simplemente devolverá la cadena original). Si descargas la última versión de Node.js del sitio web, incluirá ICU.
Resolución de la marca de tiempo
Puede establecer el mtime (el tiempo de modificación) de un archivo en 1444291759414 (resolución en milisegundos) y sorprenderse al ver a veces que fs.stat
devuelve el nuevo mtime como 1444291759000 (resolución de 1 segundo) o 1444291758000 (resolución de 2 segundos). Esto no es un error en Node. Node.js devuelve la marca de tiempo tal como la almacena el sistema de archivos, y no todos los sistemas de archivos admiten una resolución de marca de tiempo en nanosegundos, milisegundos o 1 segundo. Algunos sistemas de archivos incluso tienen una resolución muy gruesa para la marca de tiempo atime en particular, por ejemplo, 24 horas para algunos sistemas de archivos FAT.
No corrompa los nombres de archivo y las marcas de tiempo a través de la normalización
Los nombres de archivo y las marcas de tiempo son datos de usuario. Al igual que nunca reescribiría automáticamente los datos de un archivo de usuario para poner en mayúsculas los datos o normalizar CRLF a terminaciones de línea LF, nunca debe cambiar, interferir o corromper los nombres de archivo o las marcas de tiempo mediante la normalización de mayúsculas/minúsculas / forma Unicode / marca de tiempo. La normalización solo debe utilizarse para la comparación, nunca para alterar los datos.
La normalización es efectivamente un código hash con pérdida. Puede usarlo para probar ciertos tipos de equivalencia (por ejemplo, ¿varias cadenas se ven igual aunque tengan secuencias de bytes diferentes?), pero nunca puede usarlo como sustituto de los datos reales. Su programa debe pasar los datos del nombre de archivo y la marca de tiempo tal como están.
Su programa puede crear nuevos datos en NFC (o en cualquier combinación de forma Unicode que prefiera) o con un nombre de archivo en minúsculas o mayúsculas, o con una marca de tiempo de resolución de 2 segundos, pero su programa no debe corromper los datos de usuario existentes imponiendo la normalización de mayúsculas/minúsculas / forma Unicode / marca de tiempo. En su lugar, adopte un enfoque de superconjunto y conserve las mayúsculas y minúsculas, la forma Unicode y la resolución de la marca de tiempo en su programa. De esa manera, podrá interactuar de forma segura con los sistemas de archivos que hacen lo mismo.
Utilice las funciones de comparación de normalización de forma adecuada
Asegúrese de que utiliza las funciones de comparación de mayúsculas y minúsculas / forma Unicode / marca de tiempo de forma adecuada. No utilice una función de comparación de nombres de archivo que no distinga entre mayúsculas y minúsculas si está trabajando en un sistema de archivos que distingue entre mayúsculas y minúsculas. No utilice una función de comparación que no distinga entre formas Unicode si está trabajando en un sistema de archivos que distingue entre formas Unicode (por ejemplo, NTFS y la mayoría de los sistemas de archivos Linux, que conservan tanto NFC como NFD o formas Unicode mixtas). No compare las marcas de tiempo con una resolución de 2 segundos si está trabajando en un sistema de archivos con una resolución de marca de tiempo de nanosegundos.
Esté preparado para pequeñas diferencias en las funciones de comparación
Tenga cuidado de que sus funciones de comparación coincidan con las del sistema de archivos (o sondee el sistema de archivos si es posible para ver cómo se compararía realmente). La falta de distinción entre mayúsculas y minúsculas, por ejemplo, es más compleja que una simple comparación toLowerCase()
. De hecho, toUpperCase()
suele ser mejor que toLowerCase()
(ya que maneja ciertos caracteres de idiomas extranjeros de forma diferente). Pero aún mejor sería sondear el sistema de archivos, ya que cada sistema de archivos tiene su propia tabla de comparación de mayúsculas y minúsculas incorporada.
Como ejemplo, el HFS+ de Apple normaliza los nombres de archivo a la forma NFD, pero esta forma NFD es en realidad una versión anterior de la forma NFD actual y, a veces, puede ser ligeramente diferente de la forma NFD del último estándar Unicode. No espere que HFS+ NFD sea exactamente igual que Unicode NFD todo el tiempo.