Skip to content

Commit

Permalink
Initial state
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS committed Sep 13, 2019
0 parents commit 69c82ba
Show file tree
Hide file tree
Showing 35 changed files with 1,322 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.vs/
bin/
obj/
*.user
samples/Sample.WebAssembly/wwwroot/_content/
86 changes: 86 additions & 0 deletions BlazorInputFile.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29230.61
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorInputFile", "BlazorInputFile\BlazorInputFile.csproj", "{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{EEEC40BE-3F2B-4B6E-9040-71FF3DC274AA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Server", "samples\Sample.Server\Sample.Server.csproj", "{BE1CED5B-9590-4170-BB54-822555E920CF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.WebAssembly", "samples\Sample.WebAssembly\Sample.WebAssembly.csproj", "{28B157AE-2F2D-4438-8493-3FE9148E442B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Core", "samples\Sample.Core\Sample.Core.csproj", "{5D825DE4-4A0D-439F-A3BB-F16B33461F16}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Debug|x64.ActiveCfg = Debug|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Debug|x64.Build.0 = Debug|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Debug|x86.ActiveCfg = Debug|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Debug|x86.Build.0 = Debug|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Release|Any CPU.Build.0 = Release|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Release|x64.ActiveCfg = Release|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Release|x64.Build.0 = Release|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Release|x86.ActiveCfg = Release|Any CPU
{C3C06DC6-FC66-4266-B9B9-16F2F233B87D}.Release|x86.Build.0 = Release|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Debug|x64.ActiveCfg = Debug|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Debug|x64.Build.0 = Debug|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Debug|x86.ActiveCfg = Debug|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Debug|x86.Build.0 = Debug|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Release|Any CPU.Build.0 = Release|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Release|x64.ActiveCfg = Release|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Release|x64.Build.0 = Release|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Release|x86.ActiveCfg = Release|Any CPU
{BE1CED5B-9590-4170-BB54-822555E920CF}.Release|x86.Build.0 = Release|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Debug|x64.ActiveCfg = Debug|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Debug|x64.Build.0 = Debug|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Debug|x86.ActiveCfg = Debug|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Debug|x86.Build.0 = Debug|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Release|Any CPU.Build.0 = Release|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Release|x64.ActiveCfg = Release|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Release|x64.Build.0 = Release|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Release|x86.ActiveCfg = Release|Any CPU
{28B157AE-2F2D-4438-8493-3FE9148E442B}.Release|x86.Build.0 = Release|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Debug|x64.ActiveCfg = Debug|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Debug|x64.Build.0 = Debug|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Debug|x86.ActiveCfg = Debug|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Debug|x86.Build.0 = Debug|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Release|Any CPU.Build.0 = Release|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Release|x64.ActiveCfg = Release|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Release|x64.Build.0 = Release|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Release|x86.ActiveCfg = Release|Any CPU
{5D825DE4-4A0D-439F-A3BB-F16B33461F16}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BE1CED5B-9590-4170-BB54-822555E920CF} = {EEEC40BE-3F2B-4B6E-9040-71FF3DC274AA}
{28B157AE-2F2D-4438-8493-3FE9148E442B} = {EEEC40BE-3F2B-4B6E-9040-71FF3DC274AA}
{5D825DE4-4A0D-439F-A3BB-F16B33461F16} = {EEEC40BE-3F2B-4B6E-9040-71FF3DC274AA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {843608D9-10E0-41C6-9955-83621659FFE5}
EndGlobalSection
EndGlobal
16 changes: 16 additions & 0 deletions BlazorInputFile/BlazorInputFile.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<RazorLangVersion>3.0</RazorLangVersion>
<LangVersion>preview</LangVersion>
<PackageVersion>0.1.0-preview</PackageVersion>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="$(ServerPackagesVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(ServerPackagesVersion)" />
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions BlazorInputFile/FileListEntryImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.IO;

namespace BlazorInputFile
{
// This is public only because it's used in a JSInterop method signature,
// but otherwise is intended as internal
public class FileListEntryImpl : IFileListEntry
{
internal InputFile Owner { get; set; }

private Stream _stream;

public event EventHandler OnDataRead;

public int Id { get; set; }

public DateTime LastModified { get; set; }

public string Name { get; set; }

public long Size { get; set; }

public string Type { get; set; }

public Stream Data
{
get
{
_stream ??= Owner.OpenFileStream(this);
return _stream;
}
}

internal void RaiseOnDataRead()
{
OnDataRead?.Invoke(this, null);
}
}
}
80 changes: 80 additions & 0 deletions BlazorInputFile/FileListEntryStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorInputFile
{
// TODO: When ReadAsync is called, don't just fetch the segment of data that's being requested.
// That will be very slow, as you may be doing a separate round-trip for each 1KB or so of data.
// Instead, have a larger buffer whose size == SignalR.MaxMessageSize and populate that. Then
// many of the ReadAsync calls can return immediately with already-loaded data.
//
// This is still not as fast as allowing the client to send as much data as it wants, and using
// TCP to apply backpressure. In the future we could achieve something closer to that by having
// an even larger buffer, and telling the client to send N messages in parallel. The ReadAsync
// calls would return whenever their portion of the buffer was populated. This is much more
// complicated to implement.

public abstract class FileListEntryStream : Stream
{
protected readonly IJSRuntime _jsRuntime;
protected readonly ElementReference _inputFileElement;
protected readonly FileListEntryImpl _file;
private long _position;

public FileListEntryStream(IJSRuntime jsRuntime, ElementReference inputFileElement, FileListEntryImpl file)
{
_jsRuntime = jsRuntime;
_inputFileElement = inputFileElement;
_file = file;
}

public override bool CanRead => true;

public override bool CanSeek => false;

public override bool CanWrite => false;

public override long Length => _file.Size;

public override long Position
{
get => _position;
set => throw new NotSupportedException();
}

public override void Flush()
=> throw new NotSupportedException();

public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException("Synchronous reads are not supported");

public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();

public override void SetLength(long value)
=> throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();

public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var maxBytesToRead = (int)Math.Min(count, Length - Position);
if (maxBytesToRead == 0)
{
return 0;
}

var actualBytesRead = await CopyFileDataIntoBuffer(_position, buffer, offset, maxBytesToRead, cancellationToken);
_position += actualBytesRead;
_file.RaiseOnDataRead();
return actualBytesRead;
}

protected abstract Task<int> CopyFileDataIntoBuffer(long sourceOffset, byte[] destination, int destinationOffset, int maxBytes, CancellationToken cancellationToken);
}
}
20 changes: 20 additions & 0 deletions BlazorInputFile/IFileListEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.IO;

namespace BlazorInputFile
{
public interface IFileListEntry
{
DateTime LastModified { get; }

string Name { get; }

long Size { get; }

string Type { get; }

Stream Data { get; }

event EventHandler OnDataRead;
}
}
47 changes: 47 additions & 0 deletions BlazorInputFile/InputFile.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@implements IDisposable
@inject IJSRuntime JSRuntime

<input type="file" @ref="inputFileElement" @attributes="UnmatchedParameters" />

@code {
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> UnmatchedParameters { get; set; }
[Parameter] public EventCallback<IFileListEntry[]> OnChange { get; set; }
[Parameter] public int MaxMessageSize { get; set; } = 20 * 1024; // TODO: Use SignalR default
[Parameter] public int MaxBufferSize { get; set; } = 1024 * 1024;

ElementReference inputFileElement;
IDisposable thisReference;

[JSInvokable]
public Task NotifyChange(FileListEntryImpl[] files)
{
foreach (var file in files)
{
// So that method invocations on the file can be dispatched back here
file.Owner = (InputFile)(object)this;
}

return OnChange.InvokeAsync(files);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
thisReference = DotNetObjectReference.Create(this);
await JSRuntime.InvokeAsync<object>("BlazorInputFile.init", inputFileElement, thisReference);
}
}

internal Stream OpenFileStream(FileListEntryImpl file)
{
return SharedMemoryFileListEntryStream.IsSupported(JSRuntime)
? (Stream)new SharedMemoryFileListEntryStream(JSRuntime, inputFileElement, file)
: new RemoteFileListEntryStream(JSRuntime, inputFileElement, file, MaxMessageSize, MaxBufferSize);
}

void IDisposable.Dispose()
{
thisReference?.Dispose();
}
}
58 changes: 58 additions & 0 deletions BlazorInputFile/PreFetchingSequence.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Threading;

namespace BlazorInputFile
{
internal class PreFetchingSequence<T>
{
private readonly Func<long, CancellationToken, T> _fetchCallback;
private readonly int _maxBufferCapacity;
private readonly long _totalFetchableItems;
private readonly Queue<T> _buffer;
private long _maxFetchedIndex;

public PreFetchingSequence(Func<long, CancellationToken, T> fetchCallback, long totalFetchableItems, int maxBufferCapacity)
{
_fetchCallback = fetchCallback;
_buffer = new Queue<T>();
_maxBufferCapacity = maxBufferCapacity;
_totalFetchableItems = totalFetchableItems;
}

public T ReadNext(CancellationToken cancellationToken)
{
EnqueueFetches(cancellationToken);
if (_buffer.Count == 0)
{
throw new InvalidOperationException("There are no more entries to read");
}

var next = _buffer.Dequeue();
EnqueueFetches(cancellationToken);
return next;
}

public bool TryPeekNext(out T result)
{
if (_buffer.Count > 0)
{
result = _buffer.Peek();
return true;
}
else
{
result = default;
return false;
}
}

private void EnqueueFetches(CancellationToken cancellationToken)
{
while (_buffer.Count < _maxBufferCapacity && _maxFetchedIndex < _totalFetchableItems)
{
_buffer.Enqueue(_fetchCallback(_maxFetchedIndex++, cancellationToken));
}
}
}
}
Loading

0 comments on commit 69c82ba

Please sign in to comment.