| 
 | 1 | +#nullable enable  | 
 | 2 | + | 
 | 3 | +using System.Diagnostics;  | 
 | 4 | +using System.IO;  | 
 | 5 | + | 
 | 6 | +using BizHawk.Common.StringExtensions;  | 
 | 7 | + | 
 | 8 | +namespace BizHawk.Client.Common  | 
 | 9 | +{  | 
 | 10 | +	internal class FileStreamWithTemp : IDisposable  | 
 | 11 | +	{  | 
 | 12 | +		private FileStream? _stream; // is never null until this.Dispose()  | 
 | 13 | +		public FileStream Stream  | 
 | 14 | +		{  | 
 | 15 | +			get => _stream ?? throw new ObjectDisposedException("Cannot access a disposed FileStream.");  | 
 | 16 | +		}  | 
 | 17 | +		public string FinalPath;  | 
 | 18 | +		public string TempPath;  | 
 | 19 | + | 
 | 20 | +		public bool UsingTempFile => TempPath != FinalPath;  | 
 | 21 | + | 
 | 22 | +		private bool _finished = false;  | 
 | 23 | + | 
 | 24 | +		private FileStreamWithTemp(string final, string temp)  | 
 | 25 | +		{  | 
 | 26 | +			FinalPath = final;  | 
 | 27 | +			TempPath = temp;  | 
 | 28 | +			_stream = new(temp, FileMode.Create, FileAccess.Write);  | 
 | 29 | +		}  | 
 | 30 | + | 
 | 31 | +		// There is no public constructor. This is the only way to create an instance.  | 
 | 32 | +		public static FileWriteResult<FileStreamWithTemp> Create(string path)  | 
 | 33 | +		{  | 
 | 34 | +			string writePath = path;  | 
 | 35 | +			try  | 
 | 36 | +			{  | 
 | 37 | +				// If the file already exists, we will write to a temporary location first and preserve the old one until we're done.  | 
 | 38 | +				if (File.Exists(path))  | 
 | 39 | +				{  | 
 | 40 | +					writePath = path.InsertBeforeLast('.', ".saving", out bool inserted);  | 
 | 41 | +					if (!inserted) writePath = $"{path}.saving";  | 
 | 42 | + | 
 | 43 | +					if (File.Exists(writePath))  | 
 | 44 | +					{  | 
 | 45 | +						// The user should probably have dealt with this on the previously failed save.  | 
 | 46 | +						// But maybe we should support plain old "try again", so let's delete it.  | 
 | 47 | +						File.Delete(writePath);  | 
 | 48 | +					}  | 
 | 49 | +				}  | 
 | 50 | +				return new(new FileStreamWithTemp(path, writePath), writePath);  | 
 | 51 | +			}  | 
 | 52 | +			catch (IOException ex)  | 
 | 53 | +			{  | 
 | 54 | +				return new(FileWriteEnum.FailedToOpen, writePath, ex);  | 
 | 55 | +			}  | 
 | 56 | +		}  | 
 | 57 | + | 
 | 58 | +		/// <summary>  | 
 | 59 | +		/// This method must be called after writing has finished and must not be called twice.  | 
 | 60 | +		/// Dispose will be called regardless of the result.  | 
 | 61 | +		/// </summary>  | 
 | 62 | +		/// <exception cref="InvalidOperationException">If called twice.</exception>  | 
 | 63 | +		public FileWriteResult CloseAndDispose()  | 
 | 64 | +		{  | 
 | 65 | +			// In theory it might make sense to allow the user to try again if we fail inside this method.  | 
 | 66 | +			// If we implement that, it is probably best to make a static method that takes a FileWriteResult.  | 
 | 67 | +			// So even then, this method should not ever be called twice.  | 
 | 68 | +			if (_finished) throw new InvalidOperationException("Cannot close twice.");  | 
 | 69 | + | 
 | 70 | +			_finished = true;  | 
 | 71 | +			Dispose();  | 
 | 72 | + | 
 | 73 | +			if (!UsingTempFile) return new(FileWriteEnum.Success, FinalPath, null);  | 
 | 74 | + | 
 | 75 | +			if (File.Exists(FinalPath))  | 
 | 76 | +			{  | 
 | 77 | +				try  | 
 | 78 | +				{  | 
 | 79 | +					File.Delete(FinalPath);  | 
 | 80 | +				}  | 
 | 81 | +				catch (IOException ex)  | 
 | 82 | +				{  | 
 | 83 | +					return new(FileWriteEnum.FailedToDeleteOldFile, TempPath, ex);  | 
 | 84 | +				}  | 
 | 85 | +			}  | 
 | 86 | +			try  | 
 | 87 | +			{  | 
 | 88 | +				File.Move(TempPath, FinalPath);  | 
 | 89 | +			}  | 
 | 90 | +			catch (IOException ex)  | 
 | 91 | +			{  | 
 | 92 | +				return new(FileWriteEnum.FailedToRename, TempPath, ex);  | 
 | 93 | +			}  | 
 | 94 | + | 
 | 95 | +			return new(FileWriteEnum.Success, FinalPath, null);  | 
 | 96 | +		}  | 
 | 97 | + | 
 | 98 | +		/// <summary>  | 
 | 99 | +		/// Closes and deletes the file. Use if there was an error while writing.  | 
 | 100 | +		/// Do not call <see cref="CloseAndDispose"/> after this.  | 
 | 101 | +		/// </summary>  | 
 | 102 | +		public void Abort()  | 
 | 103 | +		{  | 
 | 104 | +			if (_dispoed) throw new ObjectDisposedException("Cannot use a disposed file stream.");  | 
 | 105 | +			_finished = true;  | 
 | 106 | +			Dispose();  | 
 | 107 | + | 
 | 108 | +			try  | 
 | 109 | +			{  | 
 | 110 | +				// Delete because the file is almost certainly useless and just clutter.  | 
 | 111 | +				File.Delete(TempPath);  | 
 | 112 | +			}  | 
 | 113 | +			catch { /* eat? this is probably not very important */ }  | 
 | 114 | +		}  | 
 | 115 | + | 
 | 116 | +		private bool _dispoed;  | 
 | 117 | +		public void Dispose()  | 
 | 118 | +		{  | 
 | 119 | +			if (_dispoed) return;  | 
 | 120 | +			_dispoed = true;  | 
 | 121 | + | 
 | 122 | +			_stream!.Flush(flushToDisk: true);  | 
 | 123 | +			_stream.Dispose();  | 
 | 124 | +			_stream = null;  | 
 | 125 | + | 
 | 126 | +			// The caller should call CloseAndDispose and handle potential failure.  | 
 | 127 | +			Debug.Assert(_finished, $"{nameof(FileWriteResult)} should not be disposed before calling {nameof(CloseAndDispose)}");  | 
 | 128 | +		}  | 
 | 129 | +	}  | 
 | 130 | +}  | 
0 commit comments