Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to safely save data to an existing file with C#?

Tags:

c#

io

How do you safely save data to a file that already exists in C#? I have some data that is serialized to a file and I'm pretty sure is not a good idea to safe directly to the file because if anything goes wrong the file will get corrupted and the previous version will get lost.

So this is what I've been doing so far:

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);

The problem is that when I tried to save to a file that was in my Dropbox, sometimes I got an exception telling me that it cannot save to a file that already exists. Apparently the first File.Delete(fileName); didn't delete the file right away but after a little bit. So I got an exception in the File.Move(tempFile, fileName); because the file existed and then the file got erased and my file got lost.

I've used other applications with files in my Dropbox and somehow they manage to not mess it up. When I'm trying to save to a file in my Dropbox folder, sometimes I get a message telling me that the file is being used or stuff like that but I never had a problem with a file being erased.

So what would it be the standard / best practice here?

OK this is what I came up with after reading all answers:

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 is just a little function I made that creates an string with n random digits.

I don't see how could this mess up the original file unless the OS is going wacko. It's pretty close to Hans's answer except that I first save the file to a temporary file so that, if something goes wrong when serializing, I don't need to rename the file back to it's original name, which can also go wrong. Please! let me know if you find any flaw.

like image 717
Juan Avatar asked Mar 22 '11 04:03

Juan


2 Answers

I'm not sure how secure this is, but assuming your OS isn't crashing, guess what? There's an app for that: File.Replace

File.Replace(tempFile, fileName, backupFileName);

I think what you really need in a critical situation is transactions; only then can you guarantee against the loss of data. Take a look at this article for a .NET solution, but be aware that it might be a bit harder to use than a simple file replacement solution.

like image 189
user541686 Avatar answered Sep 24 '22 15:09

user541686


This is the way I usually do it:

  1. Always write to a new file (say hello.dat) with an auto-increment serial number attached, or the time attached (just make sure that it is unique, so use ticks or microseconds etc.) -- say hello.dat.012345. If that file exists, generate another number and try again.
  2. After saving, forget about it. Do other things.
  3. Have a background process that keeps running through these new files. If exists, do the following:
  4. Rename the original file to a backup with a serial number or timestamp hello.dat -> hello.dat.bak.023456
  5. Rename the last file to the original name hello.dat
  6. Remove the backup file
  7. Remove all intermediate files between the last file and the original file (they are all overwritten by the last file anyway).
  8. Loop back to #3

If there is any failure anywhere between #3 and #8, send a warning message. You're never losing any data, but temp files may build up. Clean your directory once in a while.

like image 23
Stephen Chung Avatar answered Sep 24 '22 15:09

Stephen Chung