Skip to content

Commit 3624805

Browse files
authored
Merge pull request #17 from M-Files/filedownloadstream-blocksize_exceedsmaximum
FileDownloadStream.Read handles large block size
2 parents 65a53ed + c6063b0 commit 3624805

File tree

5 files changed

+150
-47
lines changed

5 files changed

+150
-47
lines changed

MFilesAPI.Extensions.Tests/Files/Downloading/FileDownloadStream/FileDownloadStream.cs

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,8 @@ public void ReadOpensDownloadSessionIfNotOpen()
403403
// Mock a download session to return.
404404
var downloadSessionMock = new Mock<FileDownloadSession>();
405405
downloadSessionMock.SetupGet(m => m.DownloadID).Returns(1);
406-
downloadSessionMock.SetupGet(m => m.FileSize).Returns(1000);
407-
downloadSessionMock.SetupGet(m => m.FileSize32).Returns(1000);
406+
downloadSessionMock.SetupGet(m => m.FileSize).Returns(3);
407+
downloadSessionMock.SetupGet(m => m.FileSize32).Returns(3);
408408
return downloadSessionMock.Object;
409409
})
410410
.Verifiable();
@@ -441,7 +441,7 @@ public void ReadOpensDownloadSessionIfNotOpen()
441441

442442
// Attempt to read.
443443
byte[] data = new byte[4096];
444-
stream.Read(data, 0, 4096);
444+
Assert.AreEqual(3, stream.Read(data, 0, 4096));
445445

446446
// Ensure that the download session is not empty.
447447
Assert.IsNotNull(stream.DownloadSession);
@@ -451,6 +451,94 @@ public void ReadOpensDownloadSessionIfNotOpen()
451451
vaultObjectFileOperationsMock.Verify();
452452
}
453453

454+
[TestMethod]
455+
public void ReadWithLargeBlockSizeRetrievesMultipleBlocks()
456+
{
457+
458+
// Set up a file to download.
459+
var objectFileMock = new Mock<ObjectFile>();
460+
objectFileMock.SetupGet(m => m.ID).Returns(12345);
461+
objectFileMock.SetupGet(m => m.Version).Returns(1);
462+
463+
// Set up the vault object file operations mock.
464+
var vaultObjectFileOperationsMock = new Mock<VaultObjectFileOperations>();
465+
466+
// We don't really care what we download, as long as we can check it happened.
467+
vaultObjectFileOperationsMock
468+
.Setup(m => m.DownloadFileInBlocks_BeginEx
469+
(
470+
Moq.It.IsAny<int>(),
471+
Moq.It.IsAny<int>(),
472+
Moq.It.IsAny<MFFileFormat>()
473+
))
474+
.Returns((int receivedFileId, int receivedFileVersion, MFFileFormat receivedFileFormat) =>
475+
{
476+
// Mock a download session to return.
477+
var downloadSessionMock = new Mock<FileDownloadSession>();
478+
downloadSessionMock.SetupGet(m => m.DownloadID).Returns(1);
479+
downloadSessionMock.SetupGet(m => m.FileSize).Returns(8 * 1024 * 1024 + 4);
480+
downloadSessionMock.SetupGet(m => m.FileSize32).Returns(8 * 1024 * 1024 + 4);
481+
return downloadSessionMock.Object;
482+
})
483+
.Verifiable();
484+
485+
// When DownloadFileInBlocks_ReadBlock is called (reading a block of content), return something.
486+
vaultObjectFileOperationsMock
487+
.Setup(m => m.DownloadFileInBlocks_ReadBlock
488+
(
489+
Moq.It.IsAny<int>(),
490+
Moq.It.IsAny<int>(),
491+
Moq.It.IsAny<long>()
492+
))
493+
.Returns((int downloadSession, int blockSize, long offset) =>
494+
{
495+
if (offset < 8 * 1024 * 1024)
496+
return new byte[4 * 1024 * 1024];
497+
return new byte[4];
498+
})
499+
.Verifiable();
500+
501+
// Set up the mock vault.
502+
var vaultMock = new Mock<Vault>();
503+
vaultMock
504+
.SetupGet(m => m.ObjectFileOperations)
505+
.Returns(vaultObjectFileOperationsMock.Object);
506+
507+
// Create the stream.
508+
var stream = new FileDownloadStreamProxy(objectFileMock.Object, vaultMock.Object);
509+
510+
// Read a large set of data (just over 8MB)
511+
// to cause the method to go back to the server for
512+
// multiple blocks.
513+
byte[] data = new byte[8 * 1024 * 1024 + 4];
514+
int ret = stream.Read(data, 0, data.Length);
515+
516+
// Make sure that we got the data we expected.
517+
Assert.AreEqual(ret, data.Length);
518+
519+
// Ensure we got hit as expected.
520+
vaultMock.Verify();
521+
vaultObjectFileOperationsMock
522+
.Verify(
523+
// Did it get block one (0 -> 4MB)?
524+
(m) => m.DownloadFileInBlocks_ReadBlock(1, 4 * 1024 * 1024, 0),
525+
Times.Once
526+
);
527+
vaultObjectFileOperationsMock
528+
.Verify(
529+
// Did it get block two (4MB -> 8MB)?
530+
(m) => m.DownloadFileInBlocks_ReadBlock(1, 4 * 1024 * 1024, 4 * 1024 * 1024),
531+
Times.Once
532+
);
533+
vaultObjectFileOperationsMock
534+
.Verify(
535+
// Did it get block three (8MB -> end)?
536+
(m) => m.DownloadFileInBlocks_ReadBlock(1, 4, 8 * 1024 * 1024),
537+
Times.Once
538+
);
539+
540+
}
541+
454542
[TestMethod]
455543
public void ReadIteratesThroughData()
456544
{
@@ -628,8 +716,8 @@ private class FileDownloadStreamProxy
628716
set => base.DownloadSession = value;
629717
}
630718

631-
private long length = 0;
632-
public override long Length => this.length;
719+
private long? length;
720+
public override long Length => this.length ?? base.Length;
633721

634722
/// <inheritdoc />
635723
public override void SetLength(long value)
Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net45</TargetFramework>
5-
4+
<TargetFramework>net462</TargetFramework>
5+
<IsTestProject>true</IsTestProject>
66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
1111
<PackageReference Include="Moq" Version="4.13.1" />
12-
<PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
13-
<PackageReference Include="MSTest.TestFramework" Version="2.0.0" />
1412
<PackageReference Include="coverlet.collector" Version="1.0.1" />
13+
<PackageReference Include="MSTest" Version="3.3.1" />
1514
</ItemGroup>
1615

1716
<ItemGroup>
@@ -20,12 +19,7 @@
2019

2120
<ItemGroup>
2221
<ProjectReference Include="..\MFilesAPI.Extensions\MFilesAPI.Extensions.csproj" />
23-
</ItemGroup>
24-
25-
<ItemGroup>
26-
<Reference Include="Interop.MFilesAPI">
27-
<HintPath>..\lib\Interop.MFilesAPI.dll</HintPath>
28-
</Reference>
22+
<PackageReference Include="Interop.MFilesAPI" Version="21.11.3" />
2923
</ItemGroup>
3024

3125
</Project>

MFilesAPI.Extensions.Tests/packages.config

Lines changed: 0 additions & 7 deletions
This file was deleted.

MFilesAPI.Extensions/Files/Downloading/FileDownloadStream.cs

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
using System.Text;
66

77
namespace MFilesAPI.Extensions
8-
{
8+
{
99
/// <summary>
1010
/// Allows access to files from within the M-Files vault in an implementation of <see cref="Stream"/>.
1111
/// </summary>
1212
public class FileDownloadStream
1313
: Stream
14-
{
14+
{
1515
/// <summary>
1616
/// The vault to download from.
1717
/// </summary>
@@ -35,13 +35,21 @@ public class FileDownloadStream
3535
/// <summary>
3636
/// The format of the file to download.
3737
/// </summary>
38-
public MFFileFormat FileFormat { get; protected set; }
39-
38+
public MFFileFormat FileFormat { get; protected set; }
39+
4040
/// <summary>
4141
/// The open file download session.
4242
/// </summary>
4343
public FileDownloadSession DownloadSession { get; protected set; }
4444

45+
/// <summary>
46+
/// The M-Files API limits download block size.
47+
/// If a block larger than this is requested
48+
/// via <see cref="Read(byte[], int, int)"/>
49+
/// then data will be read from M-Files in blocks of this size.
50+
/// </summary>
51+
public const int MaximumBlockSize = 4 * 1024 * 1024;
52+
4553
/// <summary>
4654
/// Creates a <see cref="FileDownloadStream"/> but does not open the download session.
4755
/// </summary>
@@ -168,27 +176,47 @@ public override int Read(byte[] buffer, int offset, int count)
168176
if (this.Position >= this.DownloadSession.FileSize)
169177
return 0;
170178

171-
// Read the block.
172-
var blockData = this
173-
.Vault
174-
.ObjectFileOperations
175-
.DownloadFileInBlocks_ReadBlock
176-
(
177-
this.DownloadSession.DownloadID,
178-
count,
179-
this.position
180-
);
179+
// Loop through and get max 4MB blocks.
180+
int dataRead = 0;
181+
bool atEnd = false;
182+
while (dataRead < count && !atEnd)
183+
{
184+
// Work out how much to get.
185+
var blockSize = count;
186+
if(blockSize > MaximumBlockSize)
187+
blockSize = MaximumBlockSize;
188+
if (dataRead + blockSize > this.Length)
189+
blockSize = (int)(this.Length - dataRead);
190+
if (blockSize <= 0)
191+
break;
192+
193+
// Read the block.
194+
var blockData = this
195+
.Vault
196+
.ObjectFileOperations
197+
.DownloadFileInBlocks_ReadBlock
198+
(
199+
this.DownloadSession.DownloadID,
200+
blockSize,
201+
this.position
202+
);
203+
204+
// Check the buffer is big enough.
205+
if (dataRead + blockData.Length > buffer.Length)
206+
throw new ArgumentException($"The buffer size ({buffer.Length}) is not big enough to hold the amount of data requested ({dataRead + blockData.Length}).", nameof(buffer));
181207

182-
// Check the buffer is big enough.
183-
if (blockData.Length > buffer.Length)
184-
throw new ArgumentException($"The buffer size ({buffer.Length}) is not big enough to hold the amount of data requested ({count}).", nameof(buffer));
208+
// Copy the data into the supplied buffer.
209+
Array.Copy(blockData, 0, buffer, dataRead + offset, blockData.Length);
185210

186-
// Copy the data into the supplied buffer.
187-
Array.Copy(blockData, buffer, blockData.Length);
211+
// Update our position with the number of bytes read.
212+
this.position += blockData.Length;
213+
dataRead += blockData.Length;
214+
atEnd = blockData.Length < blockSize;
188215

189-
// Return the number of bytes read.
190-
this.position += blockData.Length;
191-
return blockData.Length;
216+
}
217+
218+
219+
return dataRead;
192220
}
193221

194222
/// <inheritdoc />
@@ -207,7 +235,7 @@ public override void Write(byte[] buffer, int offset, int count)
207235
public override bool CanWrite => false;
208236

209237
/// <inheritdoc />
210-
public override long Length => this.DownloadSession?.FileSize ?? this.FileSize;
238+
public override long Length => (long)(this.DownloadSession?.FileSize ?? this.FileSize);
211239

212240
private long position = 0;
213241

MFilesAPI.Extensions/MFilesAPI.Extensions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net45;netstandard2.0;net472</TargetFrameworks>
3+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
44
<Copyright>M-Files Corporation 2020 onwards</Copyright>
55
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
66
<Version>1.0.6</Version>

0 commit comments

Comments
 (0)