Как сделать создание файла атомарной операцией?

Я использую Python для записи фрагментов текста в файлы за одну операцию:

open(file, 'w').write(text)

Если сценарий прерывается, поэтому запись файла не завершается, я хочу, чтобы файл не был, а был частично завершенным файлом. Можно ли это сделать?


person hoju    schedule 25.02.2010    source источник
comment
связанные: потокобезопасная и отказоустойчивая запись файлов   -  person jfs    schedule 10.09.2012


Ответы (6)


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

f = open(tmpFile, 'w')
f.write(text)
# make sure that all data is on disk
# see http://stackoverflow.com/questions/7433057/is-rename-without-fsync-safe
f.flush()
os.fsync(f.fileno()) 
f.close()

os.rename(tmpFile, myFile)

Согласно документу http://docs.python.org/library/os.html#os.rename

В случае успеха переименование будет атомарной операцией (это требование POSIX). В Windows, если dst уже существует, будет вызвана ошибка OSError, даже если это файл; может быть невозможно реализовать атомарное переименование, когда dst называет существующий файл

также

Операция может завершиться ошибкой в ​​некоторых разновидностях Unix, если src и dst находятся в разных файловых системах.

Примечание:

  • Это может быть не атомарная операция, если местоположения src и dest не находятся в одной и той же файловой системе.

  • os.fsync шаг можно пропустить, если производительность/быстродействие более важны, чем целостность данных в таких случаях, как сбой питания, сбой системы и т. д.

person Anurag Uniyal    schedule 25.02.2010
comment
но вы можете добавить os.fsync(f) перед f.close(), так как это гарантирует, что данные нового файла действительно находятся на диске. - person Dan D.; 08.03.2011
comment
Для полноты картины модуль tempfile предоставляет простой и безопасный способ создания временных файлов. - person itsadok; 29.08.2011
comment
И для большей полноты: rename является атомарным только в пределах одной и той же файловой системы в POSIX, поэтому проще всего создать tmpFile в каталоге myFile. - person darkk; 13.01.2012
comment
Я обнаружил, что это не будет работать в Windows, если файл уже существует: в Windows, если dst уже существует, будет вызвана ошибка OSError. - person hoju; 30.06.2012
comment
В то время как os.fsync необходим, если вы беспокоитесь о внезапном завершении работы ОС (например, отключение питания или паника ядра), это излишне для случая, когда вы просто обеспокоены прерыванием процесса. - person R Samuel Klatchko; 01.09.2012
comment
@RSamuelKlatchko да, может быть, но это одна строка, это не повредит и сохранит данные в редких случаях, о которых вы упомянули. - person Anurag Uniyal; 02.09.2012
comment
@AnuragUniyal - больно это или нет, зависит от того, как часто выполняется атомарная запись. os.fsync может быть очень медленным, так как ему приходится ждать, пока ядро ​​очистит свои буферы. Если кто-то использует этот код для записи нескольких файлов, это определенно может вызвать заметное замедление работы. - person R Samuel Klatchko; 02.09.2012
comment
@RSamuelKlatchko да, я согласен, я обновлю вопрос с этой информацией. - person Anurag Uniyal; 03.09.2012
comment
Просто для полноты... tmpFile должен находиться в том же каталоге, что и конечный файл, чтобы убедиться, что они находятся в одной файловой системе. - person Romuald Brunet; 09.05.2013
comment
@ JFSebastian обратите внимание, что sqlite добавляет этот fsync(opendir(filename)), чтобы переименование также записывалось на диск. Это не влияет на атомарность этой модификации, только на относительный порядок этой операции по сравнению с предыдущим/следующим в другом файле. - person Dima Tisnek; 14.03.2014
comment
@qarma: примечание: мой комментарий касается fsync() файла файла в ответе, а не каталога — здесь есть несколько проблем — и возможная необходимость fsync(opendir()) только подтверждает, что fsync() в ответа недостаточно. - person jfs; 28.09.2015
comment
Если myfile является символической ссылкой, это сделает его обычным файлом. Не повредит ли использование os.rename(tmpfile, os.path.realpath(myfile)) атомарной функции? - person Jason; 18.05.2019
comment
Зачем изобретать велосипед? Теперь для этого есть библиотеки, например, safer. Вы можете использовать память или временные файлы, и это даже работает с сокетами! - person Eric; 17.08.2020
comment
вы пропустили генерацию имени файла временного файла. Что, если два процесса попытаются записать в один и тот же временный файл? - person Boris; 22.10.2020

Простой фрагмент, реализующий атомарную запись с использованием Python tempfile.

with open_atomic('test.txt', 'w') as f:
    f.write("huzza")

или даже чтение и запись в один и тот же файл:

with open('test.txt', 'r') as src:
    with open_atomic('test.txt', 'w') as dst:
        for line in src:
            dst.write(line)

с помощью двух простых контекстных менеджеров

import os
import tempfile as tmp
from contextlib import contextmanager

@contextmanager
def tempfile(suffix='', dir=None):
    """ Context for temporary file.

    Will find a free temporary filename upon entering
    and will try to delete the file on leaving, even in case of an exception.

    Parameters
    ----------
    suffix : string
        optional file suffix
    dir : string
        optional directory to save temporary file in
    """

    tf = tmp.NamedTemporaryFile(delete=False, suffix=suffix, dir=dir)
    tf.file.close()
    try:
        yield tf.name
    finally:
        try:
            os.remove(tf.name)
        except OSError as e:
            if e.errno == 2:
                pass
            else:
                raise

@contextmanager
def open_atomic(filepath, *args, **kwargs):
    """ Open temporary file object that atomically moves to destination upon
    exiting.

    Allows reading and writing to and from the same filename.

    The file will not be moved to destination in case of an exception.

    Parameters
    ----------
    filepath : string
        the file path to be opened
    fsync : bool
        whether to force write the file to disk
    *args : mixed
        Any valid arguments for :code:`open`
    **kwargs : mixed
        Any valid keyword arguments for :code:`open`
    """
    fsync = kwargs.get('fsync', False)

    with tempfile(dir=os.path.dirname(os.path.abspath(filepath))) as tmppath:
        with open(tmppath, *args, **kwargs) as file:
            try:
                yield file
            finally:
                if fsync:
                    file.flush()
                    os.fsync(file.fileno())
        os.rename(tmppath, filepath)
person Nils Werner    schedule 07.04.2015
comment
Временный файл должен находиться в той же файловой системе, что и заменяемый файл. Этот код не будет надежно работать в системах с несколькими файловыми системами. Для вызова NamedTemporaryFile требуется параметр dir=. - person textshell; 17.07.2016
comment
Спасибо за комментарий, я недавно изменил этот фрагмент, чтобы вернуться к shutil.move в случае сбоя os.rename. Это позволяет ему работать за границами ФС. - person Nils Werner; 18.07.2016
comment
Кажется, что это работает при запуске, но Shutil.move использует copy2, который не является атомарным. И если бы copy2 хотел быть атомарным, ему нужно было бы создать временный файл в той же файловой системе, что и конечный файл. Таким образом, исправление, позволяющее вернуться к Shutil.move, только маскирует проблему. Вот почему большинство фрагментов кода помещают временный файл в тот же каталог, что и целевой файл. Это также возможно с использованием tempfile.NamedTemporaryFile с использованием именованного аргумента каталога. Поскольку перемещение файла в каталоге, который не доступен для записи, в любом случае не работает, это кажется самым простым и надежным решением. - person textshell; 18.07.2016
comment
Правильно, я предположил, что shutils.move() не является атомарным из-за последовательного вызова shutils.copy2() и shutils.remove(). Новая реализация (см. редактирование) теперь будет создавать файл в текущем каталоге, а также лучше обрабатывать исключения. - person Nils Werner; 19.07.2016
comment
Почему это может быть атомарным при чтении и записи в один и тот же файл? В приведенном выше примере open('test.txt', 'r') as src: используется для чтения содержимого файла. Письмо в этом смысле атомарно, но чтение может быть другим. Для типов файлов, таких как .ini, воспроизведение с декораторами при использовании с configparser для операций чтения. Не уверен, что этот образец полностью оправдывает атомарность чтения из одного и того же файла более чем 200 000 потоков. Это вызовет ошибку «Слишком много открытых файлов». - person bh4r4th; 10.01.2020
comment
@ bh4r4th Я не понимаю твоего комментария. Но атомарность или нет, открытие 200 000 файлов — это слишком много. - person Nils Werner; 10.01.2020
comment
Да, имеет смысл слишком много файлов. Я обновляю файл, в котором хранится статус после каждого обновления. У меня запускается 200000 обновлений. Изменит мою реализацию. - person bh4r4th; 13.01.2020
comment
tempfile.NamedTemporaryFile().name у меня всегда начинается с /tmp. Если tmpfs — это файловая система в памяти, как ваш код может быть атомарным, если ему нужно записать содержимое файла из tmpfs в памяти на диск? - person Boris; 22.10.2020

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

Одной из таких библиотек является python-atomicwrites от untitaker, которая даже имеет надлежащую поддержку Windows:

Из README:

from atomicwrites import atomic_write

with atomic_write('foo.txt', overwrite=True) as f:
    f.write('Hello world.')
    # "foo.txt" doesn't exist yet.

# Now it does.

Установка через PIP:

pip install atomicwrites
person vog    schedule 28.06.2016
comment
Вот команда для его установки: pip install atomicwrites==1.4.0 - person neves; 03.02.2021
comment
@neves Спасибо за подсказку. Я улучшил свой ответ соответственно. Однако я удалил явный номер версии в конце, так как это было бы опасно рекомендовать, особенно в ответе, который существует годами и почти наверняка будет существовать годами. - person vog; 04.02.2021
comment
В этом контексте вы правы, но рекомендуется исправлять версии ваших зависимостей. - person neves; 05.02.2021

Я использую этот код для атомарной замены/записи файла:

import os
from contextlib import contextmanager

@contextmanager
def atomic_write(filepath, binary=False, fsync=False):
    """ Writeable file object that atomically updates a file (using a temporary file).

    :param filepath: the file path to be opened
    :param binary: whether to open the file in a binary mode instead of textual
    :param fsync: whether to force write the file to disk
    """

    tmppath = filepath + '~'
    while os.path.isfile(tmppath):
        tmppath += '~'
    try:
        with open(tmppath, 'wb' if binary else 'w') as file:
            yield file
            if fsync:
                file.flush()
                os.fsync(file.fileno())
        os.rename(tmppath, filepath)
    finally:
        try:
            os.remove(tmppath)
        except (IOError, OSError):
            pass

Применение:

with atomic_write('path/to/file') as f:
    f.write("allons-y!\n")

Он основан на этом рецепте.

person Jakub Jirutka    schedule 30.08.2014
comment
цикл while является колоритным, возможно, два параллельных процесса открывают один и тот же файл. tempfile.NamedTemporaryFile может преодолеть это. - person Mic92; 31.05.2016
comment
Я думаю, что такой tmppath был бы лучше '.{filepath}~{random}', чтобы избежать условий гонки, если два процесса делают одно и то же. Это не решает состояние гонки, но, по крайней мере, вы не получаете файл с содержимым двух процессов. - person guettli; 11.10.2016

Ответы на этой странице довольно старые, теперь есть библиотеки, которые делают это за вас.

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

Их пример - это то, что вам нужно:

# dangerous
with open(filename, 'w') as fp:
    json.dump(data, fp)
    # If an exception is raised, the file is empty or partly written
# safer
with safer.open(filename, 'w') as fp:
    json.dump(data, fp)
    # If an exception is raised, the file is unchanged.

Это в PyPI, просто установите его с помощью pip install --user safer или получите последнюю версию на https://github.com/rec/safer< /а>

person Eric    schedule 17.08.2020

Атомное решение для Windows для зацикливания папки и переименования файлов. Протестировано, атомарно для автоматизации, вы можете увеличить вероятность, чтобы свести к минимуму риск не иметь такое же имя файла. В вашей случайной библиотеке для комбинаций буквенных символов используется метод random.choice, для цифры str(random.random.range(50,999999999,2). Вы можете варьировать диапазон цифр по своему усмотрению.

import os import random

path = "C:\\Users\\ANTRAS\\Desktop\\NUOTRAUKA\\"

def renamefiles():
    files = os.listdir(path)
    i = 1
    for file in files:
        os.rename(os.path.join(path, file), os.path.join(path, 
                  random.choice('ABCDEFGHIJKL') + str(i) + str(random.randrange(31,9999999,2)) + '.jpg'))
        i = i+1

for x in range(30):
    renamefiles()
person Community    schedule 04.02.2018