/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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. */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; namespace Amazon.Runtime.Internal.Util { /// /// Provides line-based read/write access to a file. /// The file can be read into memory, changed, then written back to disk. /// When the file is persisted back to disk, an optimistic concurrency /// check is performed to make sure the file hasn't changed since it was /// originally read. /// /// This class is not threadsafe. /// public class OptimisticLockedTextFile { /// /// a full copy of the original file /// This is used for optimistic concurrency. /// Note that this assumes a small file and does not scale large files. /// private string OriginalContents { get; set; } /// /// path of the file /// public string FilePath { get; private set; } /// /// Read/write access to the lines that make up the file. /// Any changes to this List are persisted back to disk when Persist() is called. /// /// NOTE: /// The lines have the original line endings on them to preserve the /// original text as much as possible. /// public List Lines { get; private set; } /// /// Construct a new OptimisticLockedTextFile. /// /// path of the file public OptimisticLockedTextFile(string filePath) { FilePath = filePath; Read(); } /// /// Persist changes to disk after an optimistic concurrency check is completed. /// public void Persist() { var newContents = ToString(); var path = Path.GetDirectoryName(FilePath); if (!Directory.Exists(path)) Directory.CreateDirectory(path); // open the file with exclusive access using (var fileStream = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { // get a current copy of the file string currentContents = null; using (var streamReader = new StreamReader(fileStream)) { currentContents = streamReader.ReadToEnd(); // optimistic concurrency check - make sure the file hasn't changed since it was read if (string.Equals(currentContents, OriginalContents, StringComparison.Ordinal)) { // write the new contents fileStream.Seek(0, SeekOrigin.Begin); using (var streamWriter = new StreamWriter(fileStream)) { streamWriter.Write(newContents); streamWriter.Flush(); // set the length in case the new contents are shorter than the old contents fileStream.Flush(); fileStream.SetLength(fileStream.Position); OriginalContents = newContents; } } else { throw new IOException(string.Format(CultureInfo.InvariantCulture, "Cannot write to file {0}. The file has been modified since it was last read.", FilePath)); } } } } public override string ToString() { // Make sure all lines have endings, with the exception of the last line. var contents = new StringBuilder(); for (int i = 0; i < Lines.Count; i++) { var line = Lines[i]; if (i < Lines.Count - 1 && !HasEnding(line)) { contents.AppendLine(line); } else { contents.Append(line); } } return contents.ToString(); } private void Read() { // Store a copy of the file for checking concurrency OriginalContents = ""; if (File.Exists(FilePath)) { try { OriginalContents = File.ReadAllText(FilePath); } catch (FileNotFoundException) { // This is OK. The Persist() method will create it if necessary. } catch (DirectoryNotFoundException) { // This is OK. The Persist() method will create it if necessary. } } // Parse the lines ourselves since we need to preserve the line endings. // Parsing ourselves also avoids a race condition: // Doing ReadAllText then ReadAllLines would leave a small gap in time where the file could be changed. Lines = ReadLinesWithEndings(OriginalContents); } private static bool HasEnding(string line) { var lastChar = line[line.Length - 1]; return lastChar == '\n' || lastChar == '\r'; } private static List ReadLinesWithEndings(string str) { var lines = new List(); var length = str.Length; var i = 0; var currentLineStart = 0; while (i < length) { if (str[i] == '\r') { i++; if (i < length && str[i] == '\n') { i++; } lines.Add(str.Substring(currentLineStart, i - currentLineStart)); currentLineStart = i; } else if (str[i] == '\n') { i++; lines.Add(str.Substring(currentLineStart, i - currentLineStart)); currentLineStart = i; } else { i++; } } if (currentLineStart < i) { lines.Add(str.Substring(currentLineStart, i - currentLineStart)); } return lines; } } }