From 10b203b14b62e2092d589e85800abfc2421719b4 Mon Sep 17 00:00:00 2001 From: sghctoma Date: Fri, 22 Dec 2023 10:38:26 +0100 Subject: [PATCH] Fix IOSStorageFolder permission problem (#13976) * Fix IOSStorageFolder permission problem This commit fixes "Operation not permitted" errors when accessing folders opened via StorageProvider.OpenFolderPickerAsync by calling [Start/Stop]AccessingSecurityScopedResource on IOSStorageFolder's Url. * Narrow down security scoped resource access scope Wrap individual operations in [Start/Stop]AccessingSecurityScopedResource calls instead of retaining access for the whole lifetime of the folder. --- .../Storage/IOSSecurityScopedStream.cs | 8 +- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 188 ++++++++++++------ 2 files changed, 128 insertions(+), 68 deletions(-) diff --git a/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs b/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs index 8349258c98..424ec7589a 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs @@ -13,13 +13,15 @@ internal sealed class IOSSecurityScopedStream : Stream private readonly UIDocument _document; private readonly FileStream _stream; private readonly NSUrl _url; + private readonly NSUrl _securityScopedAncestorUrl; - internal IOSSecurityScopedStream(NSUrl url, FileAccess access) + internal IOSSecurityScopedStream(NSUrl url, NSUrl securityScopedAncestorUrl, FileAccess access) { _document = new UIDocument(url); var path = _document.FileUrl.Path!; _url = url; - _url.StartAccessingSecurityScopedResource(); + _securityScopedAncestorUrl = securityScopedAncestorUrl; + _securityScopedAncestorUrl.StartAccessingSecurityScopedResource(); _stream = File.Open(path, FileMode.Open, access); } @@ -60,7 +62,7 @@ internal sealed class IOSSecurityScopedStream : Stream { _stream.Dispose(); _document.Dispose(); - _url.StopAccessingSecurityScopedResource(); + _securityScopedAncestorUrl.StopAccessingSecurityScopedResource(); } } } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 09fd6e88ec..f6697777be 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -17,9 +17,10 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem { private readonly string _filePath; - protected IOSStorageItem(NSUrl url) + protected IOSStorageItem(NSUrl url, NSUrl? securityScopedAncestorUrl = null) { Url = url ?? throw new ArgumentNullException(nameof(url)); + SecurityScopedAncestorUrl = securityScopedAncestorUrl ?? url; using (var doc = new UIDocument(url)) { @@ -32,6 +33,11 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem } internal NSUrl Url { get; } + // Calling StartAccessingSecurityScopedResource on items retrieved from, or created in a folder + // fails, because only folders directly opened via StorageProvider.OpenFolderPickerAsync have + // security-scoped NSUrls. This property stores and exposes that ancestor's Url, so we can have + // recursive access to an opened folder. + internal NSUrl SecurityScopedAncestorUrl { get; } internal string FilePath => _filePath; public bool CanBookmark => true; @@ -57,39 +63,59 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem public Task GetParentAsync() { - return Task.FromResult(new IOSStorageFolder(Url.RemoveLastPathComponent())); + return Task.FromResult(new IOSStorageFolder(Url.RemoveLastPathComponent(), SecurityScopedAncestorUrl)); } public Task DeleteAsync() { - return NSFileManager.DefaultManager.Remove(Url, out var error) - ? Task.CompletedTask - : Task.FromException(new NSErrorException(error)); + try + { + SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource(); + + return NSFileManager.DefaultManager.Remove(Url, out var error) + ? Task.CompletedTask + : Task.FromException(new NSErrorException(error)); + } + finally + { + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); + } } - public Task MoveAsync(IStorageFolder destination) + public async Task MoveAsync(IStorageFolder destination) { if (destination is not IOSStorageFolder folder) { throw new InvalidOperationException("Destination folder must be initialized the StorageProvider API."); } - var isDir = this is IStorageFolder; - var newPath = new NSUrl(System.IO.Path.Combine(folder.FilePath, Name), isDir); - - if (NSFileManager.DefaultManager.Move(folder.Url, newPath, out var error)) + try { - return isDir - ? Task.FromResult(new IOSStorageFolder(newPath)) - : Task.FromResult(new IOSStorageFile(newPath)); - } + SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource(); + folder.SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource(); - if (error is not null) + var isDir = this is IStorageFolder; + var newPath = new NSUrl(System.IO.Path.Combine(folder.FilePath, Name), isDir); + + if (NSFileManager.DefaultManager.Move(Url, newPath, out var error)) + { + return isDir + ? new IOSStorageFolder(newPath) + : new IOSStorageFile(newPath); + } + + if (error is not null) + { + throw new NSErrorException(error); + } + + return null; + } + finally { - throw new NSErrorException(error); + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); + folder.SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); } - - return Task.FromResult(null); } public Task ReleaseBookmarkAsync() @@ -102,7 +128,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem { try { - if (!Url.StartAccessingSecurityScopedResource()) + if (!SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource()) { return Task.FromResult(null); } @@ -120,7 +146,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem } finally { - Url.StopAccessingSecurityScopedResource(); + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); } } @@ -131,89 +157,121 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile { - public IOSStorageFile(NSUrl url) : base(url) + public IOSStorageFile(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base(url, securityScopedAncestorUrl) { } public Task OpenReadAsync() { - return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); + return Task.FromResult(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Read)); } public Task OpenWriteAsync() { - return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Write)); + return Task.FromResult(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Write)); } } internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder { - public IOSStorageFolder(NSUrl url) : base(url) + public IOSStorageFolder(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base(url, securityScopedAncestorUrl) { } public async IAsyncEnumerable GetItemsAsync() { - // TODO: find out if it can be lazily enumerated. - var tcs = new TaskCompletionSource>(); + try + { + SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource(); - new NSFileCoordinator().CoordinateRead(Url, - NSFileCoordinatorReadingOptions.WithoutChanges, - out var error, - uri => - { - var content = NSFileManager.DefaultManager.GetDirectoryContent(uri, null, NSDirectoryEnumerationOptions.None, out var error); - if (error is not null) - { - tcs.TrySetException(new NSErrorException(error)); - } - else + // TODO: find out if it can be lazily enumerated. + var tcs = new TaskCompletionSource>(); + + new NSFileCoordinator().CoordinateRead(Url, + NSFileCoordinatorReadingOptions.WithoutChanges, + out var error, + uri => { - var items = content - .Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u)) - .ToArray(); - tcs.TrySetResult(items); - } - }); + var content = NSFileManager.DefaultManager.GetDirectoryContent(uri, null, NSDirectoryEnumerationOptions.None, out var error); + if (error is not null) + { + tcs.TrySetException(new NSErrorException(error)); + } + else + { + var items = content + .Select(u => u.HasDirectoryPath ? + (IStorageItem)new IOSStorageFolder(u, SecurityScopedAncestorUrl) : + new IOSStorageFile(u, SecurityScopedAncestorUrl)) + .ToArray(); + tcs.TrySetResult(items); + } + }); - if (error is not null) - { - throw new NSErrorException(error); - } + if (error is not null) + { + throw new NSErrorException(error); + } - var items = await tcs.Task; - foreach (var item in items) + var items = await tcs.Task; + foreach (var item in items) + { + yield return item; + } + } + finally { - yield return item; + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); } } public Task CreateFileAsync(string name) { - var path = System.IO.Path.Combine(FilePath, name); - NSFileAttributes? attributes = null; - if (NSFileManager.DefaultManager.CreateFile(path, null, attributes)) + try { - return Task.FromResult(new IOSStorageFile(new NSUrl(path, false))); - } + if (!SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource()) + { + return Task.FromResult(null); + } - return Task.FromResult(null); + var path = System.IO.Path.Combine(FilePath, name); + NSFileAttributes? attributes = null; + if (NSFileManager.DefaultManager.CreateFile(path, new NSData(), attributes)) + { + return Task.FromResult(new IOSStorageFile(new NSUrl(path, false), SecurityScopedAncestorUrl)); + } + + return Task.FromResult(null); + } + finally + { + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); + } } public Task CreateFolderAsync(string name) { - var path = System.IO.Path.Combine(FilePath, name); - NSFileAttributes? attributes = null; - if (NSFileManager.DefaultManager.CreateDirectory(path, false, attributes, out var error)) + try { - return Task.FromResult(new IOSStorageFolder(new NSUrl(path, true))); - } + SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource(); - if (error is not null) + var path = System.IO.Path.Combine(FilePath, name); + NSFileAttributes? attributes = null; + if (NSFileManager.DefaultManager.CreateDirectory(path, false, attributes, out var error)) + { + return Task.FromResult(new IOSStorageFolder(new NSUrl(path, true), SecurityScopedAncestorUrl)); + } + + if (error is not null) + { + throw new NSErrorException(error); + } + + return Task.FromResult(null); + } + finally { - throw new NSErrorException(error); + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); } - - return Task.FromResult(null); } }