|
1 | 1 | @using System.IO |
2 | 2 | @using System.IO.Compression |
3 | | -@inject BlazorDownloadFile.IBlazorDownloadFileService BlazorDownloadFileService |
| 3 | +@inject IJSRuntime JS |
4 | 4 | @inject NavigationManager NavManager |
5 | 5 |
|
6 | 6 | <ul class="nav nav-pills mb-4 pb-1 border-bottom" role="navigation" aria-label="Compression format"> |
|
92 | 92 | { |
93 | 93 | <li class="list-group-item d-flex align-items-center gap-2 py-2"> |
94 | 94 | <span class="text-truncate flex-grow-1 small fw-medium">@file.OriginalName</span> |
| 95 | + @if (file.Status == "To Process" && file.OriginalSize > WarnFileSizeBytes) |
| 96 | + { |
| 97 | + <small class="text-warning text-nowrap">⚠ >250 MB</small> |
| 98 | + } |
95 | 99 | @if(file.Status == "Compressing" || file.Status == "Decompressing") |
96 | 100 | { |
97 | 101 | <div class="spinner-grow text-primary spinner-grow-sm flex-shrink-0" role="status"> |
|
126 | 130 |
|
127 | 131 | private CompressionFormat _format = CompressionFormat.GZip; |
128 | 132 | private int _dragCount; |
| 133 | + private const long WarnFileSizeBytes = 250L * 1024 * 1024; |
| 134 | + private static readonly SemaphoreSlim _semaphore = new(4, 4); |
129 | 135 |
|
130 | 136 | private static string FormatSlug(CompressionFormat fmt) => fmt switch |
131 | 137 | { |
|
186 | 192 | { |
187 | 193 | if (_format == CompressionFormat.Brotli) |
188 | 194 | { |
189 | | - var s = new BrotliSharpLib.BrotliStream(output, CompressionMode.Compress); |
| 195 | + var s = new BrotliSharpLib.BrotliStream(output, CompressionMode.Compress, true); |
190 | 196 | s.SetQuality(ToBrotliQuality(level)); |
191 | 197 | return s; |
192 | 198 | } |
193 | 199 | if (_format == CompressionFormat.ZStd) |
194 | 200 | return new ZstdSharp.CompressionStream(output, ToZstdLevel(level)); |
195 | 201 | return _format switch |
196 | 202 | { |
197 | | - CompressionFormat.Deflate => new DeflateStream(output, level), |
198 | | - CompressionFormat.ZLib => new ZLibStream(output, level), |
199 | | - _ => new GZipStream(output, level) |
| 203 | + CompressionFormat.Deflate => new DeflateStream(output, level, leaveOpen: true), |
| 204 | + CompressionFormat.ZLib => new ZLibStream(output, level, leaveOpen: true), |
| 205 | + _ => new GZipStream(output, level, leaveOpen: true) |
200 | 206 | }; |
201 | 207 | } |
202 | 208 |
|
|
252 | 258 | { |
253 | 259 | BrowserFile = f, |
254 | 260 | OriginalName = f.Name, |
| 261 | + OriginalSize = f.Size, |
255 | 262 | Status = "To Process" |
256 | 263 | })); |
257 | 264 | } |
|
263 | 270 | { |
264 | 271 | var task = Task.Run(async () => |
265 | 272 | { |
266 | | - file.Status = "Compressing"; |
267 | | - file.NewName = $"{file.OriginalName}{Extension}"; |
268 | | - StateHasChanged(); |
269 | | - |
| 273 | + await _semaphore.WaitAsync(); |
270 | 274 | try |
271 | 275 | { |
272 | | - var browserFile = file.BrowserFile; |
273 | | - file.OriginalSize = browserFile.Size; |
274 | | - using var inputStream = browserFile.OpenReadStream(long.MaxValue); |
275 | | - using var outputStream = new MemoryStream(); |
276 | | - using (var compressionStream = CreateCompressStream(outputStream, compressionLevel)) |
| 276 | + file.Status = "Compressing"; |
| 277 | + file.NewName = $"{file.OriginalName}{Extension}"; |
| 278 | + StateHasChanged(); |
| 279 | + |
| 280 | + try |
277 | 281 | { |
278 | | - await inputStream.CopyToAsync(compressionStream); |
| 282 | + var browserFile = file.BrowserFile; |
| 283 | + using var inputStream = browserFile.OpenReadStream(long.MaxValue); |
| 284 | + using var outputStream = new MemoryStream(); |
| 285 | + using (var compressionStream = CreateCompressStream(outputStream, compressionLevel)) |
| 286 | + { |
| 287 | + await inputStream.CopyToAsync(compressionStream); |
| 288 | + } |
| 289 | + file.OutputSize = outputStream.Length; |
| 290 | + outputStream.Position = 0; |
| 291 | + using var streamRef = new DotNetStreamReference(outputStream, leaveOpen: true); |
| 292 | + await JS.InvokeVoidAsync("downloadFileFromStream", file.NewName, streamRef); |
| 293 | + |
| 294 | + file.Status = "Finished"; |
| 295 | + } |
| 296 | + catch(Exception ex){ |
| 297 | + file.Status = $"Error: {ex.Message}"; |
279 | 298 | } |
280 | | - var outputBytes = outputStream.ToArray(); |
281 | | - file.OutputSize = outputBytes.Length; |
282 | | - await BlazorDownloadFileService.DownloadFile(file.NewName, outputBytes, "application/octet-stream"); |
283 | 299 |
|
284 | | - file.Status = "Finished"; |
285 | | - } |
286 | | - catch(Exception ex){ |
287 | | - file.Status = $"Error: {ex.Message}"; |
| 300 | + StateHasChanged(); |
288 | 301 | } |
289 | | - |
290 | | - StateHasChanged(); |
| 302 | + finally { _semaphore.Release(); } |
291 | 303 | }); |
292 | 304 | tasks.Add(task); |
293 | 305 | } |
|
302 | 314 | { |
303 | 315 | var task = Task.Run(async () => |
304 | 316 | { |
305 | | - file.Status = "Decompressing"; |
306 | | - file.NewName = file.OriginalName.Replace(Extension, ""); |
307 | | - StateHasChanged(); |
308 | | - |
| 317 | + await _semaphore.WaitAsync(); |
309 | 318 | try |
310 | 319 | { |
311 | | - var browserFile = file.BrowserFile; |
312 | | - file.OriginalSize = browserFile.Size; |
313 | | - using var inputStream = browserFile.OpenReadStream(long.MaxValue); |
314 | | - using var outputStream = new MemoryStream(); |
315 | | - using (var decompressionStream = CreateDecompressStream(inputStream)) |
| 320 | + file.Status = "Decompressing"; |
| 321 | + file.NewName = file.OriginalName.Replace(Extension, ""); |
| 322 | + StateHasChanged(); |
| 323 | + |
| 324 | + try |
316 | 325 | { |
317 | | - await decompressionStream.CopyToAsync(outputStream); |
318 | | - } |
319 | | - var outputBytes = outputStream.ToArray(); |
320 | | - file.OutputSize = outputBytes.Length; |
321 | | - await BlazorDownloadFileService.DownloadFile(file.NewName, outputBytes, "application/octet-stream"); |
| 326 | + var browserFile = file.BrowserFile; |
| 327 | + using var inputStream = browserFile.OpenReadStream(long.MaxValue); |
| 328 | + using var outputStream = new MemoryStream(); |
| 329 | + // BrotliSharpLib.BrotliStream.ReadAsync calls synchronous Read internally, which |
| 330 | + // fails on IBrowserFile streams (async-only). Pre-buffer the compressed input into |
| 331 | + // a MemoryStream first. This adds a copy of the compressed data in memory, but |
| 332 | + // compressed files are typically small relative to their decompressed output. |
| 333 | + if (_format == CompressionFormat.Brotli) |
| 334 | + { |
| 335 | + using var bufferedInput = new MemoryStream(); |
| 336 | + await inputStream.CopyToAsync(bufferedInput); |
| 337 | + bufferedInput.Position = 0; |
| 338 | + using var s = CreateDecompressStream(bufferedInput); |
| 339 | + s.CopyTo(outputStream); |
| 340 | + } |
| 341 | + else |
| 342 | + { |
| 343 | + using (var decompressionStream = CreateDecompressStream(inputStream)) |
| 344 | + { |
| 345 | + await decompressionStream.CopyToAsync(outputStream); |
| 346 | + } |
| 347 | + } |
| 348 | + file.OutputSize = outputStream.Length; |
| 349 | + outputStream.Position = 0; |
| 350 | + using var streamRef = new DotNetStreamReference(outputStream, leaveOpen: true); |
| 351 | + await JS.InvokeVoidAsync("downloadFileFromStream", file.NewName, streamRef); |
322 | 352 |
|
323 | | - file.Status = "Finished"; |
324 | | - } |
325 | | - catch(Exception ex){ |
326 | | - file.Status = $"Error: {ex.Message}"; |
| 353 | + file.Status = "Finished"; |
| 354 | + } |
| 355 | + catch(Exception ex){ |
| 356 | + file.Status = $"Error: {ex.Message}"; |
| 357 | + } |
| 358 | + StateHasChanged(); |
327 | 359 | } |
328 | | - StateHasChanged(); |
| 360 | + finally { _semaphore.Release(); } |
329 | 361 | }); |
330 | 362 | tasks.Add(task); |
331 | 363 | } |
|
0 commit comments