From fce56827e2e3bfd700850fe3449dd4b82f364275 Mon Sep 17 00:00:00 2001
From: nickodei <46863421+nickodei@users.noreply.github.com>
Date: Thu, 23 Jan 2025 16:07:12 +0100
Subject: [PATCH] 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
---
api/Avalonia.nupkg.xml | 12 +++
.../Platform/Storage/AndroidStorageItem.cs | 73 +++++++++++++++++++
.../Storage/FileIO/BclStorageFolder.cs | 6 ++
.../Platform/Storage/FileIO/BclStorageItem.cs | 22 ++++++
.../Platform/Storage/IStorageFolder.cs | 18 +++++
src/Avalonia.Native/StorageItem.cs | 14 ++++
.../Avalonia.Browser/Interop/StorageHelper.cs | 6 ++
.../Storage/BrowserStorageProvider.cs | 43 +++++++++++
.../webapp/modules/storage/storageItem.ts | 20 +++++
.../Avalonia.iOS/Storage/IOSStorageItem.cs | 32 ++++++++
10 files changed, 246 insertions(+)
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index f6797ada37..6b0f1c59cc 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -49,6 +49,18 @@
baseline/netstandard2.0/Avalonia.Controls.dll
target/netstandard2.0/Avalonia.Controls.dll
+
+ CP0006
+ M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String)
+ baseline/netstandard2.0/Avalonia.Base.dll
+ target/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Platform.Storage.IStorageFolder.GetFolderAsync(System.String)
+ baseline/netstandard2.0/Avalonia.Base.dll
+ target/netstandard2.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object)
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
index bb27379a70..330f2ded86 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
@@ -294,6 +294,79 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
return destination;
}
}
+
+ private async Task 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 GetFolderAsync(string name)
+ {
+ var folder = await GetItemAsync(name, true);
+ return (IStorageFolder?)folder;
+ }
+
+ public async Task GetFileAsync(string name)
+ {
+ var file = await GetItemAsync(name, false);
+ return (IStorageFile?)file;
+ }
}
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
index 05572d6058..73f75355d6 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
@@ -19,4 +19,10 @@ internal sealed class BclStorageFolder(DirectoryInfo directoryInfo)
public Task CreateFolderAsync(string name) => Task.FromResult(
(IStorageFolder?)WrapFileSystemInfo(CreateFolderCore(directoryInfo, name)));
+
+ public Task GetFolderAsync(string name) => Task.FromResult(
+ (IStorageFolder?)WrapFileSystemInfo(GetFolderCore(directoryInfo, name)));
+
+ public Task GetFileAsync(string name) => Task.FromResult(
+ (IStorageFile?)WrapFileSystemInfo(GetFileCore(directoryInfo, name)));
}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs
index 123d0e9283..294fc2c579 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs
@@ -127,6 +127,28 @@ internal abstract class BclStorageItem(FileSystemInfo fileSystemInfo) : IStorage
.OfType()
.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);
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
index a9d1ff3669..b6d8fd4daf 100644
--- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
@@ -18,6 +18,24 @@ public interface IStorageFolder : IStorageItem
///
IAsyncEnumerable GetItemsAsync();
+ ///
+ /// Gets the folder with the specified name from the current folder.
+ ///
+ /// The name of the folder to get
+ ///
+ /// When this method completes successfully, it returns the folder with the specified name from the current folder.
+ ///
+ Task GetFolderAsync(string name);
+
+ ///
+ /// Gets the file with the specified name from the current folder.
+ ///
+ /// The name of the file to get
+ ///
+ /// When this method completes successfully, it returns the file with the specified name from the current folder.
+ ///
+ Task GetFileAsync(string name);
+
///
/// Creates a file with specified name as a child of the current storage folder
///
diff --git a/src/Avalonia.Native/StorageItem.cs b/src/Avalonia.Native/StorageItem.cs
index 12e4cc0a5e..efa0404a34 100644
--- a/src/Avalonia.Native/StorageItem.cs
+++ b/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 GetFolderAsync(string name)
+ {
+ using var scope = OpenScope();
+ var item = BclStorageItem.GetFolderCore(directoryInfo, name);
+ return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(item, ScopeOwnerUri));
+ }
+
+ public Task GetFileAsync(string name)
+ {
+ using var scope = OpenScope();
+ var item = BclStorageItem.GetFileCore(directoryInfo, name);
+ return Task.FromResult((IStorageFile?)WrapFileSystemInfo(item, ScopeOwnerUri));
+ }
}
diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
index c28efbb308..76f1273f2e 100644
--- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
+++ b/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 CreateFolder(JSObject folderHandle, string name);
+
+ [JSImport("StorageItem.getFile", AvaloniaModule.StorageModuleName)]
+ public static partial Task GetFile(JSObject folderHandle, string name);
+
+ [JSImport("StorageItem.getFolder", AvaloniaModule.StorageModuleName)]
+ public static partial Task GetFolder(JSObject folderHandle, string name);
}
diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
index 6a89a13e02..cef13b4d3b 100644
--- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
+++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
@@ -16,6 +16,8 @@ internal class BrowserStorageProvider : IStorageProvider
internal static ReadOnlySpan 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 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 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);
}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
index 56833e448c..f30e6f5916 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
+++ b/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);
}
+ public static async getFile(item: StorageItem, name: string): Promise {
+ if (item.kind !== "directory" || !item.handle) {
+ return null;
+ }
+
+ await item.verityPermissions("read");
+
+ return await ((item.handle as any).getFileHandle(name) as Promise);
+ }
+
public static async createFolder(item: StorageItem, name: string): Promise {
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);
}
+ public static async getFolder(item: StorageItem, name: string): Promise {
+ if (item.kind !== "directory" || !item.handle) {
+ return null;
+ }
+
+ await item.verityPermissions("read");
+
+ return await ((item.handle as any).getDirectoryHandle(name) as Promise);
+ }
+
public static async deleteAsync(item: StorageItem): Promise {
if (!item.handle) {
return null;
diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
index fa086c7d61..069b1e8d49 100644
--- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
+++ b/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 GetFolderAsync(string name)
+ {
+ var url = GetItem(name, true);
+ return Task.FromResult(url is null ? null : new IOSStorageFolder(url));
+ }
+
+ public Task GetFileAsync(string name)
+ {
+ var url = GetItem(name, false);
+ return Task.FromResult(url is null ? null : new IOSStorageFile(url));
+ }
}