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 } }