Как безопасно записать в файл?
Представьте, что у вас есть библиотека для работы с каким-то файлом XML или файлом конфигурации. Библиотека считывает весь файл в память и предоставляет методы для редактирования содержимого. Когда вы закончите манипулировать контентом, вы можете позвонить write
сохранить содержимое обратно в файл. Вопрос в том, как сделать это безопасным способом.
Перезапись существующего файла (начало записи в исходный файл), очевидно, небезопасна. Если write
метод завершается неудачей, прежде чем он будет выполнен, в результате вы получите половинный файл и потеряли данные.
Лучшим вариантом будет записать куда-нибудь временный файл, и когда write
Метод завершен, вы копируете временный файл в исходный файл.
Теперь, если копия почему-то дает сбой, вы все равно правильно сохранили данные во временном файле. И если копирование удастся, вы можете удалить временный файл.
В системах POSIX я думаю, что вы можете использовать rename
системный вызов, который является атомарной операцией. Но как бы вы сделали это лучше всего в системе Windows? В частности, как вы справляетесь с этим лучше всего с помощью Python?
Кроме того, есть ли другая схема для безопасной записи в файлы?
8 ответов
Если вы видите документацию Python, в ней четко упоминается, что os.rename() является атомарной операцией. Так что в вашем случае запись данных во временный файл с последующим переименованием в исходный файл будет вполне безопасна.
Другой способ может работать так:
- пусть оригинальный файл будет abc.xml
- создать abc.xml.tmp и записать в него новые данные
- переименуйте abc.xml в abc.xml.bak
- переименуйте abc.xml.tmp в abc.xml
- после того, как новый abc.xml будет правильно установлен, удалите abc.xml.bak
Как вы можете видеть, у вас есть abc.xml.bak, который вы можете использовать для восстановления, если есть какие-либо проблемы, связанные с файлом tmp и копированием его обратно.
Если вы хотите быть POSIXly правильно и сохранить, вы должны:
- Запись во временный файл
- Флеш и
fsync
файл (илиfdatasync
) - Переименовать поверх исходного файла
Обратите внимание, что вызов fsync оказывает непредсказуемое влияние на производительность - в результате Linux на ext3 может зависать от целого числа секунд дискового ввода-вывода, в зависимости от других ожидающих операций ввода-вывода.
Заметить, что rename
не является атомарной операцией в POSIX - по крайней мере, не по отношению к данным файла, как вы ожидаете. Однако большинство операционных систем и файловых систем будут работать таким образом. Но, похоже, вы пропустили очень большую дискуссию по Linux о Ext4 и гарантиях файловой системы на атомарность. Я не знаю точно, где ссылаться, но вот начало: ext4 и потеря данных.
Однако обратите внимание, что на многих системах переименование будет на практике настолько безопасным, насколько вы ожидаете. Тем не менее, невозможно достичь ни производительности, ни надежности во всех возможных конфигурациях Linux!
При записи во временный файл, а затем переименовании временного файла, можно ожидать, что операции являются зависимыми и будут выполняться по порядку.
Однако проблема заключается в том, что большинство, если не все файловые системы разделяют метаданные и данные. Переименование - это только метаданные. Это может показаться вам ужасным, но файловые системы ценят метаданные по сравнению с данными (например, ведение журнала в HFS+ или Ext3,4)! Причина в том, что метаданные легче, и если метаданные повреждены, вся файловая система повреждена - файловая система должна, конечно, сохранить себя, а затем сохранить данные пользователя в этом порядке.
Ext4 сломал rename
ожидание, когда оно впервые появилось, однако для его решения были добавлены эвристики. Проблема заключается не в неудачном переименовании, а в успешном переименовании. Ext4 может успешно зарегистрировать переименование, но не сможет записать данные файла, если вскоре произойдет сбой. В результате получается файл нулевой длины, в котором нет ни оригинальных, ни новых данных.
Короче говоря, POSIX не дает такой гарантии. Прочитайте связанную статью Ext4 для получения дополнительной информации!
В Win API я нашел довольно приятную функцию ReplaceFile, которая делает то, что предлагает имя, даже с необязательным резервным копированием. Всегда есть способ с DeleteFile, комбо MoveFile.
В общем, то, что вы хотите сделать, действительно хорошо. И я не могу придумать более подходящую схему записи.
Упрощенное решение. использование tempfile
создать временный файл и, если запись удалась, просто переименуйте файл в исходный файл конфигурации.
Для блокировки файла, см. Portalocker.
Стандартное решение таково.
Напишите новый файл с похожим именем. X.ext# например.
Когда этот файл был закрыт (и, возможно, даже прочитан и проверен), вы переименовываетесь.
X.ext (оригинал) в X.ext ~
X.ext # (новый) в X.ext
(Только для сумасшедших параноиков) вызовите функцию синхронизации ОС для принудительной записи в грязный буфер.
Ни в коем случае не потеряно и не испорчено. Единственный сбой может случиться во время переименования. Но вы ничего не потеряли и не испортили. Оригинал подлежит восстановлению вплоть до окончательного переименования.
Теперь есть кодифицированный, чистый Python, и я осмелюсь сказать Pythonic решение этой проблемы в служебной библиотеке boltons: boltons.fileutils.atomic_save.
Просто pip install boltons
, затем:
from boltons.fileutils import atomic_save
with atomic_save('/path/to/file.txt') as f:
f.write('this will only overwrite if it succeeds!\n')
Есть много практических вариантов, все хорошо документированы. Полное раскрытие, я автор Boltons, но эта конкретная часть была построена с большой помощью сообщества. Не стесняйтесь оставлять записку, если что-то неясно!
По предложению RedGlyph, я добавил реализацию ReplaceFile, которая использует ctypes для доступа к API Windows. Я впервые добавил это в jaraco.windows.api.filesystem.
ReplaceFile = windll.kernel32.ReplaceFileW
ReplaceFile.restype = BOOL
ReplaceFile.argtypes = [
LPWSTR,
LPWSTR,
LPWSTR,
DWORD,
LPVOID,
LPVOID,
]
REPLACEFILE_WRITE_THROUGH = 0x1
REPLACEFILE_IGNORE_MERGE_ERRORS = 0x2
REPLACEFILE_IGNORE_ACL_ERRORS = 0x4
Затем я проверил поведение с помощью этого сценария.
from jaraco.windows.api.filesystem import ReplaceFile
import os
open('orig-file', 'w').write('some content')
open('replacing-file', 'w').write('new content')
ReplaceFile('orig-file', 'replacing-file', 'orig-backup', 0, 0, 0)
assert open('orig-file').read() == 'new content'
assert open('orig-backup').read() == 'some content'
assert not os.path.exists('replacing-file')
Хотя это работает только в Windows, похоже, у него есть много приятных функций, которых не хватало бы другим подпрограммам на замену. Смотрите API документы для деталей.
Вы можете использовать модуль fileinput для обработки резервного копирования и записи на месте:
import fileinput
for line in fileinput.input(filename,inplace=True, backup='.bak'):
# inplace=True causes the original file to be moved to a backup
# standard output is redirected to the original file.
# backup='.bak' specifies the extension for the backup file.
# manipulate line
newline=process(line)
print(newline)
Если вам нужно прочитать все содержимое до того, как вы сможете написать новую строку, то вы можете сначала сделать это, а затем распечатать все новое содержимое с помощью
newcontents=process(contents)
for line in fileinput.input(filename,inplace=True, backup='.bak'):
print(newcontents)
break
Если сценарий внезапно завершится, у вас останется резервная копия.