На этой странице

Node.js открывает доступ ко многим возможностям файловой системы. Но не все файловые системы одинаковы. Ниже приведены рекомендуемые лучшие практики, чтобы ваш код оставался простым и безопасным при работе с разными файловыми системами.

Прежде чем работать с файловой системой, нужно знать, как она себя ведёт. Разные файловые системы ведут себя по-разному и имеют больше или меньше возможностей, чем другие: чувствительность к регистру, нечувствительность к регистру, сохранение регистра, сохранение формы Unicode, разрешение временных меток, расширенные атрибуты, inode'ы, права Unix, альтернативные потоки данных и т. д.

Остерегайтесь выводить поведение файловой системы из process.platform. Например, не считайте, что раз ваша программа работает на Darwin, вы, следовательно, работаете с нечувствительной к регистру файловой системой (HFS+), — ведь пользователь может использовать чувствительную к регистру файловую систему (HFSX). Аналогично, не считайте, что раз ваша программа работает на Linux, вы, следовательно, работаете с файловой системой, которая поддерживает права Unix и inode'ы, — ведь вы можете оказаться на конкретном внешнем, USB- или сетевом диске, который их не поддерживает.

Операционная система может не облегчать вывод поведения файловой системы, но не всё потеряно. Вместо того чтобы держать список всех известных файловых систем и их поведения (который всегда будет неполным), можно прощупать (probe) файловую систему, чтобы посмотреть, как она ведёт себя на самом деле. Наличие или отсутствие определённых возможностей, которые легко прощупать, часто достаточно, чтобы вывести поведение других возможностей, которые прощупать труднее.

Помните, что у некоторых пользователей разные файловые системы могут быть смонтированы по разным путям в рабочем дереве.

У вас может возникнуть соблазн заставить программу вести себя как файловая система по наименьшему общему знаменателю: нормализуя все имена файлов в верхний регистр, все имена файлов в форму NFC Unicode и все временные метки, скажем, до 1-секундного разрешения. Это был бы подход наименьшего общего знаменателя.

Не делайте так. Вы смогли бы безопасно взаимодействовать только с той файловой системой, которая имеет точно такие же характеристики наименьшего общего знаменателя во всех отношениях. Вы не смогли бы работать с более продвинутыми файловыми системами так, как ожидают пользователи, и столкнулись бы с коллизиями имён файлов или временных меток. Вы почти наверняка потеряли бы и повредили данные пользователя через цепочку сложных взаимозависимых событий и создали бы баги, которые было бы трудно, если вообще возможно, решить.

Что произойдёт, когда вам позже понадобится поддержать файловую систему с разрешением временных меток всего 2 секунды или 24 часа? Что произойдёт, когда стандарт Unicode продвинется и включит слегка иной алгоритм нормализации (как уже случалось в прошлом)?

Подход наименьшего общего знаменателя обычно пытается создать переносимую программу, используя только «переносимые» системные вызовы. Это приводит к программам, которые «текут» и на деле не переносимы.

Используйте по максимуму каждую поддерживаемую платформу, приняв подход надмножества. Например, переносимая программа для бэкапов должна корректно синхронизировать btime'ы (время создания файла или папки) между Windows-системами и не должна уничтожать или изменять btime'ы, даже несмотря на то что btime'ы не поддерживаются в Linux-системах. Та же переносимая программа для бэкапов должна корректно синхронизировать права Unix между Linux- системами и не должна уничтожать или изменять права Unix, даже несмотря на то что права Unix не поддерживаются в Windows-системах.

Работайте с разными файловыми системами, заставляя программу вести себя как более продвинутая файловая система. Поддерживайте надмножество всех возможных возможностей: чувствительность к регистру, сохранение регистра, чувствительность к форме Unicode, сохранение формы Unicode, права Unix, высокоточные наносекундные временные метки, расширенные атрибуты и т. д.

Как только в программе есть сохранение регистра, вы всегда сможете реализовать нечувствительность к регистру, если вам понадобится взаимодействовать с нечувствительной к регистру файловой системой. Но если вы откажетесь от сохранения регистра в программе, вы не сможете безопасно взаимодействовать с сохраняющей регистр файловой системой. То же верно для сохранения формы Unicode и сохранения разрешения временных меток.

Если файловая система выдаёт вам имя файла в смеси нижнего и верхнего регистра, сохраняйте имя файла ровно в том регистре, в котором оно дано. Если файловая система выдаёт вам имя файла в смешанной форме Unicode, NFC или NFD (или NFKC, или NFKD), сохраняйте имя файла ровно в той последовательности байтов, в которой оно дано. Если файловая система выдаёт вам временную метку в миллисекундах, сохраняйте временную метку в миллисекундном разрешении.

При работе с более простой файловой системой вы всегда можете уместно понизить точность (downsample) с помощью функций сравнения, требуемых поведением файловой системы, на которой работает ваша программа. Если вы знаете, что файловая система не поддерживает права Unix, то не стоит ожидать, что вы прочитаете те же права Unix, что записали. Если вы знаете, что файловая система не сохраняет регистр, будьте готовы увидеть ABC в листинге директории, когда ваша программа создаёт abc. Но если вы знаете, что файловая система сохраняет регистр, то вам стоит считать ABC иным именем файла, чем abc, при обнаружении переименований файлов или если файловая система чувствительна к регистру.

Вы можете создать директорию под названием test/abc и с удивлением иногда увидеть, что fs.readdir('test') возвращает ['ABC']. Это не баг Node. Node возвращает имя файла так, как его хранит файловая система, а не все файловые системы поддерживают сохранение регистра. Некоторые файловые системы преобразуют все имена файлов в верхний (или нижний) регистр.

Сохранение регистра и сохранение формы Unicode — похожие концепции. Чтобы понять, почему форму Unicode нужно сохранять, сначала убедитесь, что вы понимаете, почему нужно сохранять регистр. Сохранение формы Unicode так же просто, если понимать его правильно.

Unicode может кодировать одни и те же символы несколькими разными последовательностями байтов. Несколько строк могут выглядеть одинаково, но иметь разные последовательности байтов. При работе с UTF-8-строками следите, чтобы ваши ожидания соответствовали тому, как работает Unicode. Так же как вы не ожидаете, что все UTF-8-символы кодируются одним байтом, не стоит ожидать, что несколько UTF-8-строк, выглядящих одинаково для человеческого глаза, имеют одинаковое байтовое представление. Такое ожидание может быть у вас относительно ASCII, но не относительно UTF-8.

Вы можете создать директорию под названием test/café (форма NFC Unicode с последовательностью байтов <63 61 66 c3 a9> и string.length === 5) и с удивлением иногда увидеть, что fs.readdir('test') возвращает ['café'] (форма NFD Unicode с последовательностью байтов <63 61 66 65 cc 81> и string.length === 6). Это не баг Node. Node.js возвращает имя файла так, как его хранит файловая система, а не все файловые системы поддерживают сохранение формы Unicode.

HFS+, например, нормализует все имена файлов в форму, почти всегда совпадающую с формой NFD. Не ожидайте, что HFS+ поведёт себя так же, как NTFS или EXT4, и наоборот. Не пытайтесь навсегда изменить данные через нормализацию как дырявую абстракцию, чтобы замаскировать различия Unicode между файловыми системами. Это создало бы проблемы, ничего не решая. Вместо этого сохраняйте форму Unicode и используйте нормализацию только как функцию сравнения.

Нечувствительность к форме Unicode и сохранение формы Unicode — два разных поведения файловой системы, которые часто путают друг с другом. Так же как нечувствительность к регистру иногда некорректно реализовывали, навсегда нормализуя имена файлов в верхний регистр при хранении и передаче имён файлов, так и нечувствительность к форме Unicode иногда некорректно реализовывали, навсегда нормализуя имена файлов к определённой форме Unicode (NFD в случае HFS+) при хранении и передаче имён файлов. Можно — и гораздо лучше — реализовать нечувствительность к форме Unicode, не жертвуя сохранением формы Unicode, используя нормализацию Unicode только для сравнения.

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.