Безопасное потоковое обновление файла - PullRequest
2 голосов
/ 28 ноября 2008

Мы выполняем обновления больших текстовых файлов, записывая новые записи во временный файл, а затем заменяя старый файл временным файлом. Сильно сокращенная версия:

var tpath = Path.GetTempFileName();
try
{
    using (var sf = new StreamReader(sourcepath))
    using (var tf = new StreamWriter(tpath))
    {
        string line;
        while ((line = sf.ReadLine()) != null)
            tf.WriteLine(UpdateLine(line));
    }

    File.Delete(sourcepath);
    File.Move(tpath, sourcepath);
}
catch
{
    File.Delete(tpath);
    throw;
}

Если что-то вызывает исключение (файл не найден, разрешения нет), исходный файл остается без изменений, и это то, что нам нужно.

Однако в коде есть следующие проблемы:

  1. Есть ли в реальной ситуации ситуация, когда Delete работает, но Move дает сбой? Это приведет к удалению исходных и обновленных данных. Это было бы плохо.

  2. Наиболее распространенная ошибка - исходный файл, открытый из другого приложения, и ошибка Delete. Это означает, что вся работа по обновлению отбрасывается. Есть ли способ узнать, можно ли удалить исходный файл при запуске, и отменить обновление, если нет?

  3. У нас есть пользователи, которые помещают в файлы свойства Сводка проводника Windows, такие как Заголовок или Комментарии. Они удаляются, когда мы удаляем файл. Есть ли способ скопировать свойства Summary старого файла в новый файл? Должны ли мы сделать это?

Ответы [ 8 ]

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

Обычный способ избежать проблемы «удалить, затем переместить не удается»:

  • Запись в файл .new
  • Переместить file.current в file.old
  • Переместить file.new в file.current
  • Удалить file.new

Затем, когда вы придете к чтению, используйте file.new, если file.current отсутствует, удалив file.old, если вы его видите.

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

Не уверен насчет копирования резюме и т. Д., Я боюсь.

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

Почему бы не попробовать сначала проверить атрибуты FileAttributes?

Попробуйте что-то вроде этого:

//If File is readonly
if ( (file.Attribute & System.FileAttributes.ReadOnly) == System.FileAttributes.ReadOnly ) 
        //Don't delete. 

Также попробуйте использовать .OpenWrite (). Если вы можете открыть файл для записи, он не доступен и в данный момент не используется. Вы можете открыть файл для записи, только если он в данный момент не открыт. Я не рекомендую это, но это может помочь вам.

  FileStream fs = File.OpenWrite(file);
  fs.Close();
  return false; 

Вы также можете использовать метод проверки FileLock. Примерно так:

protected virtual bool IsFileLocked(FileInfo file)
{
    try
    {
        using (file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None))
        {
           return false;
        }
    }

    catch (IOException)
    {
        return true;
    }

}

Вы также можете проверить FileIOPermission.Write. Это позволяет увидеть, доступен ли файл для записи (и может ли он быть удален).

fileIOPerm = New FileIOPermission(FileIOPermissionAccess.Write, FileSpec);
fileIOPerm.Demand();

Что касается вопроса № 3 в исходном сообщении ... Вы всегда можете переместить файлы во временную папку, используя File.Copy (path1, path2, true). Возможно, вы захотите использовать временную папку и написать лучшую логику для манипулирования файлами.

Если вы решили использовать временную папку или временные файлы / промежуточные файлы, то вы бы также исправили свой вопрос №2. Попробуйте сначала переместить файлы.

1 голос
/ 29 ноября 2008

Много хороших предложений. Я смог решить проблемы с:

var sInfo = new FileInfo(sourcePath);
if (sInfo.IsReadOnly)
    throw new IOException("File '" + sInfo.FullName + "' is read-only.");

var tPath = Path.GetTempFileName();
try
{
    // This throws if sourcePath does not exist, is opened, or is not readable.
    using (var sf = sInfo.OpenText())
    using (var tf = new StreamWriter(tPath))
    {
        string line;
        while ((line = sf.ReadLine()) != null)
            tf.WriteLine(UpdateLine(line));
    }

    string backupPath = sInfo.FullName + ".bak";
    if (File.Exists(backupPath))
        File.Delete(backupPath);

    File.Move(tPath, backupPath);
    tPath = backupPath;
    File.Replace(tPath, sInfo.FullName, null);
}
catch (Exception ex)
{
    File.Delete(tPath);
    throw new IOException("File '" + sInfo.FullName + "' could not be overwritten.", ex);
}

OpenText выдает, если исходный файл открыт или не читается, а обновление не выполнено. Если что-то выбрасывает, оригинальный файл остается без изменений. Replace копирует свойства Summary старых файлов в новый файл. Это работает, даже если исходный файл находится на другом томе, чем временная папка.

1 голос
/ 28 ноября 2008

Как уже упоминалось, вам действительно следует изучить ReplaceFile, который призван помочь вам в том, что вы делаете. Функция .NET - это просто оболочка для функции Win32, в которой можно надеяться, что проблемы атомарности были решены.

1 голос
/ 28 ноября 2008

Транзакционная NTFS в Windows Vista или более поздней версии может быть полезна для вашего сценария.

1 голос
/ 28 ноября 2008

Какой-то "грязный" трюк.

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

  2. Я думаю, что эта статья поможет вам там MSDN

  3. Если пользователям нужны эти «заголовки» и «комментарии», вы должны оставить их. Я никогда не пытался скопировать их из одного файла в другой, поэтому я не знаю, как вам там помочь.

0 голосов
/ 29 ноября 2008

В этом фрагменте кода показана методика получения монопольного доступа к файлу (в данном случае читаем):

// Try to open a file exclusively
FileInfo fi = new FileInfo(fullFilePath);

int attempts = maxAttempts;
do
{
    try
    {
        // Try to open for reading with exclusive access...
        fs = fi.Open(FileMode.Open, FileAccess.Read, FileShare.None);
    }
    // Ignore any errors... 
    catch { }

    if (fs != null)
    {
        break;
    }
    else
    {
        Thread.Sleep(100);
    }
}
while (--attempts > 0);

// Did we manage to open file exclusively?
if (fs != null)
{
    // use open file....

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

Мне показалось полезным обернуть этот шаблон в его собственный класс.

class Program {
    static void Main( string[] args ) {
        using( var ft = new FileTransaction( @"C:\MyDir\MyFile.txt" ) )
        using( var sw = new StreamWriter( ft.TempPath ) ) {
            sw.WriteLine( "Hello" );
            ft.Commit();
        }
    }
}

public class FileTransaction :IDisposable {
    public string TempPath { get; private set; }
    private readonly string filePath;

    public FileTransaction( string filePath ) {
        this.filePath = filePath;
        this.TempPath = Path.GetTempFileName();
    }

    public void Dispose() {
        if( TempPath != null ) {
            try {
                File.Delete( TempPath );
            }
            catch { }
        }
    }

    public void Commit() {
        try {
            var oldPath = filePath + ".old";
            File.Move( filePath, oldPath );
        }
        catch {}

        File.Move( TempPath, filePath );

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