#region Copyright 2010-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.IO; using System.Globalization; using CSharpTest.Net.Collections; using CSharpTest.Net.Utils; namespace CSharpTest.Net.IO { /// /// Creates a temp file based on the given file being replaced and when a call to Commit() is /// made the target file is replaced with the current contents of the temporary file. Use the /// TransactFile if you MUST be certain to succeed then Commit(), otherwise this implementation /// provides a 'good-enough' transaction and is optimized for larger files. /// public class ReplaceFile : TempFile, Interfaces.ITransactable { Stream _lock; bool _committed; readonly bool _created; readonly string _targetFile; readonly string _backupExt; /// /// Derives a new filename that doesn't exist from the provided name, ie. file.txt becomes file.txt.~0001 /// /// the name of the file /// [out] the temp file name /// A stream with exclusive write access to the file public static Stream CreateDerivedFile(string originalPath, out string tempfilePath) { string tempFile = null; int tempInt, ordinal = 0; int maxAttempt = 10; if (!Path.IsPathRooted(originalPath)) originalPath = Path.GetFullPath(originalPath); SetList existingFiles = new SetList( Directory.GetFiles(Path.GetDirectoryName(originalPath), Path.GetFileName(originalPath) + ".~????", SearchOption.TopDirectoryOnly) ); if (existingFiles.Count > 0) { string extension = Path.GetExtension(existingFiles[existingFiles.Count - 1]); if (extension.Length == 6 && int.TryParse(extension.Substring(2), NumberStyles.AllowHexSpecifier, null, out tempInt)) ordinal = Math.Max(ordinal, tempInt); } Stream stream = null; while (stream == null) { while (tempFile == null || File.Exists(tempFile)) tempFile = String.Format("{0}.~{1:x4}", originalPath, ++ordinal); try { stream = File.Open(tempFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read); } catch (System.IO.IOException) { if (maxAttempt-- <= 0) throw; tempFile = null; stream = null; } } tempfilePath = tempFile; return stream; } /// /// Creates a temp file based on the given file being replaced and when a call to Commit() is /// made the target file is replaced with the current contents of the temporary file. /// public ReplaceFile(string targetName) : base(null) { if (!Path.IsPathRooted(targetName)) targetName = Path.GetFullPath(targetName); //hold an exclusive write-lock until we are committed. _created = !File.Exists(targetName); _lock = File.Open(targetName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); try { _committed = false; string tempFile; CreateDerivedFile(targetName, out tempFile).Dispose(); base.TempPath = tempFile; _targetFile = targetName; _backupExt = null; } catch { this.Dispose(); throw; } } /// /// Creates a backup of the target file when replacing using the extension provided /// /// The name of the file to replace /// A valid file extension beginning with '.' public ReplaceFile(string targetName, string backupExtension) : this(targetName) { try { if (!FileUtils.IsValidExtension(backupExtension)) throw new ArgumentException(Resources.InvalidFileExtension(backupExtension)); _backupExt = backupExtension; } catch { this.Dispose(); throw; } } /// /// Returns the originally provided filename that is being replaced /// public string TargetFile { get { return _targetFile; } } /// /// Commits the replace operation on the file /// public void Commit() { _committed = true; Dispose(true); } /// /// Aborts the operation and reverts pending changes /// public void Rollback() { Check.Assert(_committed == false); Dispose(true); } /// /// Disposes of the open stream and the temporary file. /// protected override void Dispose(bool disposing) { if (_lock != null) { //here is the optimistic part, if someone else interrupts this process and locks //the _targetfile we are going to fail. _lock.Dispose(); _lock = null; if (_committed && disposing) { string backupFile = null; if (_backupExt != null) backupFile = Path.ChangeExtension(_targetFile, _backupExt); //doesn't work in Win95, but the what does? File.Replace(TempPath, _targetFile, backupFile, true); } else if (_created && !_committed && disposing) { try { File.Delete(_targetFile); } catch { } } } base.Dispose(disposing); } } }