Как безопасно записать в файл? - PullRequest
15 голосов
/ 28 ноября 2009

Представьте, что у вас есть библиотека для работы с каким-либо XML-файлом или файлом конфигурации. Библиотека считывает весь файл в память и предоставляет методы для редактирования содержимого. Когда вы закончите манипулировать контентом, вы можете вызвать write, чтобы сохранить контент обратно в файл. Вопрос в том, как сделать это безопасным способом.

Перезапись существующего файла (начало записи в исходный файл), очевидно, небезопасна. Если метод write завершится неудачно, прежде чем он будет завершен, вы получите половинный файл и потеряли данные.

Лучше было бы записать куда-нибудь временный файл *1007*, а когда метод write завершится, вы скопируете временный файл в исходный файл. * Теперь, если при копировании возникла ошибка, вы все равно правильно сохранили данные во временном файле. И если копирование выполнено успешно, вы можете удалить временный файл.

В системах POSIX, я думаю, вы можете использовать системный вызов rename, который является атомарной операцией. Но как бы вы сделали это лучше всего в системе Windows? В частности, как вы справляетесь с этим лучше всего, используя Python ?

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

Ответы [ 8 ]

14 голосов
/ 28 ноября 2009

Если вы видите документацию 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 и его копированием.

11 голосов
/ 28 ноября 2009

Если вы хотите быть POSIXly правильным и сохранить, вы должны:

  1. Запись во временный файл
  2. Сброс и fsync файл (или fdatasync)
  3. Переименовать поверх исходного файла

Обратите внимание, что вызов fsync оказывает непредсказуемое влияние на производительность - в результате Linux на ext3 может зависать от целого числа секунд дискового ввода-вывода, в зависимости от других ожидающих операций ввода-вывода.

Обратите внимание, что rename является , а не атомарной операцией в POSIX - по крайней мере, не по отношению к данным файла, как вы ожидаете. Однако большинство операционных систем и файловых систем будут работать таким образом. Но, похоже, вы пропустили очень большую дискуссию по Linux о Ext4 и гарантиях файловой системы на атомарность. Я не знаю точно, где ссылаться, но вот начало: ext4 и потеря данных .

Обратите внимание, что во многих системах переименование будет на практике настолько безопасным, насколько вы ожидаете. Тем не менее, невозможно достичь ни производительности, ни надежности во всех возможных конфигурациях Linux!

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

Однако проблема в том, что большинство, если не все файловые системы разделяют метаданные и данные. Переименование - это только метаданные. Это может показаться вам ужасным, но файловые системы ценят метаданные по сравнению с данными (например, ведение журнала в HFS + или Ext3,4)! Причина в том, что метаданные легче, и если метаданные повреждены, вся файловая система повреждена - файловая система должна, конечно, сохранить себя, а затем сохранить данные пользователя в этом порядке.

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

Короче говоря, POSIX не дает такой гарантии. Прочитайте связанную статью Ext4 для получения дополнительной информации!

5 голосов
/ 28 ноября 2009

В Win API я обнаружил довольно приятную функцию ReplaceFile , которая делает то, что предлагает название, даже с дополнительной резервной копией. Всегда есть способ с DeleteFile , MoveFile комбо.

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

4 голосов
/ 28 ноября 2009

Стандартное решение таково.

  1. Написать новый файл с похожим именем. X.ext # например.

  2. Когда этот файл был закрыт (и, возможно, даже прочитан и проверен), вы двое переименуете.

    • X.ext (оригинал) до X.ext ~

    • X.ext # (новый) до X.ext

  3. (Только для сумасшедших параноиков) вызывать функцию синхронизации ОС для принудительной записи в грязный буфер.

Ни в коем случае не потеряно и не испорчено. Единственный сбой может случиться во время переименования. Но вы ничего не потеряли и не испортили. Оригинал подлежит восстановлению вплоть до окончательного переименования.

3 голосов
/ 06 февраля 2016

В коде утилиты boltons : boltons.fileutils.atomic_save .

теперь есть кодифицированный чистый Python, и я осмелюсь сказать Pythonic для этого

Просто 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, но эта конкретная часть была построена с большой помощью сообщества. Не стесняйтесь оставить записку , если что-то неясно!

3 голосов
/ 28 ноября 2009

Упрощенное решение. Используйте tempfile для создания временного файла и, если запись удалась, просто переименуйте файл в исходный файл конфигурации.

О блокировке файла см. portalocker .

2 голосов
/ 28 ноября 2009

По предложению 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 документы .

0 голосов
/ 28 ноября 2009

Вы можете использовать модуль 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

Если сценарий внезапно завершится, у вас все равно будет резервная копия.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...