1
0
Эх сурвалжийг харах

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.
sghctoma 1 жил өмнө
parent
commit
10b203b14b

+ 5 - 3
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();
         }
     }
 }

+ 123 - 65
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<IStorageFolder?> GetParentAsync()
     {
-        return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent()));
+        return Task.FromResult<IStorageFolder?>(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<IStorageItem?> MoveAsync(IStorageFolder destination)
+    public async Task<IStorageItem?> 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<IStorageItem?>(new IOSStorageFolder(newPath))
-                : Task.FromResult<IStorageItem?>(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<IStorageItem?>(null);
     }
 
     public Task ReleaseBookmarkAsync()
@@ -102,7 +128,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
     {
         try
         {
-            if (!Url.StartAccessingSecurityScopedResource())
+            if (!SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource())
             {
                 return Task.FromResult<string?>(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<Stream> OpenReadAsync()
     {
-        return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read));
+        return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Read));
     }
 
     public Task<Stream> OpenWriteAsync()
     {
-        return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Write));
+        return Task.FromResult<Stream>(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<IStorageItem> GetItemsAsync()
     {
-        // TODO: find out if it can be lazily enumerated.
-        var tcs = new TaskCompletionSource<IReadOnlyList<IStorageItem>>();
+        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<IReadOnlyList<IStorageItem>>();
+
+            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<IStorageFile?> 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<IStorageFile?>(new IOSStorageFile(new NSUrl(path, false)));
-        }
+            if (!SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource())
+            {
+                return Task.FromResult<IStorageFile?>(null);
+            }
 
-        return Task.FromResult<IStorageFile?>(null);
+            var path = System.IO.Path.Combine(FilePath, name);
+            NSFileAttributes? attributes = null;
+            if (NSFileManager.DefaultManager.CreateFile(path, new NSData(), attributes))
+            {
+                return Task.FromResult<IStorageFile?>(new IOSStorageFile(new NSUrl(path, false), SecurityScopedAncestorUrl));
+            }
+
+            return Task.FromResult<IStorageFile?>(null);
+        }
+        finally
+        {
+            SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
+        }
     }
 
     public Task<IStorageFolder?> 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<IStorageFolder?>(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<IStorageFolder?>(new IOSStorageFolder(new NSUrl(path, true), SecurityScopedAncestorUrl));
+            }
+
+            if (error is not null)
+            {
+                throw new NSErrorException(error);
+            }
+
+            return Task.FromResult<IStorageFolder?>(null);
+        }
+        finally
         {
-            throw new NSErrorException(error);
+            SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
         }
-        
-        return Task.FromResult<IStorageFolder?>(null);
     }
 }