Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New file picker API #7234

Closed
kekekeks opened this issue Dec 21, 2021 · 12 comments · Fixed by #8303
Closed

New file picker API #7234

kekekeks opened this issue Dec 21, 2021 · 12 comments · Fixed by #8303
Milestone

Comments

@kekekeks
Copy link
Member

Our current file dialogs are desktop-centric and assume that the app has direct access to the file system, which is not the case for sandboxed environments.

While keeping the current API for desktop platforms, we need to design a new one that would work well with WebAssembly, macOS App Store apps, iOS and Android (possibly with UWP too, but I'm not sure how relevant it is since we don't support XBox and HoloLens anyway).

Main differences of the API would be:

  1. the lack of direct access to the file system, so the API should provide Streams instead of file paths
  2. apps might need to implement extra functionality to conform with the OS guidelines (e. g. subscribe to file changes via NSFilePresenter in case of iOS
  3. directory access would be more complicated (e. g. https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories?language=objc), so we might want to skip it in the initial implementation
  4. some sandboxes allow to save a file "bookmark" that allows the app to access the same file after restart (see [OSX] App Store Sandbox Bookmarks API #6540)
  5. more complicated required file type information rather than simply a file extension. For example iOS requires to specify a uniform type identifier that are not MIME-types (e. g. com.adobe.pdf, org.idpf.epub-container, public.mp3
  6. sometimes it's only possible to "export" a file. In case of WebAssembly saving file means creating an object URI from ReadableStream-backed Response object and waiting for browser to start a download), we can only report the intended file name and file type via a virtual Content-Disposition header. In case of iOS that would be the Share function
  7. Some platforms (e. g. iOS) has different APIs for accessing documents (UIDocumentPickerViewController) and photos/videos (UIImagePickerController)
class FilePickerOpenOptions
{
    string Title { get; set; }
    string DefaultFileName { get; set; }
    IList<FilePickerFileType> FileTypes { get; set; }
}

class FilePickerOpenOptions
{
    string Title { get; set; }
    string DefaultFileName { get; set; }
    FilePickerFileType DefaultFileType {get; set; }
    IList<FilePickerFileType> FileTypes { get; set; }
}


class FilePickerFileType
{
    // For desktop
    string Title { get; init; }
    IReadOnlyList<string> Extensions {get; init; }
    // For web
    IReadOnlyList<string> MimeTypes {get; init; }
    // For Apple platforms
    IReadOnlyList<string> AppleUniformTypeIdentifiers {get; init; }

}

// We should provide some built-in types for common file types
class FilePickerFileTypes
{
     static FilePickerFileType Pdf {get;} = new FilePickerFileType
     {
        Title = "PDF document",
        Extensions = new [] {"pdf"},
        AppleUniformTypeIdentifiers = new[] {"com.adobe.pdf"},
        MimeTypes = new [] { "application/pdf" }
     };
}

interface IFilePicker
{
    bool CanOpen { get; }
    Task<IFilePickerFile> OpenAsync(FilePickerOptions options);
    bool CanSave { get; }
    Task<IFilePickerFile> SaveAsync(FilePickerOptions options);
    bool CanExport { get; }
    Task Export(FilePickerOptions options, Func<Stream, FilePickerFileType, Task> writer);
    IFilePickerBookmark OpenBookmark(string bookmark);
}

interface IFilePickerWriteContext
{
    Stream Stream {get;}
    FilePickerFileType FileType {get;}
}

interface IFilePickerBookmark : IDisposable
{
    bool CanOpenRead {get;}
    bool CanOpenWrite {get;}
    Task<Stream> OpenRead();
    Task<Stream> OpenWrite();
}

interface IFilePickerFile : IDisposable
{
     Stream Stream {get;}
     bool CanBookmark{get;}
     Task<string> SaveBookmark();
}
@kekekeks
Copy link
Member Author

@maxkatz6 @Mikolaytis

@maxkatz6
Copy link
Member

What about:

interface IFilePickerFile : IDisposable
{
     string FileName {get;}
     Stream Stream {get;}
     bool CanBookmark{get;}
     Task<string> SaveBookmark();
     bool TryGetFullPath(out string path);
}

@maxkatz6
Copy link
Member

Is IFilePickerFile.Stream supposed to be read only? And writeable at the same time only on supported platforms?

sometimes it's only possible to "export" a file. In case of WebAssembly saving file means creating an object URI from ReadableStream-backed Response object and waiting for browser to start a download)

I suppose it's about older browsers including firefox, right? With newer FileAPI it's possible to open file and write to it without "downloading" it https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API#writing_to_files

@kekekeks
Copy link
Member Author

string FileName {get;}
bool TryGetFullPath(out string path);

Yes, we could provide that optional info.

I suppose it's about older browsers including firefox, right?

изображение

I'm also pretty sure that it's not possible to write to files on iOS because of the way the sandbox works.

@workgroupengineering
Copy link
Contributor

workgroupengineering commented Dec 22, 2021

I think a good design to start defining the API surface is Windows.Storage and Windows.Storage.Pickers.

we can deifine like code this:

// We may have various providers eg. AppleLocalStorage, DropBoxStorage, ..
public interface IStorageProvider
{
   ....
}

public interface IStorageItemMeta
{
   string Name { get ;}    
}

public interface IStorageItemWritableMeta
{
  Task SaveAsync(IStorageItem item, CancellationToken token = default);
}

public interface IStoregeItemStringMeta : IStorageItemWritableMeta
{
   string Value {get; set; }
}

public interface IStoregeItemDateTimeMeta : IStorageItemWritableMeta
{
   DateTimeOffset  Value {get; set; }
}

public interface IStoregeItemLongMeta : IStorageItemWritableMeta
{
   long Value {get; set; }
}

public interface IContentTypeMeta : IStorageItemMeta
{
}


public interface IStorageItem
{
   
  IStorageProvider Provider { get; }
  // Gets the name of the item including the file name extension if there is one.
  string Name {get; }
  // Gets the full file-system path of the item, if the item has a path.
  string? Path {get;}
  // Indicates if the file is local, is cached locally, or can be downloaded.
  bool IsAvailable {get;}

  IReadOnlyList<IStorageItem>  Owners {get;}
  
  DateTimeOffset  DateCreated { get;}
  DateTimeOffset  DateModified { get;}
   
  Task<IReadOnlyList<IStoregeItemMeta>> GetMetaAsync(CancellationToken token = default);

  bool TryGetMeta(string metaName, out IStoregeItemMeta? meta);

  Task DeleteAsync(CancellationToken token = default);
  Task RenamAsync(string desiredName, CancellationToken token = default);
}


public interface IFileStorage: IStorageItem
{   
    Task<Stream> OpenOrCreate(CancellationToken token = default);
    bool CanRead {get; }
    bool CanWrite {get; }
    // Can be AppleUniformTypeIdentifiers , MimeTypes , Extension,
    IContentTypeMeta ContentType { get; }
}

public interface IFolderStorage: IStorageItem
{
    Task<IStorageItem> GetsItemsAsync (CancellationToken token = default);    
    Task<bool> TryAddItemAsync(IStorageItem item,CancellationToken token = default);
}

public interface IPickerOperation
{
  srting Title {get;set;}
  Environment.SpecialFolder SuggestedStartLocation  {get;set;}
  IIdentity  User {get;set;}
}

public interface IFilePicker:IPickerOperation
{
  string SuggestedFileName {get;set;}
}

public interface IFileOpenPicker:IFilePicker
{   
   IEnumerable<IContentTypeMeta> FilterBy {get;set;}
   bool CheckExist {get;set;}
   Task<IFileStorage?> PickSingleFileAsync(CancellationToken token = default);
   Task<IRealOnlyList<IFileStorage>?> PickMultipleFileAsync(CancellationToken token = default);
}

public interface IFileSavePicker:IFilePicker
{
   bool CanOverwrite {get;set;}
   IReadOnlyList<IContentTypeMeta> FileTypeChoices {get;set;}
   Task<IFileStorage?> PickSaveFileAsync(CancellationToken token = default);
}

public interface IFolderPicker:IPickerOperation
{
   bool CanCreateNew {get;set;}
   Task<IFolderStorage?> PickFolderAsync(CancellationToken token = default);
}

@kekekeks
Copy link
Member Author

kekekeks commented Dec 22, 2021

@workgroupengineering I don't see how exactly does it map to macOS/iOS/Android sandboxes and WASM platform limitations. UWP sandbox is not relevant because it's essentially a dead platform anyway. Note that UWP API compatibility is a non-goal for us while properly mapping to various platform's APIs is.

@workgroupengineering
Copy link
Contributor

I don't see how exactly does it map to macOS/iOS/Android sandboxes and WASM platform limitations.

The mapping is done via IStorageProvider and IContentTypeMeta. IStorageProvider deals with the creation of the appropriate concrete types of IStorageItem with the respective metadata.

UWP sandbox is not relevant because it's essentially a dead platform anyway. Note that UWP API compatibility is a non-goal for us while properly mapping to various platform's APIs is.

The fact that she is dead does not mean that we cannot learn from good things. It is not necessary to re-invent the wheel every time. Uno supports macOS/iOS/Android/WASM/Linux(GTK/Framebuffer) using UWP API design.

Your design seems to me very connected to the macOS world.

Having two models for file system access is confusing to the developer. If I make a desktop app I have to use Avalonia.Dialogs. *, If the app has to run in a sandbox I have to use IFilePicker.

@kekekeks
Copy link
Member Author

The mapping is done via IStorageProvider and IContentTypeMeta. IStorageProvider deals with the creation of the appropriate concrete types of IStorageItem with the respective metadata.

How would that work with ReadableStream-based Response + createObjectURL? The stream becomes readable when user confirms the "download", not before.

Uno supports macOS/iOS/Android/WASM/Linux(GTK/Framebuffer) using UWP API design.

Uno has 3043 throw new NotImplementedException() lines in their code base accorting to GitHub search. What doesn't throw usually only somewhat works.

We don't want to somewhat implement UWP on top of iOS/macOS. We want to provide an API that meets the requirements of all target platforms and allows the developer to conform with the target platform guidelines. That's why use cases were listed in the initial message of this thread. We do care about supporting those use cases, we do not care about doing int in UWP way. Avalonia is cross-platform-first, neither UWP first nor WPF-first

It is not necessary to re-invent the wheel every time.

That's exactly why I'm looking at iOS/macOS, since they've got their sandboxing API right.

Your design seems to me very connected to the macOS world.

Yes, that's because we want to be compatible and conformant to Apple ecosystem since macOS and iOS are our target platforms. We are not willing to ignore Apple guidelines for the sake of having an UWP-like API.

Having two models for file system access is confusing to the developer. If I make a desktop app I have to use Avalonia.Dialogs. *, If the app has to run in a sandbox I have to use IFilePicker.

IFilePicker can and will be implemented on top of "full" file dialogs on platforms that do support them. For example on Linux the "bookmark" would simply be a file path. So if one wants their app sandbox/mobile/wasm compatible, they'll use the new file picker that we are trying to design to be sandbox/mobile/wasm-compatible.

@workgroupengineering
Copy link
Contributor

workgroupengineering commented Dec 23, 2021

The mapping is done via IStorageProvider and IContentTypeMeta. IStorageProvider deals with the creation of the appropriate concrete types of IStorageItem with the respective metadata.

How would that work with ReadableStream-based Response + createObjectURL? The stream becomes readable when user confirms the "download", not before.

it is responsibility of I*Picker using appropriate IStorageProvider after download.

Uno supports macOS/iOS/Android/WASM/Linux(GTK/Framebuffer) using UWP API design.

Uno has 3043 throw new NotImplementedException() lines in their code base accorting to GitHub search. What doesn't throw usually only somewhat works.

Uno's repository is a bit complicated, you can't find an answer with a simple search. A lot of code is generated in the compile phase of the source code generators.
You can see a working example of FilePicker in wasm here https://nuget.info/packages/Avalonia/0.10.11.
Here you will find the supported functions by platform.

We don't want to somewhat implement UWP on top of iOS/macOS. We want to provide an API that meets the requirements of all target platforms and allows the developer to conform with the target platform guidelines. That's why use cases were listed in the initial message of this thread. We do care about supporting those use cases, we do not care about doing int in UWP way. Avalonia is cross-platform-first, neither UWP first nor WPF-first

It is not necessary to re-invent the wheel every time.

That's exactly why I'm looking at iOS/macOS, since they've got their sandboxing API right.

Your design seems to me very connected to the macOS world.

Yes, that's because we want to be compatible and conformant to Apple ecosystem since macOS and iOS are our target platforms. We are not willing to ignore Apple guidelines for the sake of having an UWP-like API.

Having two models for file system access is confusing to the developer. If I make a desktop app I have to use Avalonia.Dialogs. *, If the app has to run in a sandbox I have to use IFilePicker.

IFilePicker can and will be implemented on top of "full" file dialogs on platforms that do support them. For example on Linux the "bookmark" would simply be a file path. So if one wants their app sandbox/mobile/wasm compatible, they'll use the new file picker that we are trying to design to be sandbox/mobile/wasm-compatible.

You have probably misunderstood my intent, I don't want to force the use of the UWP API, but I want to find together a solution that satisfies as many needs as possible. The UWP API seems like a good place to start.

As you say Avalonia and cross platform cannot be guided only by Apple's guidelines but must take the best of all supported platforms.

@kekekeks
Copy link
Member Author

According to your link Uno forces you to write to a temporary buffer before actually asking the user to save the file:

For the download picker, the experience requires the use of CachedFileManager. Triggering PickSaveFileAsync does not actually show the download picker to the user. Instead, only a temporary file is created to allow you to write any content. Afterwards, calling CompleteUpdatesAsync opens the download dialog which allows the user to save the file.

Which makes it not possible to do advanced scenarios like streaming content that doesn't fit into the limited RAM allocated for wasm apps. That is why I've introduced Task Export(FilePickerOptions options, Func<Stream, FilePickerFileType, Task> writer); in my initial draft. I'm pretty sure they have other hacks like this for various platforms just to make the API to look like UWP instead of exposing the way the underlying platform works.

The way we are implementing cross-platform features is to first study the way the underlying platforms work, then try to figure out the API set to fit them all, then compare that to what Qt (the only cross-platform UI toolkit I've seen that gets things right) does if the have that feature and only then look at UWP to check if we can follow the least surprise principle and provide something similar.

@Mikolaytis
Copy link
Contributor

I think that provided solution by @kekekeks is good and do what it needs to do. We can holywar a lot about api. Maybe some minor tweaks can be made, but I think that we need focus more on a feature implementation more than a perfect useful api. I think there will be some minor tweaks in the process of implementation... But overall, again, I think all looks good.

@workgroupengineering
Copy link
Contributor

According to your link Uno forces you to write to a temporary buffer before actually asking the user to save the file:

For the download picker, the experience requires the use of CachedFileManager. Triggering PickSaveFileAsync does not actually show the download picker to the user. Instead, only a temporary file is created to allow you to write any content. Afterwards, calling CompleteUpdatesAsync opens the download dialog which allows the user to save the file.

Which makes it not possible to do advanced scenarios like streaming content that doesn't fit into the limited RAM allocated for wasm apps. That is why I've introduced Task Export(FilePickerOptions options, Func<Stream, FilePickerFileType, Task> writer); in my initial draft. I'm pretty sure they have other hacks like this for various platforms just to make the API to look like UWP instead of exposing the way the underlying platform works.

The way we are implementing cross-platform features is to first study the way the underlying platforms work, then try to figure out the API set to fit them all, then compare that to what Qt (the only cross-platform UI toolkit I've seen that gets things right) does if the have that feature and only then look at UWP to check if we can follow the least surprise principle and provide something similar.

Very interesting I did not know that QT also worked on Android and iOS.

I see here the desing of FileDialog and it is very similar to that of Avalonia.Dialogs. In place of a path returns a uri.

I think it is useful when I select a file in addition to the uri / path to already return information such as the ContentType, the creation, ecc.. date without necessarily opening the stream to retrieve it. In the case of Wasm, instead of downloading the file, only download the metadata.

in any case I would implement an IStorageProvider that would allow the IFilePicker to interface directly to the cloud (drive, dropbox, onedrive).

I take this opportunity to wish you a Merry Christmas

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants