diff --git a/MFilesAPI.Extensions.Tests/Files/Downloading/FileDownloadStream/FileDownloadStream.cs b/MFilesAPI.Extensions.Tests/Files/Downloading/FileDownloadStream/FileDownloadStream.cs index 8c50267..03979a4 100644 --- a/MFilesAPI.Extensions.Tests/Files/Downloading/FileDownloadStream/FileDownloadStream.cs +++ b/MFilesAPI.Extensions.Tests/Files/Downloading/FileDownloadStream/FileDownloadStream.cs @@ -403,8 +403,8 @@ public void ReadOpensDownloadSessionIfNotOpen() // Mock a download session to return. var downloadSessionMock = new Mock(); downloadSessionMock.SetupGet(m => m.DownloadID).Returns(1); - downloadSessionMock.SetupGet(m => m.FileSize).Returns(1000); - downloadSessionMock.SetupGet(m => m.FileSize32).Returns(1000); + downloadSessionMock.SetupGet(m => m.FileSize).Returns(3); + downloadSessionMock.SetupGet(m => m.FileSize32).Returns(3); return downloadSessionMock.Object; }) .Verifiable(); @@ -441,7 +441,7 @@ public void ReadOpensDownloadSessionIfNotOpen() // Attempt to read. byte[] data = new byte[4096]; - stream.Read(data, 0, 4096); + Assert.AreEqual(3, stream.Read(data, 0, 4096)); // Ensure that the download session is not empty. Assert.IsNotNull(stream.DownloadSession); @@ -451,6 +451,94 @@ public void ReadOpensDownloadSessionIfNotOpen() vaultObjectFileOperationsMock.Verify(); } + [TestMethod] + public void ReadWithLargeBlockSizeRetrievesMultipleBlocks() + { + + // Set up a file to download. + var objectFileMock = new Mock(); + objectFileMock.SetupGet(m => m.ID).Returns(12345); + objectFileMock.SetupGet(m => m.Version).Returns(1); + + // Set up the vault object file operations mock. + var vaultObjectFileOperationsMock = new Mock(); + + // We don't really care what we download, as long as we can check it happened. + vaultObjectFileOperationsMock + .Setup(m => m.DownloadFileInBlocks_BeginEx + ( + Moq.It.IsAny(), + Moq.It.IsAny(), + Moq.It.IsAny() + )) + .Returns((int receivedFileId, int receivedFileVersion, MFFileFormat receivedFileFormat) => + { + // Mock a download session to return. + var downloadSessionMock = new Mock(); + downloadSessionMock.SetupGet(m => m.DownloadID).Returns(1); + downloadSessionMock.SetupGet(m => m.FileSize).Returns(8 * 1024 * 1024 + 4); + downloadSessionMock.SetupGet(m => m.FileSize32).Returns(8 * 1024 * 1024 + 4); + return downloadSessionMock.Object; + }) + .Verifiable(); + + // When DownloadFileInBlocks_ReadBlock is called (reading a block of content), return something. + vaultObjectFileOperationsMock + .Setup(m => m.DownloadFileInBlocks_ReadBlock + ( + Moq.It.IsAny(), + Moq.It.IsAny(), + Moq.It.IsAny() + )) + .Returns((int downloadSession, int blockSize, long offset) => + { + if (offset < 8 * 1024 * 1024) + return new byte[4 * 1024 * 1024]; + return new byte[4]; + }) + .Verifiable(); + + // Set up the mock vault. + var vaultMock = new Mock(); + vaultMock + .SetupGet(m => m.ObjectFileOperations) + .Returns(vaultObjectFileOperationsMock.Object); + + // Create the stream. + var stream = new FileDownloadStreamProxy(objectFileMock.Object, vaultMock.Object); + + // Read a large set of data (just over 8MB) + // to cause the method to go back to the server for + // multiple blocks. + byte[] data = new byte[8 * 1024 * 1024 + 4]; + int ret = stream.Read(data, 0, data.Length); + + // Make sure that we got the data we expected. + Assert.AreEqual(ret, data.Length); + + // Ensure we got hit as expected. + vaultMock.Verify(); + vaultObjectFileOperationsMock + .Verify( + // Did it get block one (0 -> 4MB)? + (m) => m.DownloadFileInBlocks_ReadBlock(1, 4 * 1024 * 1024, 0), + Times.Once + ); + vaultObjectFileOperationsMock + .Verify( + // Did it get block two (4MB -> 8MB)? + (m) => m.DownloadFileInBlocks_ReadBlock(1, 4 * 1024 * 1024, 4 * 1024 * 1024), + Times.Once + ); + vaultObjectFileOperationsMock + .Verify( + // Did it get block three (8MB -> end)? + (m) => m.DownloadFileInBlocks_ReadBlock(1, 4, 8 * 1024 * 1024), + Times.Once + ); + + } + [TestMethod] public void ReadIteratesThroughData() { @@ -628,8 +716,8 @@ private class FileDownloadStreamProxy set => base.DownloadSession = value; } - private long length = 0; - public override long Length => this.length; + private long? length; + public override long Length => this.length ?? base.Length; /// public override void SetLength(long value) diff --git a/MFilesAPI.Extensions.Tests/MFilesAPI.Extensions.Tests.csproj b/MFilesAPI.Extensions.Tests/MFilesAPI.Extensions.Tests.csproj index 6608c66..7c76e8f 100644 --- a/MFilesAPI.Extensions.Tests/MFilesAPI.Extensions.Tests.csproj +++ b/MFilesAPI.Extensions.Tests/MFilesAPI.Extensions.Tests.csproj @@ -1,17 +1,16 @@  - net45 - + net462 + true false - + - - + @@ -20,12 +19,7 @@ - - - - - ..\lib\Interop.MFilesAPI.dll - + \ No newline at end of file diff --git a/MFilesAPI.Extensions.Tests/packages.config b/MFilesAPI.Extensions.Tests/packages.config deleted file mode 100644 index d336719..0000000 --- a/MFilesAPI.Extensions.Tests/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/MFilesAPI.Extensions/Files/Downloading/FileDownloadStream.cs b/MFilesAPI.Extensions/Files/Downloading/FileDownloadStream.cs index 1194073..9072ff9 100644 --- a/MFilesAPI.Extensions/Files/Downloading/FileDownloadStream.cs +++ b/MFilesAPI.Extensions/Files/Downloading/FileDownloadStream.cs @@ -5,13 +5,13 @@ using System.Text; namespace MFilesAPI.Extensions -{ +{ /// /// Allows access to files from within the M-Files vault in an implementation of . /// public class FileDownloadStream : Stream - { + { /// /// The vault to download from. /// @@ -35,13 +35,21 @@ public class FileDownloadStream /// /// The format of the file to download. /// - public MFFileFormat FileFormat { get; protected set; } - + public MFFileFormat FileFormat { get; protected set; } + /// /// The open file download session. /// public FileDownloadSession DownloadSession { get; protected set; } + /// + /// The M-Files API limits download block size. + /// If a block larger than this is requested + /// via + /// then data will be read from M-Files in blocks of this size. + /// + public const int MaximumBlockSize = 4 * 1024 * 1024; + /// /// Creates a but does not open the download session. /// @@ -168,27 +176,47 @@ public override int Read(byte[] buffer, int offset, int count) if (this.Position >= this.DownloadSession.FileSize) return 0; - // Read the block. - var blockData = this - .Vault - .ObjectFileOperations - .DownloadFileInBlocks_ReadBlock - ( - this.DownloadSession.DownloadID, - count, - this.position - ); + // Loop through and get max 4MB blocks. + int dataRead = 0; + bool atEnd = false; + while (dataRead < count && !atEnd) + { + // Work out how much to get. + var blockSize = count; + if(blockSize > MaximumBlockSize) + blockSize = MaximumBlockSize; + if (dataRead + blockSize > this.Length) + blockSize = (int)(this.Length - dataRead); + if (blockSize <= 0) + break; + + // Read the block. + var blockData = this + .Vault + .ObjectFileOperations + .DownloadFileInBlocks_ReadBlock + ( + this.DownloadSession.DownloadID, + blockSize, + this.position + ); + + // Check the buffer is big enough. + if (dataRead + blockData.Length > buffer.Length) + throw new ArgumentException($"The buffer size ({buffer.Length}) is not big enough to hold the amount of data requested ({dataRead + blockData.Length}).", nameof(buffer)); - // Check the buffer is big enough. - if (blockData.Length > buffer.Length) - throw new ArgumentException($"The buffer size ({buffer.Length}) is not big enough to hold the amount of data requested ({count}).", nameof(buffer)); + // Copy the data into the supplied buffer. + Array.Copy(blockData, 0, buffer, dataRead + offset, blockData.Length); - // Copy the data into the supplied buffer. - Array.Copy(blockData, buffer, blockData.Length); + // Update our position with the number of bytes read. + this.position += blockData.Length; + dataRead += blockData.Length; + atEnd = blockData.Length < blockSize; - // Return the number of bytes read. - this.position += blockData.Length; - return blockData.Length; + } + + + return dataRead; } /// @@ -207,7 +235,7 @@ public override void Write(byte[] buffer, int offset, int count) public override bool CanWrite => false; /// - public override long Length => this.DownloadSession?.FileSize ?? this.FileSize; + public override long Length => (long)(this.DownloadSession?.FileSize ?? this.FileSize); private long position = 0; diff --git a/MFilesAPI.Extensions/MFilesAPI.Extensions.csproj b/MFilesAPI.Extensions/MFilesAPI.Extensions.csproj index a543999..6f3ee6e 100644 --- a/MFilesAPI.Extensions/MFilesAPI.Extensions.csproj +++ b/MFilesAPI.Extensions/MFilesAPI.Extensions.csproj @@ -1,6 +1,6 @@  - net45;netstandard2.0;net472 + netstandard2.0 M-Files Corporation 2020 onwards False 1.0.6