// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Diagnostics; using System.IO; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage { /// /// Wrapper for a file stream, providing simplified creation, deletion, read, and write /// functionality. /// public class FileStreamWrapper : IFileStreamWrapper { #region Member Variables private byte[] buffer; private int bufferDataSize; private FileStream fileStream; private long startOffset; private long currentOffset; #endregion /// /// Constructs a new FileStreamWrapper and initializes its state. /// public FileStreamWrapper() { // Initialize the internal state bufferDataSize = 0; startOffset = 0; currentOffset = 0; } #region IFileStreamWrapper Implementation /// /// Initializes the wrapper by creating the internal buffer and opening the requested file. /// If the file does not already exist, it will be created. /// /// Name of the file to open/create /// The length of the internal buffer /// /// Whether or not the wrapper will be used for reading. If true, any calls to a /// method that writes will cause an InvalidOperationException /// public void Init(string fileName, int bufferLength, FileAccess accessMethod) { // Sanity check for valid buffer length, fileName, and accessMethod if (bufferLength <= 0) { throw new ArgumentOutOfRangeException(nameof(bufferLength), "Buffer length must be a positive value"); } if (string.IsNullOrWhiteSpace(fileName)) { throw new ArgumentNullException(nameof(fileName), "File name cannot be null or whitespace"); } if (accessMethod == FileAccess.Write) { throw new ArgumentException("Access method cannot be write-only", nameof(fileName)); } // Setup the buffer buffer = new byte[bufferLength]; // Open the requested file for reading/writing, creating one if it doesn't exist fileStream = new FileStream(fileName, FileMode.OpenOrCreate, accessMethod, FileShare.ReadWrite, bufferLength, false /*don't use asyncio*/); } /// /// Reads data into a buffer from the current offset into the file /// /// The buffer to output the read data to /// The number of bytes to read into the buffer /// The number of bytes read public int ReadData(byte[] buf, int bytes) { return ReadData(buf, bytes, currentOffset); } /// /// Reads data into a buffer from the specified offset into the file /// /// The buffer to output the read data to /// The number of bytes to read into the buffer /// The offset into the file to start reading bytes from /// The number of bytes read public int ReadData(byte[] buf, int bytes, long offset) { // Make sure that we're initialized before performing operations if (buffer == null) { throw new InvalidOperationException("FileStreamWrapper must be initialized before performing operations"); } MoveTo(offset); int bytesCopied = 0; while (bytesCopied < bytes) { int bufferOffset, bytesToCopy; GetByteCounts(bytes, bytesCopied, out bufferOffset, out bytesToCopy); Buffer.BlockCopy(buffer, bufferOffset, buf, bytesCopied, bytesToCopy); bytesCopied += bytesToCopy; if (bytesCopied < bytes && // did not get all the bytes yet bufferDataSize == buffer.Length) // since current data buffer is full we should continue reading the file { // move forward one full length of the buffer MoveTo(startOffset + buffer.Length); } else { // copied all the bytes requested or possible, adjust the current buffer pointer currentOffset += bytesToCopy; break; } } return bytesCopied; } /// /// Writes data to the underlying filestream, with buffering. /// /// The buffer of bytes to write to the filestream /// The number of bytes to write /// The number of bytes written public int WriteData(byte[] buf, int bytes) { // Make sure that we're initialized before performing operations if (buffer == null) { throw new InvalidOperationException("FileStreamWrapper must be initialized before performing operations"); } if (!fileStream.CanWrite) { throw new InvalidOperationException("This FileStreamWrapper canot be used for writing"); } int bytesCopied = 0; while (bytesCopied < bytes) { int bufferOffset, bytesToCopy; GetByteCounts(bytes, bytesCopied, out bufferOffset, out bytesToCopy); Buffer.BlockCopy(buf, bytesCopied, buffer, bufferOffset, bytesToCopy); bytesCopied += bytesToCopy; // adjust the current buffer pointer currentOffset += bytesToCopy; if (bytesCopied < bytes) // did not get all the bytes yet { Debug.Assert((int)(currentOffset - startOffset) == buffer.Length); // flush buffer Flush(); } } Debug.Assert(bytesCopied == bytes); return bytesCopied; } /// /// Flushes the internal buffer to the filestream /// public void Flush() { // Make sure that we're initialized before performing operations if (buffer == null) { throw new InvalidOperationException("FileStreamWrapper must be initialized before performing operations"); } if (!fileStream.CanWrite) { throw new InvalidOperationException("This FileStreamWrapper cannot be used for writing"); } // Make sure we are at the right place in the file Debug.Assert(fileStream.Position == startOffset); int bytesToWrite = (int)(currentOffset - startOffset); fileStream.Write(buffer, 0, bytesToWrite); startOffset += bytesToWrite; fileStream.Flush(); Debug.Assert(startOffset == currentOffset); } /// /// Deletes the given file (ideally, created with this wrapper) from the filesystem /// /// The path to the file to delete public static void DeleteFile(string fileName) { File.Delete(fileName); } #endregion /// /// Perform calculations to determine how many bytes to copy and what the new buffer offset /// will be for copying. /// /// Number of bytes requested to copy /// Number of bytes copied so far /// New offset to start copying from/to /// Number of bytes to copy in this iteration private void GetByteCounts(int bytes, int bytesCopied, out int bufferOffset, out int bytesToCopy) { bufferOffset = (int) (currentOffset - startOffset); bytesToCopy = bytes - bytesCopied; if (bytesToCopy > buffer.Length - bufferOffset) { bytesToCopy = buffer.Length - bufferOffset; } } /// /// Moves the internal buffer to the specified offset into the file /// /// Offset into the file to move to private void MoveTo(long offset) { if (buffer.Length > bufferDataSize || // buffer is not completely filled offset < startOffset || // before current buffer start offset >= (startOffset + buffer.Length)) // beyond current buffer end { // init the offset startOffset = offset; // position file pointer fileStream.Seek(startOffset, SeekOrigin.Begin); // fill in the buffer bufferDataSize = fileStream.Read(buffer, 0, buffer.Length); } // make sure to record where we are currentOffset = offset; } #region IDisposable Implementation private bool disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) { return; } if (disposing && fileStream != null) { if(fileStream.CanWrite) { Flush(); } fileStream.Dispose(); } disposed = true; } ~FileStreamWrapper() { Dispose(false); } #endregion } }