Работа с различными файловыми системами
Node.js предоставляет доступ ко многим функциям файловых систем. Однако не все файловые системы одинаковы. Ниже приведены рекомендуемые лучшие практики для обеспечения простоты и безопасности вашего кода при работе с различными файловыми системами.
Поведение файловой системы
Прежде чем вы сможете работать с файловой системой, вам необходимо знать, как она работает. Различные файловые системы ведут себя по-разному и имеют больше или меньше функций, чем другие: регистрозависимость, регистронезависимость, сохранение регистра, сохранение формы Юникода, разрешение временных меток, расширенные атрибуты, индексные дескрипторы (inode), разрешения Unix, альтернативные потоки данных и т. д.
Остерегайтесь делать выводы о поведении файловой системы на основе process.platform
. Например, не предполагайте, что поскольку ваша программа работает на Darwin, вы работаете с регистронезависимой файловой системой (HFS+), так как пользователь может использовать регистрозависимую файловую систему (HFSX). Аналогично, не предполагайте, что поскольку ваша программа работает в Linux, вы работаете с файловой системой, которая поддерживает разрешения Unix и индексные дескрипторы (inode), так как вы можете работать с конкретным внешним диском, USB-накопителем или сетевым диском, который этого не делает.
Операционная система может не облегчать вывод о поведении файловой системы, но не все потеряно. Вместо того чтобы вести список каждой известной файловой системы и ее поведения (который всегда будет неполным), вы можете проверить файловую систему, чтобы узнать, как она фактически работает. Наличия или отсутствия определенных функций, которые легко проверить, часто достаточно, чтобы сделать вывод о поведении других функций, которые сложнее проверить.
Помните, что некоторые пользователи могут иметь разные файловые системы, смонтированные по различным путям в рабочем дереве.
Избегайте подхода «наименьшего общего знаменателя»
Вас может соблазнить сделать так, чтобы ваша программа работала как файловая система с наименьшим общим знаменателем, нормализуя все имена файлов до верхнего регистра, нормализуя все имена файлов до формы NFC Юникода и нормализуя все временные метки файлов, скажем, до разрешения в 1 секунду. Это был бы подход «наименьшего общего знаменателя».
Не делайте этого. Вы сможете безопасно взаимодействовать только с файловой системой, которая имеет точно такие же характеристики наименьшего общего знаменателя во всех отношениях. Вы не сможете работать с более продвинутыми файловыми системами так, как ожидают пользователи, и столкнетесь со столкновениями имен файлов или временных меток. Вы почти наверняка потеряете и повредите пользовательские данные в результате серии сложных зависимых событий, а также создадите ошибки, которые будет трудно, если не невозможно, решить.
Что произойдет, если вам позже потребуется поддерживать файловую систему, которая имеет только разрешение временных меток в 2 секунды или 24 часа? Что произойдет, если стандарт Юникода будет развиваться и включать немного другой алгоритм нормализации (как это уже происходило в прошлом)?
Подход «наименьшего общего знаменателя» будет склонен пытаться создать переносимую программу, используя только «переносимые» системные вызовы. Это приводит к программам, которые имеют утечки и на самом деле не являются переносимыми.
Примите подход супермножества
Максимально используйте возможности каждой поддерживаемой платформы, используя подход супермножества. Например, портативная программа резервного копирования должна корректно синхронизировать btimes (время создания файла или папки) между системами Windows и не должна уничтожать или изменять btimes, даже если btimes не поддерживаются в системах Linux. Та же портативная программа резервного копирования должна корректно синхронизировать права доступа Unix между системами Linux и не должна уничтожать или изменять права доступа Unix, даже если права доступа Unix не поддерживаются в системах Windows.
Обрабатывайте разные файловые системы, заставляя вашу программу работать как более совершенная файловая система. Поддерживайте супермножество всех возможных функций: чувствительность к регистру, сохранение регистра, чувствительность к форме Unicode, сохранение формы Unicode, права доступа Unix, высокоточные наносекундные метки времени, расширенные атрибуты и т. д.
После того как вы реализовали сохранение регистра в вашей программе, вы всегда можете реализовать нечувствительность к регистру, если вам нужно взаимодействовать с файловой системой, нечувствительной к регистру. Но если вы откажетесь от сохранения регистра в вашей программе, вы не сможете безопасно взаимодействовать с файловой системой, сохраняющей регистр. То же самое относится к сохранению формы Unicode и сохранению разрешения меток времени.
Если файловая система предоставляет вам имя файла в сочетании строчных и прописных букв, то сохраните имя файла в том же регистре, в котором оно было предоставлено. Если файловая система предоставляет вам имя файла в смешанной форме Unicode или NFC или NFD (или NFKC или NFKD), то сохраните имя файла в том же самом байтовом потоке, который был предоставлен. Если файловая система предоставляет вам метку времени в миллисекундах, то сохраните метку времени с разрешением в миллисекунды.
При работе с менее функциональной файловой системой вы всегда можете соответствующим образом уменьшить дискретизацию, используя функции сравнения, как того требует поведение файловой системы, на которой работает ваша программа. Если вы знаете, что файловая система не поддерживает права доступа Unix, то вы не должны ожидать, что сможете прочитать те же самые права доступа Unix, которые вы записали. Если вы знаете, что файловая система не сохраняет регистр, то вы должны быть готовы увидеть ABC
в списке каталога, когда ваша программа создает abc
. Но если вы знаете, что файловая система сохраняет регистр, то вы должны рассматривать ABC
как другое имя файла, чем abc
, при обнаружении переименования файлов или если файловая система чувствительна к регистру.
Сохранение регистра
Вы можете создать директорию с именем test /abc
и удивиться, обнаружив, что иногда fs.readdir('test')
возвращает ['ABC']
. Это не ошибка в Node. Node возвращает имя файла таким, как оно хранится в файловой системе, а не все файловые системы поддерживают сохранение регистра. Некоторые файловые системы преобразуют все имена файлов в верхний (или нижний) регистр.
Сохранение формы Юникода
Сохранение регистра и сохранение формы Юникода — схожие понятия. Чтобы понять, почему следует сохранять форму Юникода, сначала убедитесь, что вы понимаете, почему следует сохранять регистр. Сохранение формы Юникода так же просто, если правильно его понимать. Юникод может кодировать одни и те же символы, используя несколько различных последовательностей байтов. Несколько строк могут выглядеть одинаково, но иметь разные последовательности байтов. Работая со строками UTF-8, убедитесь, что ваши ожидания соответствуют тому, как работает Юникод. Так же, как вы не ожидаете, что все символы UTF-8 будут кодироваться в один байт, вы не должны ожидать, что несколько строк UTF-8, которые выглядят одинаково для человеческого глаза, будут иметь одинаковое байтовое представление. Этого можно ожидать от ASCII, но не от UTF-8.
Вы можете создать директорию с именем test/ café
(NFC-форма Юникода с последовательностью байтов <63 61 66 c3 a9>
и string.length ===5
) и удивиться, обнаружив, что иногда fs.readdir('test')
возвращает ['café']
(NFD-форма Юникода с последовательностью байтов <63 61 66 65 cc 81>
и string.length ===6
). Это не ошибка в Node. Node.js возвращает имя файла таким, как оно хранится в файловой системе, а не все файловые системы поддерживают сохранение формы Юникода. HFS+, например, нормализует все имена файлов к форме, почти всегда идентичной NFD-форме. Не ожидайте, что HFS+ будет вести себя так же, как NTFS или EXT4, и наоборот. Не пытайтесь постоянно изменять данные с помощью нормализации как «утечки абстракции» для устранения различий Юникода между файловыми системами. Это создаст проблемы, ничего не решив. Вместо этого сохраняйте форму Юникода и используйте нормализацию только как функцию сравнения.
Нечувствительность к форме Юникода
Нечувствительность к форме Юникода и сохранение формы Юникода — два разных поведения файловой системы, которые часто путают друг с другом. Подобно тому, как нечувствительность к регистру иногда неправильно реализовывалась путем постоянной нормализации имен файлов в верхний регистр при хранении и передаче имен файлов, так и нечувствительность к форме Юникода иногда неправильно реализовывалась путем постоянной нормализации имен файлов до определенной формы Юникода (NFD в случае HFS+) при хранении и передаче имен файлов. Возможно и гораздо лучше реализовать нечувствительность к форме Юникода без потери сохранения формы Юникода, используя нормализацию Юникода только для сравнения.
Сравнение различных форм Юникода
Node.js предоставляет string.normalize ('NFC' / 'NFD')
, который можно использовать для нормализации строки UTF-8 до NFC или NFD. Никогда не следует хранить результат этой функции, а использовать её только как часть функции сравнения для проверки того, будут ли две строки UTF-8 выглядеть одинаково для пользователя. Можно использовать string1.normalize('NFC')=== string2.normalize('NFC')
или string1.normalize('NFD')=== string2.normalize('NFD')
в качестве функции сравнения. Какую форму использовать, не имеет значения.
Нормализация быстрая, но можно использовать кэш в качестве входных данных для функции сравнения, чтобы избежать многократной нормализации одной и той же строки. Если строка отсутствует в кэше, нормализуйте её и добавьте в кэш. Будьте осторожны, не храните и не сохраняйте кэш, используйте его только как кэш.
Обратите внимание, что использование normalize ()
требует, чтобы ваша версия Node.js включала ICU (иначе normalize ()
просто вернет исходную строку). Если вы загрузите последнюю версию Node.js с веб-сайта, она будет включать ICU.
Разрешение временной метки
Можно установить mtime (время изменения) файла на 1444291759414 (разрешение в миллисекундах) и с удивлением обнаружить, что иногда fs.stat
возвращает новое mtime как 1444291759000 (разрешение в 1 секунду) или 1444291758000 (разрешение в 2 секунды). Это не ошибка в Node. Node.js возвращает временную метку такой, как она хранится в файловой системе, и не все файловые системы поддерживают разрешение временной метки в наносекундах, миллисекундах или 1 секунде. Некоторые файловые системы имеют очень грубое разрешение, в частности, для временной метки atime, например, 24 часа для некоторых файловых систем FAT.
Не повреждайте имена файлов и метки времени путем нормализации
Имена файлов и метки времени — это пользовательские данные. Так же, как вы никогда не будете автоматически переписывать пользовательские данные файлов в верхний регистр или нормализовать CRLF до LF в конце строк, так же вы никогда не должны изменять, вмешиваться или повреждать имена файлов или метки времени путем нормализации регистра/формы Unicode/метки времени. Нормализация должна использоваться только для сравнения, никогда для изменения данных.
Нормализация — это фактически потеристая функция хэширования. Вы можете использовать её для проверки определённых видов эквивалентности (например, выглядят ли несколько строк одинаково, даже если у них разные последовательности байтов), но вы никогда не можете использовать её вместо фактических данных. Ваша программа должна передавать данные об именах файлов и метках времени как есть.
Ваша программа может создавать новые данные в NFC (или в любой комбинации форм Unicode, которую она предпочитает) или с именем файла в нижнем или верхнем регистре, или с меткой времени с разрешением в 2 секунды, но ваша программа не должна повреждать существующие пользовательские данные, навязывая нормализацию регистра/формы Unicode/метки времени. Вместо этого, используйте подход супермножества и сохраняйте регистр, форму Unicode и разрешение метки времени в вашей программе. Таким образом, вы сможете безопасно взаимодействовать с файловыми системами, которые делают то же самое.
Используйте функции сравнения нормализации соответствующим образом
Убедитесь, что вы используете функции сравнения регистра/формы Unicode/метки времени соответствующим образом. Не используйте функцию сравнения имён файлов без учёта регистра, если вы работаете с файловой системой, чувствительной к регистру. Не используйте функцию сравнения без учёта формы Unicode, если вы работаете с файловой системой, чувствительной к форме Unicode (например, NTFS и большинство файловых систем Linux, которые сохраняют как NFC, так и NFD или смешанные формы Unicode). Не сравнивайте метки времени с разрешением в 2 секунды, если вы работаете с файловой системой с разрешением метки времени в наносекунды.
Будьте готовы к незначительным различиям в функциях сравнения
Будьте осторожны, чтобы ваши функции сравнения соответствовали функциям файловой системы (или, если возможно, проведите зондирование файловой системы, чтобы увидеть, как она будет фактически сравнивать). Нечувствительность к регистру, например, сложнее, чем простое сравнение toLowerCase()
. На самом деле, toUpperCase()
обычно лучше, чем toLowerCase()
(поскольку она обрабатывает некоторые символы иностранных языков по-разному). Но ещё лучше было бы зондировать файловую систему, поскольку каждая файловая система имеет свою собственную таблицу сравнения регистра.
Например, HFS+ от Apple нормализует имена файлов до формы NFD, но эта форма NFD фактически является более старой версией текущей формы NFD и иногда может незначительно отличаться от формы NFD последнего стандарта Unicode. Не ожидайте, что HFS+ NFD будет всегда точно таким же, как Unicode NFD.