using System;
using System.IO;
using Wayne.Lib.IO.UnitTest;
namespace Wayne.Lib.IO
{
///
/// A file writing stream that ensures that the files remain consistent even when a writing operation is interrupted.
/// The writing will be performed to a temporary file that will be exchanged with the target file when the writing has
/// completed. There is a static method, Cleanup that should be called when a program starts up that will clean up and
/// restore the files in the best possible state.
///
public class SafeFileWritingStream : Stream
{
#region Fields
private readonly IFileSupport fileSupport;
private readonly SafeFileWritingInterruptPoint interruptAtStage = SafeFileWritingInterruptPoint.DontInterrupt;
private readonly SafeFileInfo safeFileName;
private readonly Stream targetFile;
private readonly Stream tempOutputFile;
private bool closed;
#endregion
#region Construction
///
/// Creates a new instance of te SafeFileWriting Stream for the specified file.
///
///
/// If some of the files that should be not is accessible.
[Obsolete("Use constructor taking IFilesupport parameter.")]
public SafeFileWritingStream(string fileName)
: this(FileSupport.fileSupport, fileName, SafeFileWritingInterruptPoint.DontInterrupt)
{
}
///
/// Creates a new instance of te SafeFileWriting Stream for the specified file.
///
/// The filesupport implementation to use.
///
/// If some of the files that should be not is accessible.
public SafeFileWritingStream(IFileSupport fileSupport, string fileName)
: this(fileSupport, fileName, SafeFileWritingInterruptPoint.DontInterrupt)
{
}
///
/// Private constuctor used for unit testing.
///
/// The filesupport implementation to use.
///
///
private SafeFileWritingStream(IFileSupport fileSupport, string fileName, SafeFileWritingInterruptPoint interruptAtStage)
{
fileName = Paths.Parse(fileName);
this.fileSupport = fileSupport;
this.interruptAtStage = interruptAtStage;
SetWritingStage(SafeFileWritingInterruptPoint.WritingNotBegun);
safeFileName = SafeFileInfo.FromOriginalFileName(fileName);
try
{
if (fileSupport.FileExists(fileName))
targetFile = fileSupport.Open(fileName, FileMode.Open, FileAccess.Write, FileShare.Read);
}
catch (IOException ioException)
{
throw new SafeFileWritingIOException("Error opening target file (" + fileName + ")", ioException);
}
//If there is an -old file try to delete it, otherwise it is ok for the File.Delete to throw.
if (fileSupport.FileExists(safeFileName.OldFileName))
{
try
{
fileSupport.Delete(safeFileName.OldFileName);
}
catch (IOException ioException)
{
throw new SafeFileWritingIOException("Error deleting a -old file (" + safeFileName.OldFileName + ")", ioException);
}
}
try
{
tempOutputFile = fileSupport.Open(safeFileName.TempFileName, FileMode.Create, FileAccess.Write, FileShare.None);
}
catch (IOException ioException)
{
throw new SafeFileWritingIOException("Error opening the temp file (" + safeFileName.TempFileName + ")", ioException);
}
SetWritingStage(SafeFileWritingInterruptPoint.WritingOngoing);
}
#endregion
#region Properties
///
/// Always false, SafeFileStream is write-only
///
public override bool CanRead
{
get { return false; }
}
///
/// Always false, SafeFileStream is write-only
///
public override bool CanSeek
{
get { return false; }
}
///
/// Always true, SafeFileStream is write-only
///
public override bool CanWrite
{
get { return true; }
}
///
/// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
///
/// The stream is closed.
/// An I/O error occurs.
public override void Flush()
{
tempOutputFile.Flush();
}
///
/// Gets the length in bytes of the stream.
///
/// CanSeek for this stream is false.
/// An I/O error occurs, such as the file being closed.
public override long Length
{
get { return tempOutputFile.Length; }
}
///
/// Gets or sets the current position of this stream.
///
/// The stream does not support seeking.
public override long Position
{
get
{
return tempOutputFile.Position;
}
set
{
throw new NotSupportedException();
}
}
#endregion
#region Methods
///
/// Not supported
///
///
///
///
///
/// Always
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
///
/// Not supported. Stream is Write-only.
///
///
///
///
/// Always.
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
///
/// Not supported. Fast-forward writing only.
///
///
public override void SetLength(long value)
{
throw new NotSupportedException();
}
///
/// Writes a block of bytes to this stream using data from a buffer.
///
/// The buffer containing data to write to the stream.
/// The zero-based byte offset in array at which to begin copying bytes to the current stream.
/// The maximum number of bytes to be written to the current stream.
/// array is null.
/// The stream is closed.
/// An I/O error occurs.
/// offset and count describe an invalid range in array.
/// offset or count is negative.
public override void Write(byte[] buffer, int offset, int count)
{
tempOutputFile.Write(buffer, offset, count);
}
///
/// Closes the stream and overwrites the target file.
///
public override void Close()
{
base.Close();
if (!closed)
{
closed = true;
//Close the temporary file.
tempOutputFile.Close();
//Unlock the original file.
if (targetFile != null)
{
targetFile.Close();
//Rename the original file to .old
fileSupport.Move(safeFileName.OriginalFileName, safeFileName.OldFileName, 100, 100);
SetWritingStage(SafeFileWritingInterruptPoint.WritingCompleteOriginalFileRenamedToOld);
}
//Change name on the temporary file to the target file name
fileSupport.Move(safeFileName.TempFileName, safeFileName.OriginalFileName, 100, 100);
SetWritingStage(SafeFileWritingInterruptPoint.WritingCompleteTempFileRenamedToTargetFile);
if (targetFile != null)
{
//Delete the old file
fileSupport.Delete(safeFileName.OldFileName, 100, 100);
SetWritingStage(SafeFileWritingInterruptPoint.WritingComplete);
}
}
}
///
/// Private method that is used to enable interrupts in different phases of the writing
/// for the unit testing.
///
///
private void SetWritingStage(SafeFileWritingInterruptPoint writingStage)
{
if (writingStage == interruptAtStage)
{
targetFile.Close();
tempOutputFile.Close();
throw new SafeFileWritingInterruptedException();
}
}
#endregion
#region Static Methods
///
/// All file types that is written with the SafeFileWritingStream should be cleaned at certain points
/// to maintain the integrity. The typical place to place a call to this method is at the startup of
/// a module. If the module wrote something and was interrupted by a program shutdown, it can rescue some data with this
/// method.
///
///
///
/// Delegate that is called when a temp file is found and asks for action to take. This delegate may be invoked several times.
/// Token that is returned in the invokation of the temporaryFileFoundCallback.
/// Folder does not exist
[Obsolete("Use Cleanup method supplying file support.")]
public static void Cleanup(string folderPath, string pattern, EventHandler temporaryFileFoundCallback, object userToken)
{
Cleanup(FileSupport.fileSupport, folderPath, pattern, temporaryFileFoundCallback, userToken);
}
///
/// All file types that is written with the SafeFileWritingStream should be cleaned at certain points
/// to maintain the integrity. The typical place to place a call to this method is at the startup of
/// a module. If the module wrote something and was interrupted by a program shutdown, it can rescue some data with this
/// method.
///
/// The filesupport implementation to use.
///
///
/// Delegate that is called when a temp file is found and asks for action to take. This delegate may be invoked several times.
/// Token that is returned in the invokation of the temporaryFileFoundCallback.
/// Folder does not exist
public static void Cleanup(IFileSupport fileSupport, string folderPath, string pattern, EventHandler temporaryFileFoundCallback, object userToken)
{
if (fileSupport.DirectoryExists(folderPath))
{
foreach (SafeFileInfo safeFile in SafeFileInfo.GetFiles(fileSupport, folderPath, pattern))
{
//Stage 0 - No writing has been interrupted.
if (safeFile.OriginalFileExists && !safeFile.TempFileExists && !safeFile.OldFileExist)
{
//Do nothing
}
//Stage 1 - Writing has been done, the Temp file has been created, and a undefined percentage of the file has been written.
else if ((safeFile.OriginalFileExists && safeFile.TempFileExists) || //Overwrite scenario
(!safeFile.OriginalFileExists && !safeFile.OldFileExist && safeFile.TempFileExists)) //Create new file scenario
{
//Remove the Temp file and send event log with the content of the temp file.
fileSupport.Delete(safeFile.TempFileName);
SafeFileWritingCleanupEventArgs evArgs = new SafeFileWritingCleanupEventArgs(safeFile.TempFileName, userToken);
if (temporaryFileFoundCallback != null)
temporaryFileFoundCallback(null, evArgs);
if (evArgs.Action == SafeFileWritingCleanupAction.StoreInRestoredTempFileDir)
{
string restoredTempFilesDir = Paths.Combine("RestoredTemp");
fileSupport.EnsureDirectoryExists(restoredTempFilesDir);
string newFileName = string.Concat(Guid.NewGuid(), Path.GetFileName(safeFile.TempFileName));
fileSupport.Move(safeFile.TempFileName, Path.Combine(restoredTempFilesDir, newFileName), 100, 100);
}
else
{
fileSupport.Delete(safeFile.TempFileName);
}
}
//Stage 2- Writing has been completed, The original file has been renamed to old.
else if (safeFile.TempFileExists && safeFile.OldFileExist)
{
//Remove the Old file
fileSupport.Delete(safeFile.OldFileName);
//...and make the temp file the real file.
fileSupport.Move(safeFile.TempFileName, safeFile.OriginalFileName, 100, 100);
}
//Stage 3 - Temp file has been sucessfully renamed but old file still remains.
else if (safeFile.OriginalFileExists && safeFile.OldFileExist)
{
fileSupport.Delete(safeFile.OldFileName);
}
}
}
else
{
throw new ArgumentException("Invalid folder " + folderPath);
}
}
#endregion
}
}