eas-cli icon indicating copy to clipboard operation
eas-cli copied to clipboard

Build for iOS device do not load existing SQLite database from bundled resource

Open severnini opened this issue 3 years ago • 1 comments

Build/Submit details page URL

https://expo.dev/accounts/cnptia-dev/projects/sqlite-app/builds/46681620-686b-4f29-a39b-4c3c86d0dcb3

Summary

When building an app which reads an existing SQLite database and available in assets folder of the app I get an blank screen on iOS Simulator.

I builded a demo app and let it available in GitHub.

I identified this problem in another app with same feature (read existing SQLite database) and no data is returned.

On MacOS:

  1. when I run expo start --localhost and deploy to an Simulator, the app works ok.
  2. when I run expo prebuild --platform ios and deploy to an Simulator, the app works ok.
  3. when I run eas build -p ios --profile preview or eas build -p ios --profile production, and install the app on a device or Simulator, the app do not work.

My network is behind corporate proxy.

Managed or bare?

Managed

Environment

$ npx expo-env-info expo-env-info 1.0.5 environment info: System: OS: Linux 5.15 Linux Mint 21 (Vanessa) Shell: 5.1.16 - /bin/bash Binaries: Node: 16.17.0 - /usr/bin/node Yarn: 1.22.19 - ~/.yarn/bin/yarn npm: 8.18.0 - /usr/bin/npm npmPackages: expo: ~46.0.9 => 46.0.9 react: 18.0.0 => 18.0.0 react-native: 0.69.5 => 0.69.5 npmGlobalPackages: eas-cli: 1.2.0 expo-cli: 6.0.5 Expo Workflow: managed

$ expo doctor Warning: could not reach expo.io. Warning: could not reach expo.fyi. Warning: could not reach expo.dev. Warning: could not reach static.expo.dev. Warning: could not reach exp.host.

We couldn't reach some of our domains, this might cause issues on our website or services. Please check your network configuration and try to access these domains in your browser.

🎉 Didn't find any issues with the project!

Error output

$ EXPO_DEBUG=true expo doctor

  • Finding all copies of expo-modules-autolinking All copies of expo-modules-autolinking satisfy ~0.10.1
  • Finding all copies of @expo/config-plugins All copies of @expo/config-plugins satisfy ^5.0.0
  • Finding all copies of @expo/prebuild-config All copies of @expo/prebuild-config satisfy ^5.0.1
  • Finding all copies of @unimodules/core No dependencies found for @unimodules/core
  • Finding all copies of @unimodules/react-native-adapter No dependencies found for @unimodules/react-native-adapter
  • Finding all copies of react-native-unimodules No dependencies found for react-native-unimodules Warning: could not reach expo.io. Warning: could not reach expo.fyi. Warning: could not reach expo.dev. Warning: could not reach static.expo.dev. Warning: could not reach exp.host.

We couldn't reach some of our domains, this might cause issues on our website or services. Please check your network configuration and try to access these domains in your browser.

🎉 Didn't find any issues with the project!

Reproducible demo or steps to reproduce from a blank project

GitHub

git clone https://github.com/severnini/sqlite-app.git

cd sqlite-app

npm install

eas build -p ios --profile preview

After the build finishes, download the tar.gz generated, extract and install in an iOS Simulator.

severnini avatar Sep 01 '22 15:09 severnini

@severnini we are experiencing same issue

thunderball1 avatar Sep 08 '22 23:09 thunderball1

After some debugging I found a solution for this.

Android

When we call Asset.fromModule(...) the file is not yet downloaded to the device's cache folder.

Example:

Destination: file:///data/user/0/host.exp.exponent/files/ExperienceData/%2540user%252Fsqlite-app/SQLite/data.db

Asset (converted to json) BEFORE download:

{"hash":"hash-removed-for-example","localUri":null,"width":null,"height":null,"downloading":false,"downloaded":false,"_downloadCallbacks":[],"name":"data","type":"db","uri":"http://127.0.0.1:19000/assets/assets/data/data.db?platform=android&hash=hash-removed-for-example?platform=android&dev=true&hot=false"}

Asset (converted to json) AFTER download:

{"hash":"hash-removed-for-example","localUri":"file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540user%252Fsqlite-app/ExponentAsset-hash-removed-for-example.db","width":null,"height":null,"downloading":false,"downloaded":true,"_downloadCallbacks":[],"name":"data","type":"db","uri":"http://127.0.0.1:19000/assets/assets/data/data.db?platform=android&hash=hash-removed-for-example?platform=android&dev=true&hot=false"}

iOS

When we do the same call, the file is already download to the device's cache folder.

Example:

Destination: file:///Users/user/Library/Developer/CoreSimulator/Devices/00674C00-78C3-4658-9FDB-5ACF5AC18382/data/Containers/Data/Application/9FACA5B1-1C86-4D65-8006-6280339A7041/Documents/SQLite/data.db

Asset (converted to json) just AFTER call Asset.fromModule(...):

{"hash":"hash-removed-for-example", "localUri": "file:///Users/user/Library/Developer/CoreSimulator/Devices/00674C00-78C3-4658-9FDB-5ACF5AC18382/data/Containers/Data/Application/9FACA5B1-1C86-4D65-8006-6280339A7041/Library/Application Support/.expo-internal/hash-removed-for-example.db","width":null,"height":null," downloading":false,"downloaded":true,"_downloadCallbacks":[],"name":"data","type":"db","uri":"https://classic-assets.eascdn.net/~assets/hash-removed-for-example"}

The strategy

  1. Call Asset.fromModule(...)

  2. Check if is not downloaded

2.1. then call asset.downloadAsync()

2.2. after download finishes, just copy the .db file to the SQLite document directory

  1. If not yet downloaded

3.1. Check for assset.localUri or if is an asset or file resource

3.1.1. If so, just copy the .db file to the SQLite document directory

3.2. Check if asset.uri is a network resource (http or https)

3.2.1. If so, download the .db file to the SQLite document directory

The code

async function openDatabase() {

  const localFolder = FileSystem.documentDirectory + 'SQLite'
  const dbName = 'data.db'
  const localURI = localFolder + '/' + dbName

  if (!(await FileSystem.getInfoAsync(localFolder)).exists) {
    await FileSystem.makeDirectoryAsync(localFolder)
  }

  let asset = Asset.fromModule(require('./assets/data/data.db'))

  if (!asset.downloaded) {
    await asset.downloadAsync().then(value => {
      asset = value
      console.log('asset downloadAsync - finished')
    })

    let remoteURI = asset.localUri

    await FileSystem.copyAsync({
        from: remoteURI,
        to: localURI
    }).catch(error => {
        console.log('asset copyDatabase - finished with error: ' + error)
    })
  } else {
    // for iOS - Asset is downloaded on call Asset.fromModule(), just copy from cache to local file
    if (asset.localUri || asset.uri.startsWith("asset") || asset.uri.startsWith("file")) {

      let remoteURI = asset.localUri || asset.uri

      await FileSystem.copyAsync({
        from: remoteURI,
        to: localURI
      }).catch(error => {
        console.log("local copyDatabase - finished with error: " + error)
      })
    } else if (asset.uri.startsWith("http") || asset.uri.startsWith("https")) {
      let remoteURI = asset.uri

      await FileSystem.downloadAsync(remoteURI, localURI)
        .catch(error => {
          console.log("local downloadAsync - finished with error: " + error)
        })
    }
  }

  return SQLite.openDatabase(dbName)
}

PS: the code is commited to github example.

Anyway, may be there is a better solution, but for now this is the way I found to have an SQLite database loaded in both Android and iOS app version.

Please feel free to comment and put some insight.

severnini avatar Oct 20 '22 14:10 severnini

@severnini Your solution worked for me! Thank you!

cavemon avatar Mar 16 '23 23:03 cavemon