apache / cordova-plugin-file

Apache Cordova File Plugin
https://cordova.apache.org/
Apache License 2.0
742 stars 761 forks source link

Cordova 11 Android throwing FileError Code 12 trying to access fileSystem #560

Open rolinger opened 1 year ago

rolinger commented 1 year ago

Bug Report

Trying to write to localStorage is breaking on Android with Cordova 11.1.0 (cordova-android 11)

Problem

I am using plugin cordova-plugin-file-downloader which is essentially a wrapper for other various plugins. My app code worked on Cordova 10 but is now breaking on Cordova 11. I tracked everything down to a FileError Code 12 which implies the system/folder I am trying to write to doesn't exist, can't be created or doesn't have permission, and the error is being generated from this cordova-plugin-file plugin.

The cordova-plugin-file-downloader call is simply:

downloader.init({folder: 'CustomFolderName', fileSystem: 'file:///storage/emulated/0/'}) ;
   or the default fileSystem:
downloader.init({folder: 'CustomFolderName''}) ;

It fails to initialize and immediately throws the FileError code 12. In <= Cordova 10, this same code worked and on Android devices I could navigate to the devices general Documents folder, open the designated CustomFolderName folder and view the file. I looked at Cordova 10 cordova-plugin-file fileSystem specs and they look the same as cordova 11 fileSystem specs; this I think this is a bug. Or if it has changed, what should I be using now because I don't am not seeing any differences?

What is expected to happen?

Folder created and document written to that folder

What does actually happen?

FileError Code 12 is generated.

Information

Command or Code

Environment, Platform, Device

Windows 10, Android - all devices

Version information

Cordova@11.1 cordova-android@11 cordova-plugin-file@7.0.0

Checklist

breautek commented 1 year ago

Android's scoped storage forbids creating custom directories in external storage root and some other directories. Scoped storage is enforced on all API 29 devices and later.

Additionally, API 29 SDK lacks the FileSystem API for scoped storage, which effectively breaks this plugin as well as any library that depends on using the Java's File APIs when trying to access the external storage.

So instead of using 'file:///storage/emulated/0/', (which may not even exist on all android devices, you should use cordova.file.externalRootDirectory to refer to this directory), try using your application's external storage directory instead, cordova.file.externalDataDirectory.

If using the external storage is not a requirement, it may be better to use the internal storage instead (and avoid all the external storage limitations, including the API 29 problem explained above): cordova.file.dataDirectory.

Let me know if this helps.

rolinger commented 1 year ago

@breautek - thanks for quick response.

On both Simulator (Pixel 3a Android 10) and on real Samsung Flip 4 (Android 13) the cordova.file.externalRootDirectory resolves as file:///storage/emulated/0/ causing the FileError Code 12 on both.

On both, externalDataDirectory resolves as: file:///storage/emulated/0/Android/data/com.myApp/files/

And changing it from externalRootDirectory to externalDataDirectory got past that issue, But still working on other related issues now - just concerned the final saved document is not going to be in a place where the users can access them.

Question: Looking to transition off of cordova-plugin-file-transfer and came across this: https://cordova.apache.org/blog/2017/10/18/from-filetransfer-to-xhr2.html - I presume the LocalFileSystem.PERSISTENT can be replaced with cordova.file.externalDataDirectory or similar? Or is PERSISTENT the only way to make this work?

rolinger commented 1 year ago

@breautek - a follow up on this. Could use your input.

For Android, I cannot write directly to: cordova.file.externalRootDirectory anymore. I guess it changed in API 29/30. However, targeting API 32, I can write to cordova.file.externalRootDirectory + "MyNewFolder" and download my file there. Ok, so I got all that working again.

However for iOS, I am continually perplexed. For iOS I have now used both cordova.file.externalDataDirectory and cordova.file.dataDirectory - in BOTH cases, my app can read the root directories and create the MyNewFolder in both and then write files to that new folder. In subsequent passes, my app can then read the contents of MyNewFolder (in both locations) and see the downloaded file there....ALL via console.log messages.

However, on the device itself, the MyNewFolder and its file are not accessible in any place that I can find. The folder and new file now exists, but how does the user access them? In the device app Files, then in the On my iPhone option, it takes me to an empty folder. But if I manually download a file from Safari, a new folder Downloads appears in the On My iPhone section and the manually downloaded file is now in that folder. Safari must be creating that Downloads folder on the first manual download.

In my app, I then try to find this new Downloads folder (printing the contents of every cordova.file.PATH to read its contents (and maybe use it as the future directory to create MyNewFolder and write my files there) but I can't find the Downloads folder anywhere.

Please help me resolve this. I have spent days on this and am pulling out what little hair I have left.

breautek commented 1 year ago

I presume the LocalFileSystem.PERSISTENT can be replaced with cordova.file.externalDataDirectory or similar? Or is PERSISTENT the only way to make this work?

Not quite. First I want to make clear of two filesystem concepts that Android has:

  1. Internal storage. This is a embedded chip that is tied to the device itself.
  2. External storage. This is an independent storage medium that may or may not exist on the device, or it may change at any time. Most android devices will emulate external storage (in which case it will have a /storage/emulated/ path), but if the user inserts an SD card, then the external storage path may change to /sdcard.

Now Internal storage is protected but every application has it's own internal storage sandbox that it basically as free reign over for the most part. Some directories or files may be read-only, such as the cordova.file.applicationStorageDirectory, which is the installation directory of your app, which you don't have permission to update, however you can create folders in this path.

LocalFileSystem.PERSISTENT is part of the (now defunct) W3C FileSystem API, which the file plugin implements. This is an implementation detail, but currently this uses the Internal storage directory. It's the equivalent to cordova.file.dataDirectory + "files/". If you were to use the File Explorer tool, you'll see the following folder path: /data/data/<app_id>/files/files/.

For Android, all of the cordova.file directory APIs will refer to a Internal storage path, while all cordova.file.external* constants will refer to an External storage path.

You're pretty much free to create any directory structure inside internal storage without permissions, within your app_id sandbox.

External storage has gone through several changes (and further changes are incoming in API 33). And access depends on several factors. For example, every Android app has an external data directory, cordova.file.externalDataDirectory, which like Internal storage, you could read and write to without any permissions.

Inside cordova.file.externalRootDirectory you'll see the several directories for different media as well as Downloads directory, and a few others, which I'm going to refer to these as "External Media" directories.

Android API 28 and earlier, your app could do a lot of different things, everywheres on external storage, including messing around with other app's external storage, as long as you had the READ/WRITE_EXTERNAL_STORAGE permission.

Android API 29 have made changes to make external storage slightly more secure, which including locking down some directories. WRITE_EXTERNAL_STORAGE no longer does anything, and you may write to External Media directories without any special permissions however you may not overwrite files that already exists if it's owned by a different application. Reading files still requires the READ_EXTERNAL_STORAGE permission. On API 29, developers could make use of a requestLegacyExternalStorage flag to revert the behaviour back to API 28 behaviour, but this is a request which the OS may reject. I believe it only honours it for users that already had your app installed, so in otherwords, fresh app installs will not honour this request. Lastly, API 29 is specifically broken with the file plugin because Android does not implement a filesystem API for external storage. This means if the app wants to access the external file storage, it must use the native MediaStorage APIs, which the file plugin does not and cannot really implement.

Starting with API 30, Android does implement filesystem API for external storage which allows for third-party libraries and native code to access the external storage again, which also "fixes" this plugin so to speak. The scoped storage changes that came in API 29 however still applies.

Starting with API 33 (currently not supported), READ_EXTERNAL_STORAGE will no longer work and instead they have READ_MEDIA_* permissions for images, audio, and video.

I can write to cordova.file.externalRootDirectory + "MyNewFolder" and download my file there. Ok, so I got all that working again.

Android doesn't make clear but they have said that some directories will be inaccessible. Therefore I wouldn't rely on using custom directories in external root, and instead either use your app's internal or external directory. Therefore I'd consider preparing a migration strategy if necessary.

However for iOS, I am continually perplexed. For iOS I have now used both cordova.file.externalDataDirectory and cordova.file.dataDirectory.....

I'm significantly less knowledgable with iOS, and I'm not sure how externalDataDirectory behaves for iOS (it's not documented...) but I do know that iOS does not have an internal/external storage concept like Android. iOS storage model is far more simple. Your app has an filesystem sandbox that is completely private. It's comparable to Android's internal storage concept. Sharing files between apps happens via non-filesystem APIs I believe, there is no way so share files between apps using purely the filesystem. So in otherwords, a browser app downloading a file may write to it's app storage sandbox, but use a non-filesystem API to share it in a more public place... For example, in order for the iOS to move a file to a shared location, it must use something like UIDocumentPickerViewController which brings up a save dialog and the user chooses where to put the file. The iOS app itself does not have direct access to anything outside of it's sandbox. I don't believe anything like this is implemented in the file plugin, as it's aimed to be... a filesystem API.

Hope this knowledge helps somehow.

rolinger commented 1 year ago

@breautek - well after a VERY long time of frustration, I finally found the answer for iOS.

Please update the cordova-plugin-file documentation. Honestly, I can't believe its not listed anywhere in official Cordova documentation; especially the cordova-plugin-file docs. For anyone wanting to use cordova-plugin-file (and maybe cordova-plugin-file-transfer) to save files to the public On My iPhone --> Downloads folder, they will need to add the following TWO keys to their apps info.plist file:

    <key>UIFileSharingEnabled</key>
    <true/>
    <key>LSSupportsOpeningDocumentsInPlace</key>
    <true/>

Yup...THATS IT. What this does is create symlink to everything in the apps documents folder (ie: cordova.file.documentsDirectory). When the user goes to the On My iPhone root folder, they will see a Downloads folder AND individual app folders for every app that has these values set to true. This allows users to find/navigate every apps root Documents folder as if it were a part of their general Downloads folder.

Unreal....spun my wheels on this one...well....for at least 3 years. Always coming up with some ugly hybrid that was always breaking; esp with each new version of iOS. But again....in all the years of searching for a solution, I never once saw any reference to these keys before. Unreal its not included in any Apache Cordova docs.

Actually, there is an OPEN request to document this very thing....its three years old! https://github.com/apache/cordova-plugin-file/issues/392 - in fact, if you do a google search for cordova UIFileSharingEnabled VERY little comes back. How come, in general, the community simply does not know about these keys?

Sorry, on a bit of rant....mind blowing how this has escaped so many for so long.

Now...this makes me wonder....are there similar and/or corresponding setting/preferences in Android that make the apps internal directories available to users to browse so we don't have to depend on externalRootDirectory storage locations?

kunalSBasic commented 5 months ago

@breautek - well after a VERY long time of frustration, I finally found the answer for iOS.

Please update the cordova-plugin-file documentation. Honestly, I can't believe its not listed anywhere in official Cordova documentation; especially the cordova-plugin-file docs. For anyone wanting to use cordova-plugin-file (and maybe cordova-plugin-file-transfer) to save files to the public On My iPhone --> Downloads folder, they will need to add the following TWO keys to their apps info.plist file:

    <key>UIFileSharingEnabled</key>
    <true/>
    <key>LSSupportsOpeningDocumentsInPlace</key>
    <true/>

Yup...THATS IT. What this does is create symlink to everything in the apps documents folder (ie: cordova.file.documentsDirectory). When the user goes to the On My iPhone root folder, they will see a Downloads folder AND individual app folders for every app that has these values set to true. This allows users to find/navigate every apps root Documents folder as if it were a part of their general Downloads folder.

Unreal....spun my wheels on this one...well....for at least 3 years. Always coming up with some ugly hybrid that was always breaking; esp with each new version of iOS. But again....in all the years of searching for a solution, I never once saw any reference to these keys before. Unreal its not included in any Apache Cordova docs.

Actually, there is an OPEN request to document this very thing....its three years old! #392 - in fact, if you do a google search for cordova UIFileSharingEnabled VERY little comes back. How come, in general, the community simply does not know about these keys?

Sorry, on a bit of rant....mind blowing how this has escaped so many for so long.

Now...this makes me wonder....are there similar and/or corresponding setting/preferences in Android that make the apps internal directories available to users to browse so we don't have to depend on externalRootDirectory storage locations?

OMG finally found the answer to my problem on which I have been banging my head on since last 4 days. Thank you so much @rolinger for such a detailed answer, really appreciate your effort.