in src/Avalonia.Native/StorageProviderApi.cs [19:297]
internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnabled) : IStorageProviderFactory, IDisposable
{
private readonly Dictionary<string, int> _openScopes = new();
private readonly IAvnStorageProvider _native = native;
public IStorageProvider CreateProvider(TopLevel topLevel)
{
return new StorageProviderImpl((TopLevelImpl)topLevel.PlatformImpl!, this);
}
public IStorageItem? TryGetStorageItem(Uri? itemUri, bool create = false)
{
if (itemUri is not null && StorageProviderHelpers.TryGetPathFromFileUri(itemUri) is { } itemPath)
{
if (new FileInfo(itemPath) is { } fileInfo
&& (create || fileInfo.Exists))
{
return sandboxEnabled
? new StorageFile(this, fileInfo, itemUri, itemUri)
: new BclStorageFile(fileInfo);
}
if (new DirectoryInfo(itemPath) is { } directoryInfo
&& (create || directoryInfo.Exists))
{
return sandboxEnabled
? new StorageFolder(this, directoryInfo, itemUri, itemUri)
: new BclStorageFolder(directoryInfo);
}
}
return null;
}
public IDisposable? OpenSecurityScope(string uriString)
{
// Multiple entries are possible.
// For example, user might open OpenRead stream, and read file properties before closing the file.
// If we don't check for nested scopes, inner closing scope will break access of the outer scope.
if (AddUse(this, uriString) == 1)
{
using var nsUriString = new AvnString(uriString);
var scopeOpened = _native.OpenSecurityScope(nsUriString).FromComBool();
if (!scopeOpened)
{
RemoveUse(this, uriString);
Logger.TryGet(LogEventLevel.Information, LogArea.macOSPlatform)?
.Log(this, "OpenSecurityScope returned false for the {Uri}", uriString);
return null;
}
}
return Disposable.Create((api: this, uriString), static state =>
{
if (RemoveUse(state.api, state.uriString) == 0)
{
using var nsUriString = new AvnString(state.uriString);
state.api._native.CloseSecurityScope(nsUriString);
}
});
static int AddUse(StorageProviderApi api, string uriString)
{
lock (api)
{
api._openScopes.TryGetValue(uriString, out var useValue);
api._openScopes[uriString] = ++useValue;
return useValue;
}
}
static int RemoveUse(StorageProviderApi api, string uriString)
{
lock (api)
{
api._openScopes.TryGetValue(uriString, out var useValue);
useValue--;
if (useValue == 0)
api._openScopes.Remove(uriString);
else
api._openScopes[uriString] = useValue;
return useValue;
}
}
}
// Avalonia.Native technically can be used for more than just macOS,
// In which case we should provide different bookmark platform keys, and parse accordingly.
private static ReadOnlySpan<byte> MacOSKey => "macOS"u8;
public unsafe string? SaveBookmark(Uri uri)
{
void* error = null;
using var uriString = new AvnString(uri.AbsoluteUri);
using var bookmarkStr = _native.SaveBookmarkToBytes(uriString, &error);
if (error != null)
{
using var errorStr = MicroComRuntime.CreateProxyOrNullFor<IAvnString>(error, true);
Logger.TryGet(LogEventLevel.Warning, LogArea.macOSPlatform)?
.Log(this, "SaveBookmark for {Uri} failed with an error\r\n{Error}", uri, errorStr.String);
return null;
}
return StorageBookmarkHelper.EncodeBookmark(MacOSKey, bookmarkStr?.Bytes);
}
// Support both kinds of bookmarks when reading.
// Since "save bookmark" implementation will be different depending on the configuration.
public unsafe Uri? ReadBookmark(string bookmark, bool isDirectory)
{
if (StorageBookmarkHelper.TryDecodeBookmark(MacOSKey, bookmark, out var bytes) == StorageBookmarkHelper.DecodeResult.Success)
{
fixed (byte* ptr = bytes)
{
using var uriString = _native.ReadBookmarkFromBytes(ptr, bytes!.Length);
return uriString is not null && Uri.TryCreate(uriString.String, UriKind.Absolute, out var uri) ?
uri :
null;
}
}
if (StorageBookmarkHelper.TryDecodeBclBookmark(bookmark, out var path))
{
return StorageProviderHelpers.UriFromFilePath(path, isDirectory);
}
return null;
}
public void ReleaseBookmark(Uri uri)
{
using var uriString = new AvnString(uri.AbsoluteUri);
_native.ReleaseBookmark(uriString);
}
public void Dispose()
{
_native.Dispose();
}
public async Task<IReadOnlyList<IStorageFile>> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var results = await OpenDialogAsync(events =>
{
_native.OpenFileDialog((IAvnWindow?)topLevel?.Native,
events,
options.AllowMultiple.AsComBool(),
options.Title ?? string.Empty,
suggestedDirectory,
options.SuggestedFileName ?? string.Empty,
fileTypes);
});
return results.OfType<IStorageFile>().ToArray();
}
public async Task<IStorageFile?> SaveFileDialog(TopLevelImpl? topLevel, FilePickerSaveOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var results = await OpenDialogAsync(events =>
{
_native.SaveFileDialog((IAvnWindow?)topLevel?.Native,
events,
options.Title ?? string.Empty,
suggestedDirectory,
options.SuggestedFileName ?? string.Empty,
fileTypes);
}, create: true);
return results.OfType<IStorageFile>().FirstOrDefault();
}
public async Task<IReadOnlyList<IStorageFolder>> SelectFolderDialog(TopLevelImpl? topLevel, FolderPickerOpenOptions options)
{
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var results = await OpenDialogAsync(events =>
{
_native.SelectFolderDialog((IAvnWindow?)topLevel?.Native,
events,
options.AllowMultiple.AsComBool(),
options.Title ?? "",
suggestedDirectory);
});
return results.OfType<IStorageFolder>().ToArray();
}
public async Task<IEnumerable<IStorageItem>> OpenDialogAsync(Action<SystemDialogEvents> runDialog, bool create = false)
{
using var events = new SystemDialogEvents();
runDialog(events);
var result = await events.Task.ConfigureAwait(false);
return (result?
.Select(f => Uri.TryCreate(f, UriKind.Absolute, out var uri) ? TryGetStorageItem(uri, create) : null)
.Where(f => f is not null) ?? [])!;
}
public Uri? TryResolveFileReferenceUri(Uri uri)
{
using var uriString = new AvnString(uri.AbsoluteUri);
using var resultString = _native.TryResolveFileReferenceUri(uriString);
return Uri.TryCreate(resultString?.String, UriKind.Absolute, out var resultUri) ? resultUri : null;
}
internal class FilePickerFileTypesWrapper(
IReadOnlyList<FilePickerFileType>? types,
string? defaultExtension)
: NativeCallbackBase, IAvnFilePickerFileTypes
{
private readonly List<IDisposable> _disposables = new();
public int Count => types?.Count ?? 0;
public int IsDefaultType(int index) => (defaultExtension is not null &&
types![index].TryGetExtensions()?.Any(defaultExtension.EndsWith) == true).AsComBool();
public int IsAnyType(int index) =>
(types![index].Patterns?.Contains("*.*") == true || types[index].MimeTypes?.Contains("*.*") == true)
.AsComBool();
public IAvnString GetName(int index)
{
return EnsureDisposable(types![index].Name.ToAvnString());
}
public IAvnStringArray GetPatterns(int index)
{
return EnsureDisposable(new AvnStringArray(types![index].Patterns ?? Array.Empty<string>()));
}
public IAvnStringArray GetExtensions(int index)
{
return EnsureDisposable(new AvnStringArray(types![index].TryGetExtensions() ?? Array.Empty<string>()));
}
public IAvnStringArray GetMimeTypes(int index)
{
return EnsureDisposable(new AvnStringArray(types![index].MimeTypes ?? Array.Empty<string>()));
}
public IAvnStringArray GetAppleUniformTypeIdentifiers(int index)
{
return EnsureDisposable(new AvnStringArray(types![index].AppleUniformTypeIdentifiers ?? Array.Empty<string>()));
}
protected override void Destroyed()
{
foreach (var disposable in _disposables)
{
disposable.Dispose();
}
}
private T EnsureDisposable<T>(T input) where T : IDisposable
{
_disposables.Add(input);
return input;
}
}
internal class SystemDialogEvents : NativeCallbackBase, IAvnSystemDialogEvents
{
private readonly TaskCompletionSource<string[]> _tcs = new();
public Task<string[]> Task => _tcs.Task;
public void OnCompleted(IAvnStringArray? ppv)
{
using (ppv)
{
_tcs.SetResult(ppv?.ToStringArray() ?? []);
}
}
}
}