如何使用C#安全地将数据保存到现有文件中?

14

如何在C#中安全地将数据保存到已存在的文件中?我有一些序列化为文件的数据,直接将其安全保存到文件中肯定不是个好主意,因为如果出现任何问题,文件会损坏,之前的版本也会丢失。

因此,这是我迄今为止所做的:

string tempFile = Path.GetTempFileName();

using (Stream tempFileStream = File.Open(tempFile, FileMode.Truncate))
{
    SafeXmlSerializer xmlFormatter = new SafeXmlSerializer(typeof(Project));
    xmlFormatter.Serialize(tempFileStream, Project);
}

if (File.Exists(fileName)) File.Delete(fileName);
File.Move(tempFile, fileName);
if (File.Exists(tempFile)) File.Delete(tempFile);
问题在于我试图保存到一个位于我的Dropbox中的文件时,有时会出现异常告诉我无法保存到已存在的文件。显然第一个File.Delete(fileName);没有立即删除文件,而是稍等片刻才删除。因此,在File.Move(tempFile, fileName);时出现了异常,因为该文件已经存在并且随后被删除,导致我的文件丢失。

我使用过其他带有Dropbox文件的应用程序,它们以某种方式设法避免出现问题。当我尝试保存到Dropbox文件夹中的文件时,有时会收到消息告诉我文件正在使用或类似的东西,但我从未遇到文件被删除的问题。

那么这里应该采用什么样的标准/最佳实践呢?

好的,在阅读所有答案后,我得出以下结论:

private string GetTempFileName(string dir)
{
    string name = null;
    int attempts = 0;
    do
    {
        name = "temp_" + Player.Math.RandomDigits(10) + ".hsp";
        attempts++;
        if (attempts > 10) throw new Exception("Could not create temporary file.");
    }
    while (File.Exists(Path.Combine(dir, name)));

    return name;
}

private void SaveProject(string fileName)
{
    bool originalRenamed = false;
    string tempNewFile = null;
    string oldFileTempName = null;

    try
    {
        tempNewFile = GetTempFileName(Path.GetDirectoryName(fileName));

        using (Stream tempNewFileStream = File.Open(tempNewFile, FileMode.CreateNew))
        {
            SafeXmlSerializer xmlFormatter = new SafeXmlSerializer(typeof(Project));
            xmlFormatter.Serialize(tempNewFileStream, Project);
        }

        if (File.Exists(fileName))
        {
            oldFileTempName = GetTempFileName(Path.GetDirectoryName(fileName));
            File.Move(fileName, oldFileTempName);
            originalRenamed = true;
        }

        File.Move(tempNewFile, fileName);
        originalRenamed = false;

        CurrentProjectPath = fileName;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    finally
    {
        if(tempNewFile != null) File.Delete(tempNewFile);

        if (originalRenamed) MessageBox.Show("'" + fileName + "'" +
            " have been corrupted or deleted in this operation.\n" +
            "A backup copy have been created at '" + oldFileTempName + "'");
        else if (oldFileTempName != null) File.Delete(oldFileTempName);
    }
}

Player.Math.RandomDigits是我编写的一个小函数,它会创建一个由n个随机数字组成的字符串。

我不认为这会破坏原始文件,除非操作系统出现问题。这与Hans的回答非常接近,只是我首先将文件保存到临时文件中,以便在序列化时出现问题时,我不需要将文件重命名回原始名称,这也可能出现问题。如果您发现任何缺陷,请告诉我!


好问题,我很好奇答案是什么。 - Alastair Pitts
为什么不直接使用File.Copy(tempFile, fileName, true);,然后再使用File.Delete(tempFile);呢? - user541686
@Mehrdad:我想应该可以了。不知道为什么没有人把它发布为答案。如果File.Copy出现任何问题,我认为最坏的情况就是什么都不会发生,原始文件保持不变。当然,我的想法可能有误... - Juan
@jsoldi:你假设数据以有序的方式写入磁盘,这是一个危险的假设,在某些情况下可能是真的,但在其他情况下可能不是。可能会出现您请求所有这些内容的情况,并且然后它们将按不同的顺序实际发生(以提高性能)。然后,您将收到与之前相同的最终结果,但如果系统本身出现问题(而不仅仅是应用程序本身),则可能无法保证崩溃恢复。这就是为什么事务解决方案是最好的选择,如果它是关键操作的原因。 - user541686
这并不是非常关键。只是一个典型的文件 -> 保存操作。 - Juan
2个回答

7

我不确定这有多安全,但是假设你的操作系统没有崩溃,猜猜看?有一个应用程序可以解决这个问题:File.Replace

File.Replace(tempFile, fileName, backupFileName);

我认为在关键情况下,您真正需要的是事务处理;只有这样才能保证数据不会丢失。请参阅这篇文章以获取.NET解决方案,但要注意它可能比简单的文件替换方案更难使用。


@jsoldi:嗯...为什么你的文件在不同的卷上?为什么不直接保存到当前卷呢? - user541686
@jsoldi: 为什么不直接写入到MemoryStream,然后将其直接写入到您想要的位置,而不需要一个临时副本呢? - user541686
这是一个可能性...在写文件的过程中,我仍然会创建原始文件的临时副本,以防万一出现任何问题。 - Juan
1
@jsoldi:不,我的意思是,将它写入到临时文件夹中,然后使用File.Replace用它替换原始文件,这样行不行? - user541686
是的,我正在考虑那个问题...让我再想一下 ;) - Juan
显示剩余2条评论

4
这是我通常的做法:
  1. 始终将数据写入新文件(例如hello.dat),并附加自增序列号或时间戳(确保唯一,可以使用毫秒级别等)--例如hello.dat.012345。如果该文件已存在,则生成另一个数字并重试。
  2. 保存后,请忘记它。做其他事情。
  3. 有一个后台进程来处理这些新文件。如果文件存在,请执行以下操作:
  4. 将原始文件重命名为备份文件,并附加序列号或时间戳hello.dat -> hello.dat.bak.023456
  5. 将最后一个文件重命名为原始名称hello.dat
  6. 删除备份文件
  7. 删除最后一个文件和原始文件之间的所有中间文件(它们都被最后一个文件覆盖了)。
  8. 返回第3步
如果在第3步到第8步之间出现任何故障,请发送警告消息。您不会丢失任何数据,但临时文件可能会积累。请定期清理您的目录。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接