如何安全地写入文件?

想象一下,你有一个用于处理某种XML文件或configuration文件的库。 该库将整个文件读入内存,并提供编辑内容的方法。 当您完成操作内容时,您可以调用write来将内容保存回文件。 问题是如何以安全的方式做到这一点。

覆盖现有文件(开始写入原始文件)显然是不安全的。 如果write方法在完成之前失败,则最终得到一半写入文件,并且丢失了数据。

一个更好的select是将某个临时文件写入某个地方,并在write方法完成后,将该临时文件复制到原始文件中。

现在,如果复制以某种方式失败,您仍然正确地保存临时文件中的数据。 如果复制成功,您可以删除临时文件。

在POSIX系统上,我猜你可以使用rename系统调用这是一个primefaces操作。 但是,如何在Windows系统上做到最好? 特别是,你如何使用Python来处理这个最好的?

另外,是否有另一种安全写入文件的scheme?

Solutions Collecting From Web of "如何安全地写入文件?"

如果你看到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正确,并保存你必须:

  1. 写入临时文件
  2. 刷新和fsync文件(或fdatasync
  3. 重命名原始文件

请注意,调用fsync会对性能产生不可预测的影响 – 因此,ext3上的Linux可能因为其他未完成的I / O而导致磁盘I / O的整秒数停顿。

请注意, rename 不是 POSIX中的一个原子操作 – 至少不像您期望的那样涉及到文件数据。 但是,大多数操作系统和文件系统将以这种方式工作。 但是你似乎错过了关于Ext4和关于原子性的文件系统保证的大型Linux讨论。 我不知道在哪里链接,但这是一个开始: ext4和数据丢失 。

但是请注意,在许多系统上,重命名将像您期望的那样安全。 然而,在所有可能的Linux配置中,都无法兼顾 – 性能和可靠性!

通过写入临时文件,然后重命名临时文件,人们会期望操作是依赖的,并且将按顺序执行。

然而,问题在于大多数(如果不是全部的话)文件系统分离元数据和数据。 重命名只是元数据。 这听起来可能很糟糕,但是文件系统比数据更重要元数据(例如HFS +或Ext3,4中的Journaling)! 原因是元数据更轻,如果元数据损坏,整个文件系统就会损坏 – 文件系统当然必须保留它自己,然后按照这个顺序保留用户的数据。

Ext4在第一次出现的时候确实打破了rename期望,但是加入了启发式来解决它。 这个问题不是一个失败的重命名,而是一个成功的重命名。 Ext4可能会成功注册重命名,但如果此后不久出现崩溃,则无法写出文件数据。 结果是一个0长度的文件,既不是信号也不是新的数据。

所以简而言之,POSIX没有这样的保证。 阅读链接的Ext4文章以获取更多信息!

在Win API中,我发现了相当不错的函数ReplaceFile ,即使使用可选的备份,它的名字也是如此。 总是有DeleteFile , MoveFile组合的方式。

总的来说,你想要做的是非常好的。 我想不出更好的写法。

标准的解决方案是这样的。

  1. 写一个类似名称的新文件。 例如,X.ext#

  2. 当这个文件被关闭(甚至可能是读取和校验和),那么你两个重命名。

    • X.ext(原文)到X.ext〜

    • X.ext#(新的)到X.ext

  3. (只为疯狂的偏执狂)调用操作系统同步功能强制脏缓冲区写入。

在任何时候都没有任何东西丢失或可摧毁。 唯一的小故障可能发生在重命名。 但是你没有损失任何东西或者损坏任何东西。 原来是可恢复的,直到最终重命名。

一个简单的解决方案。 使用tempfile创建一个临时文件,如果写入成功,只需将该文件重命名为您的原始配置文件。

要锁定文件,请参阅portalocker 。

现在有一个编码的,纯Python,我敢说在Boltons实用程序库中的Pythonic解决方案: 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') 

有很多实用的选择, 都有很好的记录 。 充分披露,我是博尔顿的作者,但这个特殊的部分是建立了很多社区的帮助。 不要犹豫,如果有什么不清楚,请放下一张纸条 !

根据RedGlyph的建议,我添加了一个使用ctypes访问Windows API的ReplaceFile的实现。 我首先将其添加到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 

如果脚本突然结束,您仍然有备份。