using System; using System.IO; using Wayne.Lib.IO.UnitTest; namespace Wayne.Lib.IO { /// <summary> /// 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. /// </summary> 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 /// <summary> /// Creates a new instance of te SafeFileWriting Stream for the specified file. /// </summary> /// <param name="fileName"></param> /// <exception cref="SafeFileWritingIOException">If some of the files that should be not is accessible.</exception> [Obsolete("Use constructor taking IFilesupport parameter.")] public SafeFileWritingStream(string fileName) : this(FileSupport.fileSupport, fileName, SafeFileWritingInterruptPoint.DontInterrupt) { } /// <summary> /// Creates a new instance of te SafeFileWriting Stream for the specified file. /// </summary> /// <param name="fileSupport">The filesupport implementation to use.</param> /// <param name="fileName"></param> /// <exception cref="SafeFileWritingIOException">If some of the files that should be not is accessible.</exception> public SafeFileWritingStream(IFileSupport fileSupport, string fileName) : this(fileSupport, fileName, SafeFileWritingInterruptPoint.DontInterrupt) { } /// <summary> /// Private constuctor used for unit testing. /// </summary> /// <param name="fileSupport">The filesupport implementation to use.</param> /// <param name="fileName"></param> /// <param name="interruptAtStage"></param> 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 /// <summary> /// Always false, SafeFileStream is write-only /// </summary> public override bool CanRead { get { return false; } } /// <summary> /// Always false, SafeFileStream is write-only /// </summary> public override bool CanSeek { get { return false; } } /// <summary> /// Always true, SafeFileStream is write-only /// </summary> public override bool CanWrite { get { return true; } } /// <summary> /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. /// </summary> /// <exception cref="ObjectDisposedException">The stream is closed.</exception> /// <exception cref="IOException">An I/O error occurs.</exception> public override void Flush() { tempOutputFile.Flush(); } /// <summary> /// Gets the length in bytes of the stream. /// </summary> /// <exception cref="NotSupportedException">CanSeek for this stream is false.</exception> /// <exception cref="IOException">An I/O error occurs, such as the file being closed.</exception> public override long Length { get { return tempOutputFile.Length; } } /// <summary> /// Gets or sets the current position of this stream. /// </summary> /// <exception cref="NotSupportedException">The stream does not support seeking.</exception> public override long Position { get { return tempOutputFile.Position; } set { throw new NotSupportedException(); } } #endregion #region Methods /// <summary> /// Not supported /// </summary> /// <param name="buffer"></param> /// <param name="offset"></param> /// <param name="count"></param> /// <returns></returns> /// <exception cref="NotSupportedException">Always</exception> public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } /// <summary> /// Not supported. Stream is Write-only. /// </summary> /// <param name="offset"></param> /// <param name="origin"></param> /// <returns></returns> /// <exception cref="NotSupportedException">Always.</exception> public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } /// <summary> /// Not supported. Fast-forward writing only. /// </summary> /// <param name="value"></param> public override void SetLength(long value) { throw new NotSupportedException(); } /// <summary> /// Writes a block of bytes to this stream using data from a buffer. /// </summary> /// <param name="buffer">The buffer containing data to write to the stream.</param> /// <param name="offset">The zero-based byte offset in array at which to begin copying bytes to the current stream.</param> /// <param name="count">The maximum number of bytes to be written to the current stream.</param> /// <exception cref="ArgumentNullException">array is null.</exception> /// <exception cref="ObjectDisposedException">The stream is closed.</exception> /// <exception cref="IOException">An I/O error occurs.</exception> /// <exception cref="ArgumentException">offset and count describe an invalid range in array.</exception> /// <exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception> public override void Write(byte[] buffer, int offset, int count) { tempOutputFile.Write(buffer, offset, count); } /// <summary> /// Closes the stream and overwrites the target file. /// </summary> 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); } } } /// <summary> /// Private method that is used to enable interrupts in different phases of the writing /// for the unit testing. /// </summary> /// <param name="writingStage"></param> private void SetWritingStage(SafeFileWritingInterruptPoint writingStage) { if (writingStage == interruptAtStage) { targetFile.Close(); tempOutputFile.Close(); throw new SafeFileWritingInterruptedException(); } } #endregion #region Static Methods /// <summary> /// 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. /// </summary> /// <param name="folderPath"></param> /// <param name="pattern"></param> /// <param name="temporaryFileFoundCallback">Delegate that is called when a temp file is found and asks for action to take. This delegate may be invoked several times.</param> /// <param name="userToken">Token that is returned in the invokation of the temporaryFileFoundCallback.</param> /// <exception cref="ArgumentException">Folder does not exist</exception> [Obsolete("Use Cleanup method supplying file support.")] public static void Cleanup(string folderPath, string pattern, EventHandler<SafeFileWritingCleanupEventArgs> temporaryFileFoundCallback, object userToken) { Cleanup(FileSupport.fileSupport, folderPath, pattern, temporaryFileFoundCallback, userToken); } /// <summary> /// 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. /// </summary> /// <param name="fileSupport">The filesupport implementation to use.</param> /// <param name="folderPath"></param> /// <param name="pattern"></param> /// <param name="temporaryFileFoundCallback">Delegate that is called when a temp file is found and asks for action to take. This delegate may be invoked several times.</param> /// <param name="userToken">Token that is returned in the invokation of the temporaryFileFoundCallback.</param> public static void Cleanup(IFileSupport fileSupport, string folderPath, string pattern, EventHandler<SafeFileWritingCleanupEventArgs> 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); } } } } #endregion } }