Browse Source

Access single file or folder from IStorageFolder by name (#17771)

* added methods to IStorageFolder to access a single file or folder by name

* relaxed file/folder by name browser acces to throw less errors
pull/17680/head
nickodei 2 years ago
committed by GitHub
parent
commit
fce56827e2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 73
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  3. 6
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  4. 22
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs
  5. 18
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  6. 14
      src/Avalonia.Native/StorageItem.cs
  7. 6
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  8. 43
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  9. 20
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  10. 32
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

12
api/Avalonia.nupkg.xml

@ -49,6 +49,18 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String)</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageFolder.GetFolderAsync(System.String)</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object)</Target>

73
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -294,6 +294,79 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
return destination;
}
}
private async Task<IStorageItem?> GetItemAsync(string name, bool isDirectory)
{
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
var contentResolver = Activity.ContentResolver;
if (contentResolver == null)
{
return null;
}
var root = PermissionRoot ?? Uri;
var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri);
var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId);
var projection = new[]
{
DocumentsContract.Document.ColumnDocumentId,
DocumentsContract.Document.ColumnMimeType,
DocumentsContract.Document.ColumnDisplayName
};
if (childrenUri != null)
{
using var cursor = contentResolver.Query(childrenUri, projection, null, null, null);
if (cursor != null)
{
while (cursor.MoveToNext())
{
var id = cursor.GetString(0);
var mime = cursor.GetString(1);
var fileName = cursor.GetString(2);
if (fileName != name)
{
continue;
}
bool mineDirectory = mime == DocumentsContract.Document.MimeTypeDir;
if (isDirectory != mineDirectory)
{
return null;
}
var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id);
if (uri == null)
{
return null;
}
return isDirectory ? new AndroidStorageFolder(Activity, uri, false, this, root) :
new AndroidStorageFile(Activity, uri, this, root);
}
}
}
return null;
}
public async Task<IStorageFolder?> GetFolderAsync(string name)
{
var folder = await GetItemAsync(name, true);
return (IStorageFolder?)folder;
}
public async Task<IStorageFile?> GetFileAsync(string name)
{
var file = await GetItemAsync(name, false);
return (IStorageFile?)file;
}
}
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder

6
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -19,4 +19,10 @@ internal sealed class BclStorageFolder(DirectoryInfo directoryInfo)
public Task<IStorageFolder?> CreateFolderAsync(string name) => Task.FromResult(
(IStorageFolder?)WrapFileSystemInfo(CreateFolderCore(directoryInfo, name)));
public Task<IStorageFolder?> GetFolderAsync(string name) => Task.FromResult(
(IStorageFolder?)WrapFileSystemInfo(GetFolderCore(directoryInfo, name)));
public Task<IStorageFile?> GetFileAsync(string name) => Task.FromResult(
(IStorageFile?)WrapFileSystemInfo(GetFileCore(directoryInfo, name)));
}

22
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs

@ -127,6 +127,28 @@ internal abstract class BclStorageItem(FileSystemInfo fileSystemInfo) : IStorage
.OfType<FileSystemInfo>()
.Concat(directoryInfo.EnumerateFiles());
internal static FileSystemInfo? GetFolderCore(DirectoryInfo directoryInfo, string name)
{
var path = System.IO.Path.Combine(directoryInfo.FullName, name);
if (Directory.Exists(path))
{
return new DirectoryInfo(path);
}
return null;
}
internal static FileSystemInfo? GetFileCore(DirectoryInfo directoryInfo, string name)
{
var path = System.IO.Path.Combine(directoryInfo.FullName, name);
if (File.Exists(path))
{
return new FileInfo(path);
}
return null;
}
internal static FileInfo CreateFileCore(DirectoryInfo directoryInfo, string name)
{
var fileName = System.IO.Path.Combine(directoryInfo.FullName, name);

18
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@ -18,6 +18,24 @@ public interface IStorageFolder : IStorageItem
/// </returns>
IAsyncEnumerable<IStorageItem> GetItemsAsync();
/// <summary>
/// Gets the folder with the specified name from the current folder.
/// </summary>
/// <param name="name">The name of the folder to get</param>
/// <returns>
/// When this method completes successfully, it returns the folder with the specified name from the current folder.
/// </returns>
Task<IStorageFolder?> GetFolderAsync(string name);
/// <summary>
/// Gets the file with the specified name from the current folder.
/// </summary>
/// <param name="name">The name of the file to get</param>
/// <returns>
/// When this method completes successfully, it returns the file with the specified name from the current folder.
/// </returns>
Task<IStorageFile?> GetFileAsync(string name);
/// <summary>
/// Creates a file with specified name as a child of the current storage folder
/// </summary>

14
src/Avalonia.Native/StorageItem.cs

@ -149,4 +149,18 @@ internal class StorageFolder(
var folder = BclStorageItem.CreateFolderCore(directoryInfo, name);
return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(folder, ScopeOwnerUri));
}
public Task<IStorageFolder?> GetFolderAsync(string name)
{
using var scope = OpenScope();
var item = BclStorageItem.GetFolderCore(directoryInfo, name);
return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(item, ScopeOwnerUri));
}
public Task<IStorageFile?> GetFileAsync(string name)
{
using var scope = OpenScope();
var item = BclStorageItem.GetFileCore(directoryInfo, name);
return Task.FromResult((IStorageFile?)WrapFileSystemInfo(item, ScopeOwnerUri));
}
}

6
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -67,4 +67,10 @@ internal static partial class StorageHelper
[JSImport("StorageProvider.createFolder", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> CreateFolder(JSObject folderHandle, string name);
[JSImport("StorageItem.getFile", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> GetFile(JSObject folderHandle, string name);
[JSImport("StorageItem.getFolder", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> GetFolder(JSObject folderHandle, string name);
}

43
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -16,6 +16,8 @@ internal class BrowserStorageProvider : IStorageProvider
internal static ReadOnlySpan<byte> BrowserBookmarkKey => "browser"u8;
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
internal const string FileFolderNotFoundMessage = "A requested file or directory could not be found";
internal const string TypeMissmatchMessage = "The path supplied exists, but was not an entry of requested type";
public bool CanOpen => true;
public bool CanSave => true;
@ -385,4 +387,45 @@ internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder
throw new UnauthorizedAccessException("User denied permissions to open the file", ex);
}
}
public async Task<IStorageFolder?> GetFolderAsync(string name)
{
try
{
var storageFile = await StorageHelper.GetFolder(FileHandle, name);
if (storageFile is null)
{
return null;
}
return new JSStorageFolder(storageFile);
}
catch (JSException ex) when (ShouldSupressErrorOnFileAccess(ex))
{
return null;
}
}
public async Task<IStorageFile?> GetFileAsync(string name)
{
try
{
var storageFile = await StorageHelper.GetFile(FileHandle, name);
if (storageFile is null)
{
return null;
}
return new JSStorageFile(storageFile);
}
catch (JSException ex) when (ShouldSupressErrorOnFileAccess(ex))
{
return null;
}
}
private static bool ShouldSupressErrorOnFileAccess(JSException ex) =>
ex.Message == BrowserStorageProvider.NoPermissionsMessage ||
ex.Message.Contains(BrowserStorageProvider.TypeMissmatchMessage, StringComparison.Ordinal) ||
ex.Message.Contains(BrowserStorageProvider.FileFolderNotFoundMessage, StringComparison.Ordinal);
}

20
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -107,6 +107,16 @@ export class StorageItem {
return await ((item.handle as any).getFileHandle(name, { create: true }) as Promise<any>);
}
public static async getFile(item: StorageItem, name: string): Promise<any | null> {
if (item.kind !== "directory" || !item.handle) {
return null;
}
await item.verityPermissions("read");
return await ((item.handle as any).getFileHandle(name) as Promise<any>);
}
public static async createFolder(item: StorageItem, name: string): Promise<any | null> {
if (item.kind !== "directory" || !item.handle) {
throw new TypeError("Unable to create item in the requested directory");
@ -117,6 +127,16 @@ export class StorageItem {
return await ((item.handle as any).getDirectoryHandle(name, { create: true }) as Promise<any>);
}
public static async getFolder(item: StorageItem, name: string): Promise<any | null> {
if (item.kind !== "directory" || !item.handle) {
return null;
}
await item.verityPermissions("read");
return await ((item.handle as any).getDirectoryHandle(name) as Promise<any>);
}
public static async deleteAsync(item: StorageItem): Promise<any | null> {
if (!item.handle) {
return null;

32
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -306,4 +306,36 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
private NSUrl? GetItem(string name, bool isDirectory)
{
try
{
SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
var path = System.IO.Path.Combine(FilePath, name);
if (NSFileManager.DefaultManager.FileExists(path, ref isDirectory))
{
return new NSUrl(path, isDirectory);
}
return null;
}
finally
{
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
public Task<IStorageFolder?> GetFolderAsync(string name)
{
var url = GetItem(name, true);
return Task.FromResult<IStorageFolder?>(url is null ? null : new IOSStorageFolder(url));
}
public Task<IStorageFile?> GetFileAsync(string name)
{
var url = GetItem(name, false);
return Task.FromResult<IStorageFile?>(url is null ? null : new IOSStorageFile(url));
}
}

Loading…
Cancel
Save