添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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 [SDK 51] [IOS] expo-file-system readAsStringAsync() is throwing 'File is not readable' error #29058 [SDK 51] [IOS] expo-file-system readAsStringAsync() is throwing 'File is not readable' error #29058 roys000 opened this issue May 22, 2024 · 38 comments · Fixed by #29184

Minimal reproducible example

https://github.com/roys000/testapp

What platform(s) does this occur on?

Did you reproduce this issue in a development build?

No (only happens in a standalone build)

Summary

On iOS, after updating app through eas update , readAsStringAsync() is throwing Calling the 'readAsStringAsync' function has failed -> Caused by: File '/var/mobile/Containers/Data/Application/...' is not readable (See image below). File seems to also exists in the path as checked using getInfoAsync()

Android is fine and the problem does not occur on Expo Go nor development build. It only happens on preview/production build. Problem also seems to have surfaced on SDK 51 as it was working fine before (as tested on SDK 49 & 50).

Steps to replicate

  • Copy App.js and assets directory from the minimal reproducible example into a new project
  • Setup project with eas update and create a standalone preview build for IOS (created through eas build)
  • On launch of application, you should see that it loaded the HTML file properly.
  • Under App.js , change the require asset from test2.html to test.html
  • Publish an update through eas update
  • Re-launch application on device to pull the update, the error will be shown.
  • Is it related to and Android bug: #23288 ?

    They solved it by using this?

    export async function readAssetFile({ file }: { file: any}) {
      const [{ localUri, name, hash, type }] = await Asset.loadAsync(file);
      let uri = localUri ?? '';
      if (!uri?.startsWith('file://')) {
        if (Platform.OS === 'android') {
          uri = `${cacheDirectory}ExponentAsset-${hash}.${type}`;
      return readAsStringAsync(uri, { encoding: 'base64' });
    const imgRequire = require('./assets/test-image.png');
    const imgBase64 = await readAssetFile({file: imgRequire});
              

    @NilsBaumgartner1994 tried the solution from the same Android thread before and it resulted in the same error, suspecting it is throwing the exception when its trying to get the read permission. I purposely omitted the App.json from the example since either way you will need to build your own standalone app + publish an update through eas update. You may copy both the App.js and assets directory to your own project for reproducing the example.

    Definition of the expo-asset config in app.json also seem to be needed only if you require to load the asset at build time and is constraint to a few types which in our case require to load html files to load into a webview which does not seems to be supported.

    Hi @brentvatne can we get the expo team support on this or suggest a temporary workaround this issue?

    @roys000 thanks for clarification. Since i am exceriencing the same issue, i just wanted to double check.

    My "workaround" currently is to put the base64 or file content into a function...

    @brentvatne +1

    I followed the repro instructions, and find that the file is not readable even before doing the update.

    Prebuild is showing me warnings like

    `.html` is not a supported asset type
    

    So I will try this with a supported file type for the asset, and see if I can reproduce the original issue.

    OK after debugging, it appears that the file system utils in expo-modules-core do not include the application support directory when checking for file permissions of files within the app.

    @roys000 @NilsBaumgartner1994 the patch below seems to work for me, could you see if this fixes the issue?

    diff --git a/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift b/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift
    index 5481a53..59f04b4 100644
    --- a/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift
    +++ b/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift
    @@ -10,17 +10,22 @@ public class FileSystemLegacyUtilities: NSObject, EXInternalModule, EXFileSystem
       @objc
       public var cachesDirectory: String
    +  @objc
    +  public var applicationSupportDirectory: String
       var isScoped: Bool = false
       @objc
    -  public init(documentDirectory: String, cachesDirectory: String) {
    +  public init(documentDirectory: String, cachesDirectory: String, applicationSupportDirectory: String) {
         self.documentDirectory = documentDirectory
         self.cachesDirectory = cachesDirectory
    +    self.applicationSupportDirectory = applicationSupportDirectory
         self.isScoped = true
         super.init()
         ensureDirExists(withPath: self.cachesDirectory)
         ensureDirExists(withPath: self.documentDirectory)
    +    ensureDirExists(withPath: self.applicationSupportDirectory)
       required public override init() {
    @@ -30,9 +35,14 @@ public class FileSystemLegacyUtilities: NSObject, EXInternalModule, EXFileSystem
         let cachesPaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
         self.cachesDirectory = cachesPaths[0]
    +    let applicationSupportDirectoryPaths =
    +    NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
    +    self.applicationSupportDirectory = applicationSupportDirectoryPaths[0]
         super.init()
         ensureDirExists(withPath: self.cachesDirectory)
         ensureDirExists(withPath: self.documentDirectory)
    +    ensureDirExists(withPath: self.applicationSupportDirectory)
       public static func exportedInterfaces() -> [Protocol] {
    @@ -84,7 +94,7 @@ public class FileSystemLegacyUtilities: NSObject, EXInternalModule, EXFileSystem
       @objc
       public func getInternalPathPermissions(_ url: URL) -> EXFileSystemPermissionFlags {
    -    let scopedDirs: [String] = [cachesDirectory, documentDirectory]
    +    let scopedDirs: [String] = [cachesDirectory, documentDirectory, applicationSupportDirectory]
         let standardizedPath = url.standardized.path
         for scopedDirectory in scopedDirs {
           if standardizedPath.hasPrefix(scopedDirectory + "/") || standardizedPath == scopedDirectory {
    diff --git a/node_modules/expo-modules-core/ios/Interfaces/FileSystem/EXFileSystemInterface.h b/node_modules/expo-modules-core/ios/Interfaces/FileSystem/EXFileSystemInterface.h
    index 629c67e..fca4649 100644
    --- a/node_modules/expo-modules-core/ios/Interfaces/FileSystem/EXFileSystemInterface.h
    +++ b/node_modules/expo-modules-core/ios/Interfaces/FileSystem/EXFileSystemInterface.h
    @@ -13,6 +13,7 @@ typedef NS_OPTIONS(unsigned int, EXFileSystemPermissionFlags) {
     @property (nonatomic, readonly) NSString *documentDirectory;
     @property (nonatomic, readonly) NSString *cachesDirectory;
    +@property (nonatomic, readonly) NSString *applicationSupportDirectory;
     // TODO: Move permissionsForURI to EXFileSystemManagerInterface
     - (EXFileSystemPermissionFlags)permissionsForURI:(NSURL *)uri;

    Thank you for filing this issue!
    This comment acknowledges we believe this may be a bug and there’s enough information to investigate it.
    However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.

    Hello @douglowder! We're also facing this issue at Backpack. I will try to patch your fix and release a build shortly.

    I can consistently recreate this issue in our iOS production app (we have a shared slack too) so I will let you know what happens.

    Thank you!

    `expo-file-system` internally uses `FileSystemLegacyUtilities.swift`, which was including the cache and documents directories in its permission checks, but not the application support directory where the updates directory is located. This PR adds that directory to the appropriate files. # Test Plan - CI should pass - Bug repro in #29058 should be fixed # Checklist Please check the appropriate items below if they apply to your diff. This is required for changes to Expo modules. - [ ] Documentation is up to date to reflect these changes (eg: https://docs.expo.dev and README.md). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin).
    `expo-file-system` internally uses `FileSystemLegacyUtilities.swift`, which was including the cache and documents directories in its permission checks, but not the application support directory where the updates directory is located. This PR adds that directory to the appropriate files. # Test Plan - CI should pass - Bug repro in #29058 should be fixed # Checklist Please check the appropriate items below if they apply to your diff. This is required for changes to Expo modules. - [ ] Documentation is up to date to reflect these changes (eg: https://docs.expo.dev and README.md). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin).

    I still have the same issue. I use
    "react-native": "^0.74.2",
    "expo-file-system": "^17.0.1",
    "expo": "^51.0.14",

    in bare workflow.

    I'm still experiencing this issue when trying to load assets (using Asset.loadAsync to get the URI) -- error is the same except it it will complain about the asset directory instead of the Application Support directory. Does the asset directory need a similar patch?!

    expo 51.0.18 btw

    Hello, we are receiving this error in our iOS apps now.

    {"code":"ERR_FILE_NOT_WRITABLE"}

    when using

    const chunk = await FileSystem.readAsStringAsync(filePath, {
        encoding: FileSystem.EncodingType.Base64,
        length,
        position,
    

    This is still working fine android. We had upgraded to the latest versions and it was working fine in both iOS and android before the upgrade.

    Current versions:

        "expo": "~51.0.20",
        "expo-file-system": "~17.0.1",
        "expo-dev-client": "~4.0.20"
              

    We still have this issue in our apps, when calling readAsStringAsync

    await FileSystem.readAsStringAsync(fileUri, {
        encoding: FileSystem.EncodingType.Base64,
    

    Error:

    [Error: Calling the 'readAsStringAsync' function has failed
    → Caused by: File '/var/mobile/Media/DCIM/148APPLE/IMG_8518.PNG' is not readable]
    

    FileSystem.getInfoAsync

    {"exists": true, "isDirectory": false, "md5": "9c9ee65f9066d43a6cc605cc693289e1", "modificationTime": 1721906185.4745755, "size": 251522, "uri": "file:///var/mobile/Media/DCIM/148APPLE/IMG_8518.PNG"}
    

    Versions:

    "expo": "51.0.24",
    "expo-dev-client": "4.0.19",
    "expo-file-system": "17.0.1"
              

    I am also running into this: development build works fine, production build on iOS (v16.7 or v17.6 on iPad) errors out because an asset isn't readable. I have not tried Android.

    import { Asset } from 'expo-asset';
    import * as FileSystem from 'expo-file-system';
      const asset = Asset.fromModule(require('@/assets/myFile.zip'));
      await asset.downloadAsync();
      // The following line throws an exception with code ERR_FILE_NOT_READABLE
      const zipFileBase64 = await FileSystem.readAsStringAsync(asset.localUri, { encoding: 'base64' });
    

    Note that the error is ERR_FILE_NOT_READABLE, not ERR_FILE_NOT_EXISTS. The asset file is there inside the .ipa file.

    The value of asset.localUri is something like file:///var/containers/Bundle/Application/xxxxxxxx-xxx-xxx-xxx-xxxxxxxxxxxx/myapp.app/assets/assets/myFile.zip.

    I'm using the default metro configuration (in which, I believe, .zip files are supported as assets) and the following:

        "expo": "~51.0.22",
        "expo-asset": "~10.0.10",
        "expo-file-system": "~17.0.1",
        "react": "18.2.0",
        "react-native": "0.74.3",
    

    Any thoughts or workarounds?

    @douglowder thanks for the quick response. I have another update, we dug deeper into our codebase and realised that we were using our version of Audio Recorder which was saving the recording in a **/tmp folder and this behaviour was broken recently. I am in the process of migrating to expo-av which seems to have resolved the issue. I consider it resolved for our use case so this issue is no longer a priority for us (expo-notifications is more important 😄). Thanks again for the quick turnaround!

    Just encountered a similar issue with react-native-image-crop-picker which stores images in the tmp directory as well. The tmp directory path comes from the NSTemporaryDirectory() call (https://github.com/ivpusic/react-native-image-crop-picker/blob/4d9cb70b02b34333dda44e1c0c85c9c505d6fccf/ios/src/ImageCropPicker.m#L231C30-L231C52) which is an acceptable place to store temporary files https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory.

    Would it make sense to add the temporary directory to FileSystemLegacyUtilities as well?

    @arthuralee I had the same react-native-image-crop-picker error. #30540 fixed the issue for me. I used the following patch in expo-modules-core since it's still unreleased:

    diff --git a/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift b/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift
    index 0463a6d..ec44d45 100644
    --- a/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift
    +++ b/node_modules/expo-modules-core/ios/FileSystemUtilities/FileSystemLegacyUtilities.swift
    @@ -110,10 +110,11 @@ public class FileSystemLegacyUtilities: NSObject, EXInternalModule, EXFileSystem
           return []
         var filePermissions: EXFileSystemPermissionFlags = []
    -    if FileManager.default.isReadableFile(atPath: url.absoluteString) {
    +    // fixed with https://github.com/expo/expo/pull/30540
    +    if FileManager.default.isReadableFile(atPath: url.path) {
           filePermissions.insert(.read)
    -    if FileManager.default.isWritableFile(atPath: url.absoluteString) {
    +    if FileManager.default.isWritableFile(atPath: url.path) {
           filePermissions.insert(.write)
         return filePermissions
              

    @brentvatne

    I'm still running into the issue on iOS (but works on Android), with the following environment:

  • iOS: 15.8.3
  • Expo Client Version: 1017616
  • package.json

  • "expo": "51.0.28"
  • "expo-asset": "~10.0.10"
  • "expo-file-system": "~17.0.1"
  • "expo-updates": "~0.25.22"
  • "react": "18.2.0"
  • "react-native": "0.74.5"
  • It happily returns values for these two calls to retrieve info for a file bundled into the project:
    const sourceAsset = await Asset.fromModule(require('./somePathAndFile.Ext')).downloadAsync()
    const sourceInfo = await FileSystem.getInfoAsync(sourceAsset.localUri, {md5: true, size: true});

    But, the copy call fails (first attachment):
    await FileSystem.copyAsync({from: sourceInfo.uri, to: someTargetFileLocation})

    When I check the development information of the application (second attachment), it is showing ExpoGo SDK of 51.0.0, despite my entry in the package.json. Is there a way to force the client to use an explicit major.minor.patch version?

    Hello, sadly I have encountered the original issue as well. Any workarounds?

    expo and file-system are both up-to-date

    I get:
    FileSystem.getInfoAsync(file://uri).exists = true
    However:
    FileSystem.readAsStringAsync(file://uri) Unable to read data: Error: Calling the 'readAsStringAsync' function has failed → Caused by: 'uri' is not readable