Como Trabalhar com Diferentes Sistemas de Arquivos
O Node.js expõe muitas funcionalidades dos sistemas de arquivos. Mas nem todos os sistemas de arquivos são iguais. As melhores práticas a seguir são sugeridas para manter seu código simples e seguro ao trabalhar com diferentes sistemas de arquivos.
Comportamento do Sistema de Arquivos
Antes de poder trabalhar com um sistema de arquivos, você precisa saber como ele se comporta. Diferentes sistemas de arquivos se comportam de maneira diferente e têm mais ou menos recursos que outros: sensibilidade a maiúsculas e minúsculas, insensibilidade a maiúsculas e minúsculas, preservação de maiúsculas e minúsculas, preservação de forma Unicode, resolução de timestamp, atributos estendidos, inodes, permissões Unix, fluxos de dados alternativos etc.
Tome cuidado ao inferir o comportamento do sistema de arquivos de process.platform
. Por exemplo, não assuma que, porque seu programa está sendo executado no Darwin, você está, portanto, trabalhando em um sistema de arquivos insensível a maiúsculas e minúsculas (HFS+), pois o usuário pode estar usando um sistema de arquivos sensível a maiúsculas e minúsculas (HFSX). Da mesma forma, não assuma que, porque seu programa está sendo executado no Linux, você está, portanto, trabalhando em um sistema de arquivos que suporta permissões Unix e inodes, pois você pode estar em uma unidade externa, USB ou unidade de rede específica que não o faz.
O sistema operacional pode não facilitar a inferência do comportamento do sistema de arquivos, mas nem tudo está perdido. Em vez de manter uma lista de todos os sistemas de arquivos e comportamentos conhecidos (que sempre estará incompleta), você pode sondar o sistema de arquivos para ver como ele realmente se comporta. A presença ou ausência de certos recursos que são fáceis de sondar, muitas vezes são suficientes para inferir o comportamento de outros recursos que são mais difíceis de sondar.
Lembre-se de que alguns usuários podem ter diferentes sistemas de arquivos montados em vários caminhos na árvore de trabalho.
Evite uma Abordagem de Mínimo Múltiplo Comum
Você pode se sentir tentado a fazer seu programa agir como um sistema de arquivos de mínimo múltiplo comum, normalizando todos os nomes de arquivos para maiúsculas, normalizando todos os nomes de arquivos para a forma NFC Unicode e normalizando todos os timestamps de arquivos para, digamos, resolução de 1 segundo. Esta seria a abordagem do mínimo múltiplo comum.
Não faça isso. Você só seria capaz de interagir com segurança com um sistema de arquivos que tenha as mesmas características de mínimo múltiplo comum em todos os aspectos. Você não conseguiria trabalhar com sistemas de arquivos mais avançados da maneira que os usuários esperam, e você encontraria colisões de nomes de arquivos ou timestamps. Você certamente perderia e corromperia dados do usuário por meio de uma série de eventos dependentes complicados e criaria bugs que seriam difíceis, senão impossíveis, de resolver.
O que acontece quando você precisar mais tarde dar suporte a um sistema de arquivos que tenha apenas resolução de timestamp de 2 segundos ou 24 horas? O que acontece quando o padrão Unicode avança para incluir um algoritmo de normalização ligeiramente diferente (como aconteceu no passado)?
Uma abordagem de mínimo múltiplo comum tenderia a tentar criar um programa portátil usando apenas chamadas de sistema "portáteis". Isso leva a programas que são falhos e, de fato, não são portáteis.
Adote uma Abordagem de Superconjunto
Tire o máximo proveito de cada plataforma que você suporta adotando uma abordagem de superconjunto. Por exemplo, um programa de backup portátil deve sincronizar os horários de criação (o horário de criação de um arquivo ou pasta) corretamente entre sistemas Windows e não deve destruir ou alterar os horários de criação, mesmo que os horários de criação não sejam suportados em sistemas Linux. O mesmo programa de backup portátil deve sincronizar as permissões Unix corretamente entre sistemas Linux e não deve destruir ou alterar as permissões Unix, mesmo que as permissões Unix não sejam suportadas em sistemas Windows.
Lidere com diferentes sistemas de arquivos fazendo com que seu programa aja como um sistema de arquivos mais avançado. Suporte um superconjunto de todos os recursos possíveis: diferenciação de maiúsculas e minúsculas, preservação de maiúsculas e minúsculas, sensibilidade à forma Unicode, preservação da forma Unicode, permissões Unix, timestamps de nanossegundos de alta resolução, atributos estendidos etc.
Uma vez que você tenha a preservação de maiúsculas e minúsculas em seu programa, você sempre pode implementar a insensibilidade a maiúsculas e minúsculas se precisar interagir com um sistema de arquivos insensível a maiúsculas e minúsculas. Mas se você abrir mão da preservação de maiúsculas e minúsculas em seu programa, você não poderá interagir com segurança com um sistema de arquivos que preserva maiúsculas e minúsculas. O mesmo é verdade para a preservação da forma Unicode e a preservação da resolução de timestamp.
Se um sistema de arquivos fornecer a você um nome de arquivo em uma mistura de letras minúsculas e maiúsculas, mantenha o nome do arquivo exatamente no caso fornecido. Se um sistema de arquivos fornecer a você um nome de arquivo em forma Unicode mista ou NFC ou NFD (ou NFKC ou NFKD), mantenha o nome do arquivo na sequência de bytes exata fornecida. Se um sistema de arquivos fornecer a você um timestamp de milissegundo, mantenha o timestamp na resolução de milissegundo.
Quando você trabalha com um sistema de arquivos menor, você sempre pode fazer o downsample apropriadamente, com funções de comparação conforme necessário pelo comportamento do sistema de arquivos em que seu programa está sendo executado. Se você sabe que o sistema de arquivos não suporta permissões Unix, você não deve esperar ler as mesmas permissões Unix que você escreve. Se você sabe que o sistema de arquivos não preserva maiúsculas e minúsculas, você deve estar preparado para ver ABC
em uma listagem de diretórios quando seu programa criar abc
. Mas se você sabe que o sistema de arquivos preserva maiúsculas e minúsculas, você deve considerar ABC
como um nome de arquivo diferente de abc
, ao detectar renomeações de arquivos ou se o sistema de arquivos diferencia maiúsculas e minúsculas.
Preservação de Maiúsculas e Minúsculas
Você pode criar um diretório chamado test /abc
e se surpreender ao ver que às vezes fs.readdir('test')
retorna ['ABC']
. Isso não é um erro no Node.js. O Node.js retorna o nome do arquivo como o sistema de arquivos o armazena, e nem todos os sistemas de arquivos suportam a preservação de maiúsculas e minúsculas. Alguns sistemas de arquivos convertem todos os nomes de arquivos para maiúsculas (ou minúsculas).
Preservação da Forma Unicode
Preservação de maiúsculas e minúsculas e preservação da forma Unicode são conceitos similares. Para entender por que a forma Unicode deve ser preservada, certifique-se de que você primeiro entenda por que as maiúsculas e minúsculas devem ser preservadas. A preservação da forma Unicode é tão simples quanto quando compreendida corretamente. Unicode pode codificar os mesmos caracteres usando várias sequências de bytes diferentes. Várias strings podem parecer iguais, mas ter sequências de bytes diferentes. Ao trabalhar com strings UTF-8, tenha cuidado para que suas expectativas estejam alinhadas com o funcionamento do Unicode. Assim como você não esperaria que todos os caracteres UTF-8 codificassem para um único byte, você não deve esperar que várias strings UTF-8 que parecem iguais ao olho humano tenham a mesma representação de bytes. Esta pode ser uma expectativa que você pode ter de ASCII, mas não de UTF-8.
Você pode criar um diretório chamado test/ café
(forma NFC Unicode com sequência de bytes <63 61 66 c3 a9>
e string.length ===5
) e se surpreender ao ver que às vezes fs.readdir('test')
retorna ['café']
(forma NFD Unicode com sequência de bytes <63 61 66 65 cc 81>
e string.length ===6
). Isso não é um erro no Node.js. O Node.js retorna o nome do arquivo como o sistema de arquivos o armazena, e nem todos os sistemas de arquivos suportam a preservação da forma Unicode. O HFS+, por exemplo, normalizará todos os nomes de arquivos para uma forma quase sempre igual à forma NFD. Não espere que o HFS+ se comporte da mesma forma que o NTFS ou EXT4 e vice-versa. Não tente alterar dados permanentemente através da normalização como uma abstração com vazamentos para disfarçar as diferenças Unicode entre sistemas de arquivos. Isso criaria problemas sem resolver nenhum. Em vez disso, preserve a forma Unicode e use a normalização apenas como uma função de comparação.
Insensibilidade à Forma Unicode
Insensibilidade à forma Unicode e preservação da forma Unicode são dois comportamentos diferentes de sistema de arquivos, muitas vezes confundidos um com o outro. Assim como a insensibilidade a maiúsculas e minúsculas tem sido, por vezes, incorretamente implementada pela normalização permanente de nomes de arquivos para maiúsculas ao armazenar e transmitir nomes de arquivos, a insensibilidade à forma Unicode tem sido, por vezes, incorretamente implementada pela normalização permanente de nomes de arquivos para uma determinada forma Unicode (NFD no caso do HFS+) ao armazenar e transmitir nomes de arquivos. É possível e muito melhor implementar a insensibilidade à forma Unicode sem sacrificar a preservação da forma Unicode, usando a normalização Unicode apenas para comparação.
Comparando Diferentes Formas Unicode
Node.js fornece string.normalize ('NFC' / 'NFD')
que você pode usar para normalizar uma string UTF-8 para NFC ou NFD. Você nunca deve armazenar a saída desta função, mas apenas usá-la como parte de uma função de comparação para testar se duas strings UTF-8 pareceriam iguais para o usuário. Você pode usar string1.normalize('NFC')=== string2.normalize('NFC')
ou string1.normalize('NFD')=== string2.normalize('NFD')
como sua função de comparação. A forma que você usa não importa.
A normalização é rápida, mas você pode querer usar um cache como entrada para sua função de comparação para evitar normalizar a mesma string muitas vezes. Se a string não estiver presente no cache, normalize-a e adicione-a ao cache. Cuidado para não armazenar ou persistir o cache, use-o apenas como um cache.
Observe que o uso de normalize ()
exige que sua versão do Node.js inclua ICU (caso contrário, normalize ()
retornará apenas a string original). Se você baixar a versão mais recente do Node.js do site, ela incluirá ICU.
Resolução de Timestamp
Você pode definir o mtime (hora de modificação) de um arquivo para 1444291759414 (resolução em milissegundos) e se surpreender ao ver, às vezes, que fs.stat
retorna o novo mtime como 1444291759000 (resolução de 1 segundo) ou 1444291758000 (resolução de 2 segundos). Isso não é um bug no Node. O Node.js retorna o timestamp como o sistema de arquivos o armazena, e nem todos os sistemas de arquivos suportam resolução de timestamp em nanossegundos, milissegundos ou 1 segundo. Alguns sistemas de arquivos têm até uma resolução muito grosseira para o timestamp atime em particular, por exemplo, 24 horas para alguns sistemas de arquivos FAT.
Não corrompa nomes de arquivos e timestamps através de normalização
Nomes de arquivos e timestamps são dados do usuário. Assim como você nunca reescreveria automaticamente os dados do arquivo do usuário para maiúsculas ou normalizaria CRLF para LF no final das linhas, você nunca deve alterar, interferir ou corromper nomes de arquivos ou timestamps através de normalização de maiúsculas/minúsculas/forma Unicode/timestamp. A normalização só deve ser usada para comparação, nunca para alterar dados.
A normalização é efetivamente um código hash com perda de informação. Você pode usá-lo para testar certos tipos de equivalência (por exemplo, várias strings parecem iguais mesmo que tenham sequências de bytes diferentes), mas você nunca pode usá-lo como um substituto para os dados reais. Seu programa deve passar os dados do nome do arquivo e do timestamp como estão.
Seu programa pode criar novos dados em NFC (ou em qualquer combinação de forma Unicode que preferir) ou com um nome de arquivo em minúsculas ou maiúsculas, ou com um timestamp de resolução de 2 segundos, mas seu programa não deve corromper os dados existentes do usuário impondo normalização de maiúsculas/minúsculas/forma Unicode/timestamp. Em vez disso, adote uma abordagem de superconjunto e preserve o caso, a forma Unicode e a resolução do timestamp em seu programa. Dessa forma, você poderá interagir com segurança com sistemas de arquivos que fazem o mesmo.
Use funções de comparação de normalização apropriadamente
Certifique-se de usar as funções de comparação de maiúsculas/minúsculas/forma Unicode/timestamp apropriadamente. Não use uma função de comparação de nome de arquivo sem diferenciação de maiúsculas e minúsculas se você estiver trabalhando em um sistema de arquivos sensível a maiúsculas e minúsculas. Não use uma função de comparação insensível à forma Unicode se você estiver trabalhando em um sistema de arquivos sensível à forma Unicode (por exemplo, NTFS e a maioria dos sistemas de arquivos Linux que preservam NFC e NFD ou formas Unicode mistas). Não compare timestamps com resolução de 2 segundos se você estiver trabalhando em um sistema de arquivos com resolução de timestamp de nanossegundos.
Esteja preparado para pequenas diferenças nas funções de comparação
Tenha cuidado para que suas funções de comparação correspondam às do sistema de arquivos (ou sonde o sistema de arquivos, se possível, para ver como ele realmente compararia). A insensibilidade a maiúsculas e minúsculas, por exemplo, é mais complexa do que uma simples comparação toLowerCase()
. Na verdade, toUpperCase()
geralmente é melhor do que toLowerCase()
(já que ele lida com certos caracteres de idiomas estrangeiros de maneira diferente). Mas ainda melhor seria sondar o sistema de arquivos, pois cada sistema de arquivos possui sua própria tabela de comparação de maiúsculas e minúsculas embutida.
Como exemplo, o HFS+ da Apple normaliza os nomes de arquivos para a forma NFD, mas esta forma NFD é na verdade uma versão mais antiga da forma NFD atual e pode às vezes ser ligeiramente diferente da forma NFD do padrão Unicode mais recente. Não espere que o HFS+ NFD seja exatamente o mesmo que o Unicode NFD o tempo todo.