Browse Source

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.
pull/14023/head
sghctoma 3 years ago
committed by GitHub
parent
commit
10b203b14b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs
  2. 188
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

8
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();
}
}
}

188
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);
}
}

Loading…
Cancel
Save