-
Notifications
You must be signed in to change notification settings - Fork 61
/
FileReaderJsInterop.cs
209 lines (175 loc) · 8.25 KB
/
FileReaderJsInterop.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Blazor.FileReader
{
internal partial class FileReaderJsInterop
{
private static readonly IReadOnlyDictionary<string, string> escapeScriptTextReplacements =
new Dictionary<string, string> { { @"\", @"\\" }, { "\r", @"\r" }, { "\n", @"\n" }, { "'", @"\'" }, { "\"", @"\""" } };
private readonly bool _needsInitialization = false;
private readonly IFileReaderServiceOptions _options;
private static readonly object _bufferIdLock = new object();
private static readonly IDictionary<int, byte[]> buffers = new Dictionary<int, byte[]>();
private static int _nextBufferId = 0;
internal IJSRuntime CurrentJSRuntime { get; }
public FileReaderJsInterop(IJSRuntime jsRuntime, IFileReaderServiceOptions options)
{
CurrentJSRuntime = jsRuntime;
_options = options;
_needsInitialization = options.InitializeOnFirstCall;
}
public async Task<bool> RegisterDropEvents(ElementReference elementReference, bool additive)
{
await EnsureInitializedAsync();
return await CurrentJSRuntime.InvokeAsync<bool>($"FileReaderComponent.RegisterDropEvents", elementReference, additive);
}
public async Task<bool> UnregisterDropEvents(ElementReference elementReference)
{
await EnsureInitializedAsync();
return await CurrentJSRuntime.InvokeAsync<bool>($"FileReaderComponent.UnregisterDropEvents", elementReference);
}
public async Task<Stream> OpenFileStream(ElementReference elementReference, int index)
{
var fileInfo = await GetFileInfoFromElement(elementReference, index);
return new InteropFileStream(await OpenReadAsync(elementReference, index), fileInfo.Size, this);
}
public async Task<IBase64Stream> OpenBase64Stream(ElementReference elementReference, int index)
{
var fileInfo = await GetFileInfoFromElement(elementReference, index);
return new Base64Stream(await OpenReadAsync(elementReference, index), fileInfo.Size, this);
}
public async Task<int> GetFileCount(ElementReference elementReference)
{
await EnsureInitializedAsync();
return (int)await CurrentJSRuntime.InvokeAsync<long>($"FileReaderComponent.GetFileCount", elementReference);
}
public async Task<int> ClearValue(ElementReference elementReference)
{
await EnsureInitializedAsync();
return (int)await CurrentJSRuntime.InvokeAsync<long>($"FileReaderComponent.ClearValue", elementReference);
}
public async Task<IFileInfo> GetFileInfoFromElement(ElementReference elementReference, int index)
{
return await CurrentJSRuntime.InvokeAsync<FileInfo>($"FileReaderComponent.GetFileInfoFromElement", elementReference, index);
}
public async Task EnsureInitializedAsync(bool force = false)
{
if (!_needsInitialization && !force)
{
return;
}
await Initialize();
}
private async Task<int> OpenReadAsync(ElementReference elementReference, int fileIndex)
{
return (int)await CurrentJSRuntime.InvokeAsync<long>($"FileReaderComponent.OpenRead", elementReference, fileIndex);
}
private async Task<bool> DisposeStream(int fileRef)
{
return await CurrentJSRuntime.InvokeAsync<bool>($"FileReaderComponent.Dispose", fileRef);
}
private async Task<int> ReadFileAsync(int fileRef, byte[] buffer, long position, long bufferOffset, int count, CancellationToken cancellationToken)
{
if (this._options.UseWasmSharedBuffer)
{
return await ReadFileUnmarshalledAsync(fileRef, buffer, position, bufferOffset, count, cancellationToken);
}
return await ReadFileMarshalledAsync(fileRef, buffer, position, bufferOffset, count, cancellationToken);
}
private async Task<int> ReadFileMarshalledAsync(
int fileRef, byte[] buffer, long position, long bufferOffset, int count,
CancellationToken cancellationToken)
{
var result = await ReadFileMarshalledBase64Async(fileRef, position, count, cancellationToken);
var bytesRead = 0;
if (!string.IsNullOrEmpty(result))
{
var byteResult = Convert.FromBase64String(result);
bytesRead = byteResult.Length;
Array.Copy(byteResult, 0, buffer, bufferOffset, bytesRead);
}
return bytesRead;
}
private async Task<string> ReadFileMarshalledBase64Async(
int fileRef, long position, int count,
CancellationToken cancellationToken)
{
var data = await CurrentJSRuntime.InvokeAsync<string>(
$"FileReaderComponent.ReadFileMarshalledAsync",
new { position, count, fileRef });
return data;
}
private async Task<int> ReadFileUnmarshalledAsync(
int fileRef, byte[] buffer, long position, long bufferOffset, int count,
CancellationToken cancellationToken)
{
var callBackId = 0;
lock (_bufferIdLock)
{
callBackId = _nextBufferId++;
buffers.Add(callBackId, buffer);
}
var bytesRead = await CurrentJSRuntime.InvokeAsync<long>(
$"FileReaderComponent.ReadFileUnmarshalledAsync",
new { position, count, fileRef, callBackId, bufferOffset });
lock (_bufferIdLock)
{
buffers.Remove(callBackId);
}
return (int)bytesRead;
}
private async Task Initialize()
{
var isLoaded = await IsLoaded();
if (isLoaded)
{
return;
}
string scriptContent;
using (var stream = this.GetType().Assembly.GetManifestResourceStream("blazor:js:FileReaderComponent.js"))
{
using (var streamReader = new StreamReader(stream))
{
scriptContent = await streamReader.ReadToEndAsync();
}
}
// Load the script
await ExecuteRawScriptAsync<object>(scriptContent);
var loaderLoopBreaker = 0;
while (!await IsLoaded())
{
loaderLoopBreaker++;
await Task.Delay(100);
// Fail after 3s not to block and hide any other possible error
if (loaderLoopBreaker > 25)
{
throw new InvalidOperationException("Unable to initialize FileReaderComponent script");
}
}
}
private async Task<bool> IsLoaded()
{
return await CurrentJSRuntime.InvokeAsync<bool>("eval", "(function() { return !!window.FileReaderComponent })()");
}
private async Task<T> ExecuteRawScriptAsync<T>(string scriptContent)
{
scriptContent = escapeScriptTextReplacements.Aggregate(scriptContent, (r, pair) => r.Replace(pair.Key, pair.Value));
var blob = $"URL.createObjectURL(new Blob([\"{scriptContent}\"],{{ \"type\": \"text/javascript\"}}))";
var bootStrapScript = $"(function(){{var d = document; var s = d.createElement('script'); s.src={blob}; s.async=false; d.head.appendChild(s); d.head.removeChild(s);}})();";
return await CurrentJSRuntime.InvokeAsync<T>("eval", bootStrapScript);
}
/// <remarks>
/// While it may be tempting to remove this method because it appears to be unused,
/// this method is referenced by client code and must persist.
/// </remarks>
#pragma warning disable IDE0051 // Remove unused private members
private static byte[] GetStreamBuffer(string bufferId) => buffers[int.Parse(bufferId)];
#pragma warning restore IDE0051 // Remove unused private members
}
}