-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
637 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
using Avalonia.Platform.Storage; | ||
|
||
using Microsoft.JSInterop; | ||
|
||
namespace Avalonia.Web.Blazor.Interop.Storage | ||
{ | ||
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept); | ||
|
||
internal record FileProperties(ulong Size, long LastModified, string? Type); | ||
|
||
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider | ||
{ | ||
private const string JsFilename = "./_content/Avalonia.Web.Blazor/StorageProvider.js"; | ||
private const string PickerCancelMessage = "The user aborted a request"; | ||
|
||
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js) | ||
{ | ||
var interop = new StorageProviderInterop(js); | ||
await interop.ImportAsync(); | ||
return interop; | ||
} | ||
|
||
public StorageProviderInterop(IJSRuntime js) | ||
: base(js, JsFilename) | ||
{ | ||
} | ||
|
||
public bool CanOpen => Invoke<bool>("StorageProvider.canOpen"); | ||
public bool CanSave => Invoke<bool>("StorageProvider.canSave"); | ||
public bool CanPickFolder => Invoke<bool>("StorageProvider.canPickFolder"); | ||
|
||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) | ||
{ | ||
try | ||
{ | ||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; | ||
|
||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); | ||
var items = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openFileDialog", startIn, options.AllowMultiple, types, exludeAll); | ||
var count = items.Invoke<int>("count"); | ||
|
||
return Enumerable.Range(0, count) | ||
.Select(index => new JSStorageFile(items.Invoke<IJSInProcessObjectReference>("at", index))) | ||
.ToArray(); | ||
} | ||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) | ||
{ | ||
return Array.Empty<IStorageFile>(); | ||
} | ||
} | ||
|
||
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) | ||
{ | ||
try | ||
{ | ||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; | ||
|
||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); | ||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.saveFileDialog", startIn, options.SuggestedFileName, types, exludeAll); | ||
|
||
return item is not null ? new JSStorageFile(item) : null; | ||
} | ||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) | ||
{ | ||
return null; | ||
} | ||
} | ||
|
||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) | ||
{ | ||
try | ||
{ | ||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; | ||
|
||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.selectFolderDialog", startIn); | ||
|
||
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>(); | ||
} | ||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) | ||
{ | ||
return Array.Empty<IStorageFolder>(); | ||
} | ||
} | ||
|
||
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) | ||
{ | ||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark); | ||
return item is not null ? new JSStorageFile(item) : null; | ||
} | ||
|
||
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) | ||
{ | ||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark); | ||
return item is not null ? new JSStorageFolder(item) : null; | ||
} | ||
|
||
private static (FilePickerAcceptType[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input) | ||
{ | ||
var types = input? | ||
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) | ||
.Select(t => new FilePickerAcceptType(t.Name, t.MimeTypes! | ||
.ToDictionary(m => m, _ => (IReadOnlyList<string>)Array.Empty<string>()))) | ||
.ToArray(); | ||
if (types?.Length == 0) | ||
{ | ||
types = null; | ||
} | ||
|
||
var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; | ||
|
||
return (types, !inlcudeAll); | ||
} | ||
} | ||
|
||
internal abstract class JSStorageItem : IStorageBookmarkItem | ||
{ | ||
internal IJSInProcessObjectReference? _fileHandle; | ||
|
||
protected JSStorageItem(IJSInProcessObjectReference fileHandle) | ||
{ | ||
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); | ||
} | ||
|
||
internal IJSInProcessObjectReference FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); | ||
|
||
public string Name => FileHandle.Invoke<string>("getName"); | ||
|
||
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) | ||
{ | ||
uri = new Uri(Name, UriKind.Relative); | ||
return false; | ||
} | ||
|
||
public async Task<StorageItemProperties> GetBasicPropertiesAsync() | ||
{ | ||
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); | ||
|
||
return new StorageItemProperties( | ||
properties?.Size, | ||
dateCreated: null, | ||
dateModified: properties?.LastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(properties.LastModified) : null); | ||
} | ||
|
||
public bool CanBookmark => true; | ||
|
||
public Task<string?> SaveBookmark() | ||
{ | ||
return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask(); | ||
} | ||
|
||
public Task<IStorageFolder?> GetParentAsync() | ||
{ | ||
return Task.FromResult<IStorageFolder?>(null); | ||
} | ||
|
||
public Task ReleaseBookmark() | ||
{ | ||
return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask(); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_fileHandle?.Dispose(); | ||
_fileHandle = null; | ||
} | ||
} | ||
|
||
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile | ||
{ | ||
public JSStorageFile(IJSInProcessObjectReference fileHandle) : base(fileHandle) | ||
{ | ||
} | ||
|
||
public bool CanOpenRead => true; | ||
public async Task<Stream> OpenRead() | ||
{ | ||
var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead"); | ||
// Remove maxAllowedSize limit, as developer can decide if they read only small part or everything. | ||
return await stream.OpenReadStreamAsync(long.MaxValue, CancellationToken.None); | ||
} | ||
|
||
public bool CanOpenWrite => true; | ||
public async Task<Stream> OpenWrite() | ||
{ | ||
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); | ||
var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite"); | ||
|
||
return new JSWriteableStream(streamWriter, (long)(properties?.Size ?? 0)); | ||
} | ||
} | ||
|
||
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder | ||
{ | ||
public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) | ||
{ | ||
} | ||
} | ||
} |
124 changes: 124 additions & 0 deletions
124
src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
using System.Buffers; | ||
using System.Text.Json.Serialization; | ||
|
||
using Microsoft.JSInterop; | ||
|
||
namespace Avalonia.Web.Blazor.Interop.Storage | ||
{ | ||
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream | ||
internal sealed class JSWriteableStream : Stream | ||
{ | ||
private IJSInProcessObjectReference? _jSReference; | ||
|
||
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. | ||
private long _length, _position; | ||
|
||
internal JSWriteableStream(IJSInProcessObjectReference jSReference, long initialLength) | ||
{ | ||
_jSReference = jSReference; | ||
_length = initialLength; | ||
} | ||
|
||
private IJSInProcessObjectReference JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); | ||
|
||
public override bool CanRead => false; | ||
|
||
public override bool CanSeek => true; | ||
|
||
public override bool CanWrite => true; | ||
|
||
public override long Length => _length; | ||
|
||
public override long Position | ||
{ | ||
get => _position; | ||
set => Seek(_position, SeekOrigin.Begin); | ||
} | ||
|
||
public override void Flush() | ||
{ | ||
// no-op | ||
} | ||
|
||
public override int Read(byte[] buffer, int offset, int count) | ||
{ | ||
throw new NotSupportedException(); | ||
} | ||
|
||
public override long Seek(long offset, SeekOrigin origin) | ||
{ | ||
var position = origin switch | ||
{ | ||
SeekOrigin.Current => _position + offset, | ||
SeekOrigin.End => _length + offset, | ||
_ => offset | ||
}; | ||
JSReference.InvokeVoid("seek", position); | ||
return position; | ||
} | ||
|
||
public override void SetLength(long value) | ||
{ | ||
_length = value; | ||
|
||
// See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate | ||
// If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size | ||
if (_position > _length) | ||
{ | ||
_position = _length; | ||
} | ||
|
||
JSReference.InvokeVoid("truncate", value); | ||
} | ||
|
||
public override void Write(byte[] buffer, int offset, int count) | ||
{ | ||
throw new NotSupportedException("Synchronous writes are not supported."); | ||
} | ||
|
||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | ||
{ | ||
if (offset != 0 || count != buffer.Length) | ||
{ | ||
// TODO, we need to pass prepared buffer to the JS | ||
// Can't use ArrayPool as it can return bigger array than requested | ||
// Can't use Span/Memory, as it's not supported by JS interop yet. | ||
// Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?) | ||
buffer = buffer.AsMemory(offset, count).ToArray(); | ||
} | ||
return WriteAsyncInternal(buffer, cancellationToken).AsTask(); | ||
} | ||
|
||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) | ||
{ | ||
return WriteAsyncInternal(buffer.ToArray(), cancellationToken); | ||
} | ||
|
||
private ValueTask WriteAsyncInternal(byte[] buffer, CancellationToken _) | ||
{ | ||
_position += buffer.Length; | ||
|
||
return JSReference.InvokeVoidAsync("write", buffer); | ||
} | ||
|
||
protected override void Dispose(bool disposing) | ||
{ | ||
if (_jSReference is { } jsReference) | ||
{ | ||
_jSReference = null; | ||
jsReference.InvokeVoid("close"); | ||
jsReference.Dispose(); | ||
} | ||
} | ||
|
||
public override async ValueTask DisposeAsync() | ||
{ | ||
if (_jSReference is { } jsReference) | ||
{ | ||
_jSReference = null; | ||
await jsReference.InvokeVoidAsync("close"); | ||
await jsReference.DisposeAsync(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.