feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');

View File

@@ -0,0 +1,396 @@
# Changelog
## Unpublished
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
### 💡 Others
## 17.0.1 — 2024-04-23
_This version does not introduce any user-facing changes._
## 17.0.0 — 2024-04-18
### 🐛 Bug fixes
- [Android] remove `CookieHandler` as it's no longer in the module registry and not necessary. ([#28145](https://github.com/expo/expo/pull/28145) by [@alanjhughes](https://github.com/alanjhughes))
### 💡 Others
- [iOS] Add privacy manifest describing required reason API usage. ([#27770](https://github.com/expo/expo/pull/27770) by [@aleqsio](https://github.com/aleqsio))
- drop unused web `name` property. ([#27437](https://github.com/expo/expo/pull/27437) by [@EvanBacon](https://github.com/EvanBacon))
- Removed deprecated backward compatible Gradle settings. ([#28083](https://github.com/expo/expo/pull/28083) by [@kudo](https://github.com/kudo))
## 16.0.8 - 2024-03-07
_This version does not introduce any user-facing changes._
## 16.0.7 - 2024-02-27
### 🐛 Bug fixes
- [iOS] Fix downloadAsync for local files. ([#27187](https://github.com/expo/expo/pull/27187) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- On `iOS`, fix an issue with `copyAsync` where the copy fails if it is a photo library asset. ([#27208](https://github.com/expo/expo/pull/27208) by [@alanjhughes](https://github.com/alanjhughes))
- On `iOS`, resolve the promise manually after copying a PHAsset file. ([#27381](https://github.com/expo/expo/pull/27381) by [@alanjhughes](https://github.com/alanjhughes))
## 16.0.6 - 2024-02-06
### 🐛 Bug fixes
- On `iOS`, fix upload task requests. ([#26880](https://github.com/expo/expo/pull/26880) by [@alanjhughes](https://github.com/alanjhughes))
## 16.0.5 - 2024-01-23
### 🐛 Bug fixes
- On `iOS`, set `httpMethod` on upload requests. ([#26516](https://github.com/expo/expo/pull/26516) by [@alanjhughes](https://github.com/alanjhughes))
## 16.0.4 - 2024-01-18
_This version does not introduce any user-facing changes._
## 16.0.3 - 2024-01-10
### 🎉 New features
- Added support for macOS platform. ([#26253](https://github.com/expo/expo/pull/26253) by [@tsapeta](https://github.com/tsapeta))
## 16.0.2 - 2023-12-19
_This version does not introduce any user-facing changes._
## 16.0.1 — 2023-12-13
_This version does not introduce any user-facing changes._
## 16.0.0 — 2023-12-12
### 🐛 Bug fixes
- On `Android`, handle using files from `SAF` correctly. ([#25389](https://github.com/expo/expo/pull/25389) by [@alanjhughes](https://github.com/alanjhughes))
- Removed legacy `bundledAssets` constant that was used only in standalone apps. ([#25484](https://github.com/expo/expo/pull/25484) by [@tsapeta](https://github.com/tsapeta))
- [iOS] Added missing check for directory permissions in `deleteAsync` method. ([#25704](https://github.com/expo/expo/pull/25704) by [@tsapeta](https://github.com/tsapeta))
## 15.4.5 — 2023-11-20
### 🐛 Bug fixes
- On `Android`, use `addInterceptor` instead of `addNetworkInterceptor` in `downloadResumableStartAsync`. ([#24702](https://github.com/expo/expo/pull/24702) by [@alanhughes](https://github.com/alanjhughes))
## 15.9.0 — 2023-11-14
### 🛠 Breaking changes
- Bumped iOS deployment target to 13.4. ([#25063](https://github.com/expo/expo/pull/25063) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
## 15.8.0 — 2023-10-17
### 🛠 Breaking changes
- Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
### 🐛 Bug fixes
- On `Android`, use `addInterceptor` instead of `addNetworkInterceptor` in `downloadResumableStartAsync`. ([#24702](https://github.com/expo/expo/pull/24702) by [@alanhughes](https://github.com/alanjhughes))
## 15.7.0 — 2023-09-15
### 🎉 New features
- Added support for Apple tvOS. ([#24329](https://github.com/expo/expo/pull/24329) by [@douglowder](https://github.com/douglowder))
### 💡 Others
- Migrated to Swift and Expo Modules API on iOS. ([#23943](https://github.com/expo/expo/pull/23943) by [@tsapeta](https://github.com/tsapeta))
- Throw the correct error when we can't find the permissions modules. ([#24464](https://github.com/expo/expo/pull/24464) by [@alanhughes](https://github.com/alanjhughes))
## 15.6.0 — 2023-09-04
### 🎉 New features
- Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
## 15.4.4 - 2023-08-29
_This version does not introduce any user-facing changes._
## 15.4.3 - 2023-08-09
### 🐛 Bug fixes
- Fix regression in `copyAsync` on Android. ([#23892](https://github.com/expo/expo/pull/23892) by [@brentvatne](https://github.com/brentvatne))
## 15.5.1 — 2023-08-02
_This version does not introduce any user-facing changes._
## 15.5.0 — 2023-07-28
### 💡 Others
- Fork `uuid@3.4.0` and move into `expo-modules-core`. Remove the original dependency. ([#23249](https://github.com/expo/expo/pull/23249) by [@alanhughes](https://github.com/alanjhughes))
## 15.4.2 — 2023-06-28
_This version does not introduce any user-facing changes._
## 15.4.1 — 2023-06-27
### 🐛 Bug fixes
- Fixed hard crash on iOS when calling readDirectoryAsync. ([#23106](https://github.com/expo/expo/pull/23106) by [@aleqsio](https://github.com/aleqsio))
## 15.4.0 — 2023-06-13
### 🎉 New features
- Migrated Android codebase to use Expo modules API. ([#22728](https://github.com/expo/expo/pull/22728) by [@alanhughes](https://github.com/alanjhughes))
### 🐛 Bug fixes
- Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
## 15.3.0 — 2023-05-08
### 🛠 Breaking changes
- Removed the deprecated `UploadProgressData.totalByteSent` field. ([#22277](https://github.com/expo/expo/pull/22277) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 🐛 Bug fixes
- Add UTF-8 URI support on iOS. ([#21196](https://github.com/expo/expo/pull/21196) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 💡 Others
- Android: Switch from deprecated `toLowerCase` to `lowercase` function ([#22225](https://github.com/expo/expo/pull/22225) by [@hbiede](https://github.com/hbiede))
## 15.2.2 — 2023-02-09
_This version does not introduce any user-facing changes._
## 15.2.1 — 2023-02-09
### 🐛 Bug fixes
- Add utf-8 uri support on iOS. ([#21098](https://github.com/expo/expo/pull/21098) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## 15.2.0 — 2023-02-03
### 💡 Others
- Extract nested object definitions to the separate types, which adds: `DeletingOptions`, `InfoOptions`, `RelocatingOptions` and `MakeDirectoryOptions` types. ([#20103](https://github.com/expo/expo/pull/20103) by [@Simek](https://github.com/Simek))
- Simplify the way in which types are exported from the package. ([#20103](https://github.com/expo/expo/pull/20103) by [@Simek](https://github.com/Simek))
- Rename `UploadProgressData` `totalByteSent` field to `totalBytesSent`. ([#20804](https://github.com/expo/expo/pull/20804) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- On Android bump `compileSdkVersion` and `targetSdkVersion` to `33`. ([#20721](https://github.com/expo/expo/pull/20721) by [@lukmccall](https://github.com/lukmccall))
## 15.1.1 — 2022-10-28
_This version does not introduce any user-facing changes._
## 15.1.0 — 2022-10-25
### 🎉 New features
- Added `DirectoriesModule` to expo-file-system on Android as a temporary solution to fix cache directories being incorrect in new Sweet API modules. ([#19205](https://github.com/expo/expo/pull/19205) by [@aleqsio](https://github.com/aleqsio))
## 15.0.0 — 2022-10-06
### 🛠 Breaking changes
- Bumped iOS deployment target to 13.0 and deprecated support for iOS 12. ([#18873](https://github.com/expo/expo/pull/18873) by [@tsapeta](https://github.com/tsapeta))
### 💡 Others
- [plugin] Migrate import from @expo/config-plugins to expo/config-plugins and @expo/config-types to expo/config. ([#18855](https://github.com/expo/expo/pull/18855) by [@brentvatne](https://github.com/brentvatne))
- Drop `@expo/config-plugins` dependency in favor of peer dependency on `expo`. ([#18595](https://github.com/expo/expo/pull/18595) by [@EvanBacon](https://github.com/EvanBacon))
## 14.1.0 — 2022-07-07
_This version does not introduce any user-facing changes._
## 14.0.0 — 2022-04-18
### 🛠 Breaking changes
- Remove okhttp and okio backward compatible workaround and drop react-native 0.64 support. ([#16446](https://github.com/expo/expo/pull/16446) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Fixed failing download on Android when using `createDownloadResumable()`, because of an invalid Range header. ([#15934](https://github.com/expo/expo/pull/15934) by [@johanpoirier](https://github.com/johanpoirier))
- Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
- Fix URL scheme differences between iOS and Android. ([#16352](https://github.com/expo/expo/pull/16352) by [@hbiede](https://github.com/hbiede))
### ⚠️ Notices
- On Android bump `compileSdkVersion` to `31`, `targetSdkVersion` to `31` and `Java` version to `11`. ([#16941](https://github.com/expo/expo/pull/16941) by [@bbarthec](https://github.com/bbarthec))
## 13.2.1 — 2022-01-20
### 🐛 Bug fixes
- Fix build errors on React Native 0.66 caused by `okio` and `okhttp`. ([#15632](https://github.com/expo/expo/pull/15632) by [@kudo](https://github.com/kudo))
## 13.2.0 — 2021-12-22
### 🐛 Bug fixes
- Fixed runtime crash due to `.toUpperCase` not being invoked as a function, it was missing `()`. ([#15615](https://github.com/expo/expo/pull/15615) by [@lukebrandonfarrell](https://github.com/lukebrandonfarrell))
- Fixed `totalByteSent` in upload progress callback incorrectly sending `bytesSent` on iOS. ([#15615](https://github.com/expo/expo/pull/15615) by [@lukebrandonfarrell](https://github.com/lukebrandonfarrell))
- Fixed simulator runtime crash on arm64 devices caused by `CFRelease(NULL)`. ([#15496](https://github.com/expo/expo/pull/15496) by [@daxaxelrod](https://github.com/daxaxelrod))
### 💡 Others
- Updated `@expo/config-plugins` from `4.0.2` to `4.0.14` ([#15621](https://github.com/expo/expo/pull/15621) by [@EvanBacon](https://github.com/EvanBacon))
## 13.1.4 — 2022-02-10
_This version does not introduce any user-facing changes._
## 13.1.3 — 2022-02-01
### 🐛 Bug fixes
- Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
## 13.1.2 — 2022-01-22
### 🐛 Bug fixes
- Fixed runtime crash due to `.toUpperCase` not being invoked as a function, it was missing `()`. ([#15615](https://github.com/expo/expo/pull/15615) by [@lukebrandonfarrell](https://github.com/lukebrandonfarrell))
- Fixed `totalByteSent` in upload progress callback incorrectly sending `bytesSent` on iOS. ([#15615](https://github.com/expo/expo/pull/15615) by [@lukebrandonfarrell](https://github.com/lukebrandonfarrell))
- Fixed simulator runtime crash on arm64 devices caused by `CFRelease(NULL)`. ([#15496](https://github.com/expo/expo/pull/15496) by [@daxaxelrod](https://github.com/daxaxelrod))
## 13.1.1 — 2022-01-20
### 🐛 Bug fixes
- Fix build errors on React Native 0.66 caused by `okio` and `okhttp`. ([#15632](https://github.com/expo/expo/pull/15632) by [@kudo](https://github.com/kudo))
## 13.1.0 — 2021-11-17
### 🐛 Bug fixes
- Fixed `uploadAsync` failing to resolve when using `BINARY_CONTENT`. ([#14764](https://github.com/expo/expo/pull/14764) by [@cruzach](https://github.com/cruzach))
- Fix `okio` library build error for `react-native@0.65` or above. ([#14761](https://github.com/expo/expo/pull/14761) by [@kudo](https://github.com/kudo))
## 13.0.1 — 2021-10-01
_This version does not introduce any user-facing changes._
## 13.0.0 — 2021-09-28
### 🛠 Breaking changes
- Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach))
### 🐛 Bug fixes
- `getFreeDiskStorageAsync` now correctly reports free disk space on iOS. ([#14279](https://github.com/expo/expo/pull/14279) by [mickmaccallum](https://github.com/mickmaccallum))
- Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo))
### 💡 Others
- Updated `@expo/config-plugins` ([#14443](https://github.com/expo/expo/pull/14443) by [@EvanBacon](https://github.com/EvanBacon))
- Rewritten module to Kotlin. ([#14549](https://github.com/expo/expo/pull/14549) by [@mstach60161](https://github.com/mstach60161))
## 12.0.0 — 2021-09-08
### 🛠 Breaking changes
- Added `AndroidManifest.xml` queries for intent handling. ([#13388](https://github.com/expo/expo/pull/13388) by [@EvanBacon](https://github.com/EvanBacon))
### 💡 Others
- Migrated from `@unimodules/core` to `expo-modules-core`. ([#13749](https://github.com/expo/expo/pull/13749) by [@tsapeta](https://github.com/tsapeta))
## 11.1.0 — 2021-06-16
### 🐛 Bug fixes
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman))
- Fixed crash of file system when try to read cache dir file on android. ([#12716](https://github.com/expo/expo/pull/13232) by [@nomi9995](https://github.com/nomi9995))
### 💡 Others
- Migrated from `unimodules-file-system-interface` to `expo-modules-core`.
- Build Android code using Java 8 to fix Android instrumented test build error. ([#12939](https://github.com/expo/expo/pull/12939) by [@kudo](https://github.com/kudo))
- Refactored uuid imports to v7 style. ([#13037](https://github.com/expo/expo/pull/13037) by [@giautm](https://github.com/giautm))
## 11.0.2 — 2021-04-13
_This version does not introduce any user-facing changes._
## 11.0.1 — 2021-04-09
_This version does not introduce any user-facing changes._
## 11.0.0 — 2021-03-10
### 🎉 New features
- Converted plugin to TypeScript. ([#11715](https://github.com/expo/expo/pull/11715) by [@EvanBacon](https://github.com/EvanBacon))
- Updated Android build configuration to target Android 11 (added support for Android SDK 30). ([#11647](https://github.com/expo/expo/pull/11647) by [@bbarthec](https://github.com/bbarthec))
- Added support for Storage Access Framework (**Android only**). ([#12032](https://github.com/expo/expo/pull/12032) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- Fixed copying movies from assets not working on iOS. ([#11749](https://github.com/expo/expo/pull/11749) by [@lukmccall](https://github.com/lukmccall))
- Remove peerDependencies and unimodulePeerDependencies from Expo modules. ([#11980](https://github.com/expo/expo/pull/11980) by [@brentvatne](https://github.com/brentvatne))
## 10.0.0 — 2021-01-15
### ⚠️ Notices
- The package is now shipped with prebuilt binaries on iOS. You can read more about it on [expo.fyi/prebuilt-modules](https://expo.fyi/prebuilt-modules). ([#11224](https://github.com/expo/expo/pull/11224) by [@tsapeta](https://github.com/tsapeta))
### 🛠 Breaking changes
- Dropped support for iOS 10.0 ([#11344](https://github.com/expo/expo/pull/11344) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- Created config plugins ([#11538](https://github.com/expo/expo/pull/11538) by [@EvanBacon](https://github.com/EvanBacon))
## 9.3.0 — 2020-11-17
_This version does not introduce any user-facing changes._
## 9.2.0 — 2020-08-18
### 🐛 Bug fixes
- Added docs about Android permissions and removed old storage permission. ([#9447](https://github.com/expo/expo/pull/9447) by [@bycedric](https://github.com/bycedric))
## 9.1.0 — 2020-07-27
### 🐛 Bug fixes
- Fix background URL session completion handler not being called. ([#8599](https://github.com/expo/expo/pull/8599) by [@lukmccall](https://github.com/lukmccall))
- Fix compilation error on macOS Catalyst ([#9055](https://github.com/expo/expo/pull/9055) by [@andymatuschak](https://github.com/andymatuschak))
- Fixed `uploadAsync` native signature on Android. ([#9076](https://github.com/expo/expo/pull/9076) by [@lukmccall](https://github.com/lukmccall))
- Fixed `uploadAsync` throwing `Double cannot be cast to Integer` on Android. ([#9076](https://github.com/expo/expo/pull/9076) by [@lukmccall](https://github.com/lukmccall))
- Fixed `getInfo` returning incorrect size when provided path points to a folder. ([#9063](https://github.com/expo/expo/pull/9063) by [@lukmccall](https://github.com/lukmccall))
- Fixed `uploadAsync()` returning empty response on iOS. ([#9166](https://github.com/expo/expo/pull/9166) by [@barthap](https://github.com/barthap))
## 9.0.1 — 2020-05-29
_This version does not introduce any user-facing changes._
## 9.0.0 — 2020-05-27
### 🛠 Breaking changes
- `FileSystem.downloadAsync` and `FileSystem.DownloadResumable` work by default when the app is in background too — they won't reject when the application is backgrounded. ([#7380](https://github.com/expo/expo/pull/7380) by [@lukmccall](https://github.com/lukmccall))
- `FileSystem.downloadAsync` and `FileSystem.DownloadResumable` will reject when invalid headers dictionary is provided. These methods accept only `Record<string, string>`. ([#7380](https://github.com/expo/expo/pull/7380) by [@lukmccall](https://github.com/lukmccall))
- `FileSystem.getContentUriAsync` now returns a string. ([#7192](https://github.com/expo/expo/pull/7192) by [@lukmccall](https://github.com/lukmccall))
### 🎉 New features
- Add `FileSystem.uploadAsync` method. ([#7380](https://github.com/expo/expo/pull/7380) by [@lukmccall](https://github.com/lukmccall))
- Add ability to read Android `raw` and `drawable` resources in `FileSystem.getInfoAsync`, `FileSystem.readAsStringAsync`, and `FileSystem.copyAsync`. ([#8104](https://github.com/expo/expo/pull/8104) by [@esamelson](https://github.com/esamelson))

View File

@@ -0,0 +1,42 @@
<p>
<a href="https://docs.expo.dev/versions/latest/sdk/filesystem/">
<img
src="../../.github/resources/expo-file-system.svg"
alt="expo-file-system"
height="64" />
</a>
</p>
Provides access to the local file system on the device.
# API documentation
- [Documentation for the main branch](https://github.com/expo/expo/blob/main/docs/pages/versions/unversioned/sdk/filesystem.mdx)
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/filesystem/)
# Installation in managed Expo projects
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/filesystem/).
# Installation in bare React Native projects
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
## Installation in bare iOS React Native project
No additional set up necessary.
## Installation in bare Android React Native project
This module requires permissions to interact with the filesystem and create resumable downloads. The `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` and `INTERNET` permissions are automatically added.
```xml
<!-- Added permissions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
```
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).

View File

@@ -0,0 +1,28 @@
apply plugin: 'com.android.library'
group = 'host.exp.exponent'
version = '17.0.1'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useDefaultAndroidSdkVersions()
useExpoPublishing()
android {
namespace "expo.modules.filesystem"
defaultConfig {
versionCode 30
versionName "17.0.1"
}
}
dependencies {
api 'commons-codec:commons-codec:1.10'
api 'commons-io:commons-io:1.4'
api 'com.squareup.okhttp3:okhttp:4.9.2'
api 'com.squareup.okhttp3:okhttp-urlconnection:4.9.2'
api 'com.squareup.okio:okio:2.9.0'
api "androidx.legacy:legacy-support-v4:1.0.0"
}

View File

@@ -0,0 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application>
<provider tools:replace="android:authorities" android:name=".FileSystemFileProvider" android:authorities="${applicationId}.FileSystemFileProvider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_system_provider_paths" />
</provider>
</application>
<queries>
<!-- Query open documents -->
<intent>
<action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,26 @@
package expo.modules.filesystem
import android.content.Context
import expo.modules.core.interfaces.InternalModule
import expo.modules.interfaces.filesystem.AppDirectoriesModuleInterface
import java.io.File
/*
New Sweet API modules don't have an easy way to access scoped context. We can't initialize them with scoped context as they need a ReactApplicationContext instead.
We can't make ScopedContext inherit from ReactApplicationContext as that would require moving ScopedContext to versioned and a large refactor.
This module is a stopgap solution to provide modules with a way to access ScopedContext directories using the filesystem module, only for our internal modules.
*/
// The class needs to be 'open', because it's inherited in expoview
open class AppDirectoriesModule(private val context: Context) : AppDirectoriesModuleInterface, InternalModule {
override fun getExportedInterfaces(): List<Class<*>> =
listOf(AppDirectoriesModuleInterface::class.java)
override val cacheDirectory: File
get() = context.cacheDir
override val persistentFilesDirectory: File
get() = context.filesDir
}

View File

@@ -0,0 +1,52 @@
package expo.modules.filesystem
import okhttp3.RequestBody
import okio.Buffer
import okio.BufferedSink
import okio.ForwardingSink
import okio.Sink
import okio.buffer
import java.io.IOException
@FunctionalInterface
fun interface RequestBodyDecorator {
fun decorate(requestBody: RequestBody): RequestBody
}
@FunctionalInterface
interface CountingRequestListener {
fun onProgress(bytesWritten: Long, contentLength: Long)
}
private class CountingSink(
sink: Sink,
private val requestBody: RequestBody,
private val progressListener: CountingRequestListener
) : ForwardingSink(sink) {
private var bytesWritten = 0L
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
progressListener.onProgress(bytesWritten, requestBody.contentLength())
}
}
class CountingRequestBody(
private val requestBody: RequestBody,
private val progressListener: CountingRequestListener
) : RequestBody() {
override fun contentType() = requestBody.contentType()
@Throws(IOException::class)
override fun contentLength() = requestBody.contentLength()
override fun writeTo(sink: BufferedSink) {
val countingSink = CountingSink(sink, this, progressListener)
val bufferedSink = countingSink.buffer()
requestBody.writeTo(bufferedSink)
bufferedSink.flush()
}
}

View File

@@ -0,0 +1,48 @@
package expo.modules.filesystem
import android.content.Context
import expo.modules.interfaces.filesystem.FilePermissionModuleInterface
import expo.modules.core.interfaces.InternalModule
import expo.modules.interfaces.filesystem.Permission
import java.io.File
import java.io.IOException
import java.util.*
// The class needs to be 'open', because it's inherited in expoview
open class FilePermissionModule : FilePermissionModuleInterface, InternalModule {
override fun getExportedInterfaces(): List<Class<*>> =
listOf(FilePermissionModuleInterface::class.java)
override fun getPathPermissions(context: Context, path: String): EnumSet<Permission> =
getInternalPathPermissions(path, context) ?: getExternalPathPermissions(path)
private fun getInternalPathPermissions(path: String, context: Context): EnumSet<Permission>? {
return try {
val canonicalPath = File(path).canonicalPath
getInternalPaths(context)
.firstOrNull { dir -> canonicalPath.startsWith("$dir/") || dir == canonicalPath }
?.let { EnumSet.of(Permission.READ, Permission.WRITE) }
} catch (e: IOException) {
EnumSet.noneOf(Permission::class.java)
}
}
protected open fun getExternalPathPermissions(path: String): EnumSet<Permission> {
val file = File(path)
return EnumSet.noneOf(Permission::class.java).apply {
if (file.canRead()) {
add(Permission.READ)
}
if (file.canWrite()) {
add(Permission.WRITE)
}
}
}
@Throws(IOException::class)
private fun getInternalPaths(context: Context): List<String> =
listOf(
context.filesDir.canonicalPath,
context.cacheDir.canonicalPath
)
}

View File

@@ -0,0 +1,48 @@
package expo.modules.filesystem
import android.net.Uri
import expo.modules.kotlin.exception.CodedException
internal class FileSystemOkHttpNullException :
CodedException("okHttpClient is null")
internal class FileSystemCannotReadDirectoryException(uri: Uri?) :
CodedException("Uri '$uri' doesn't exist or isn't a directory")
internal class FileSystemCannotCreateDirectoryException(uri: Uri?) :
CodedException(
uri?.let {
"Directory '$it' could not be created or already exists"
} ?: "Unknown error"
)
internal class FileSystemUnreadableDirectoryException(uri: String) :
CodedException("No readable files with the uri '$uri'. Please use other uri")
internal class FileSystemCannotCreateFileException(uri: Uri?) :
CodedException(
uri?.let {
"Provided uri '$it' is not pointing to a directory"
} ?: "Unknown error"
)
internal class FileSystemFileNotFoundException(uri: Uri?) :
CodedException("File '$uri' could not be deleted because it could not be found")
internal class FileSystemPendingPermissionsRequestException :
CodedException("You have an unfinished permission request")
internal class FileSystemCannotMoveFileException(fromUri: Uri, toUri: Uri) :
CodedException("File '$fromUri' could not be moved to '$toUri'")
internal class FileSystemUnsupportedSchemeException :
CodedException("Can't read Storage Access Framework directory, use StorageAccessFramework.readDirectoryAsync() instead")
internal class FileSystemCannotFindTaskException :
CodedException("Cannot find task")
internal class FileSystemCopyFailedException(uri: Uri?) :
CodedException("File '$uri' could not be copied because it could not be found")
internal class CookieHandlerNotFoundException :
CodedException("Failed to find CookieHandler")

View File

@@ -0,0 +1,5 @@
package expo.modules.filesystem
import androidx.core.content.FileProvider
class FileSystemFileProvider : FileProvider()

View File

@@ -0,0 +1,10 @@
package expo.modules.filesystem
import android.content.Context
import expo.modules.core.BasePackage
import expo.modules.core.interfaces.InternalModule
class FileSystemPackage : BasePackage() {
override fun createInternalModules(context: Context): List<InternalModule> =
listOf(FilePermissionModule(), AppDirectoriesModule(context))
}

View File

@@ -0,0 +1,92 @@
package expo.modules.filesystem
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import expo.modules.kotlin.types.Enumerable
data class InfoOptions(
@Field
val md5: Boolean?,
@Field
val size: Boolean?
) : Record
data class DeletingOptions(
@Field
val idempotent: Boolean = false
) : Record
data class ReadingOptions(
@Field
val encoding: EncodingType = EncodingType.UTF8,
@Field
val position: Int?,
@Field
val length: Int?
) : Record
enum class EncodingType(val value: String) : Enumerable {
UTF8("utf8"),
BASE64("base64")
}
enum class SessionType(val value: Int) : Enumerable {
BACKGROUND(0),
FOREGROUND(1)
}
enum class FileSystemUploadType(val value: Int) : Enumerable {
BINARY_CONTENT(0),
MULTIPART(1)
}
data class MakeDirectoryOptions(
@Field
val intermediates: Boolean = false
) : Record
data class RelocatingOptions(
@Field
val from: String,
@Field
val to: String
) : Record
data class DownloadOptions(
@Field
val md5: Boolean = false,
@Field
val cache: Boolean?,
@Field
val headers: Map<String, String>?,
@Field
val sessionType: SessionType = SessionType.BACKGROUND
) : Record
data class WritingOptions(
@Field
val encoding: EncodingType = EncodingType.UTF8
) : Record
data class FileSystemUploadOptions(
@Field
val headers: Map<String, String>?,
@Field
val httpMethod: HttpMethod = HttpMethod.POST,
@Field
val sessionType: SessionType = SessionType.BACKGROUND,
@Field
val uploadType: FileSystemUploadType,
@Field
val fieldName: String?,
@Field
val mimeType: String?,
@Field
val parameters: Map<String, String>?
) : Record
enum class HttpMethod(val value: String) : Enumerable {
POST("POST"),
PUT("PUT"),
PATCH("PATCH")
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="expo_files" path="." />
<cache-path name="cached_expo_files" path="." />
</paths>

View File

@@ -0,0 +1 @@
module.exports = require('./plugin/build/withFileSystem');

View File

@@ -0,0 +1,4 @@
import { ExponentFileSystemModule } from './types';
declare const _default: ExponentFileSystemModule;
export default _default;
//# sourceMappingURL=ExponentFileSystem.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentFileSystem.d.ts","sourceRoot":"","sources":["../src/ExponentFileSystem.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;;AAEnD,wBACyB"}

View File

@@ -0,0 +1,5 @@
import { requireOptionalNativeModule } from 'expo-modules-core';
import ExponentFileSystemShim from './ExponentFileSystemShim';
export default requireOptionalNativeModule('ExponentFileSystem') ??
ExponentFileSystemShim;
//# sourceMappingURL=ExponentFileSystem.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentFileSystem.js","sourceRoot":"","sources":["../src/ExponentFileSystem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAG9D,eAAe,2BAA2B,CAA2B,oBAAoB,CAAC;IACxF,sBAAsB,CAAC","sourcesContent":["import { requireOptionalNativeModule } from 'expo-modules-core';\n\nimport ExponentFileSystemShim from './ExponentFileSystemShim';\nimport { ExponentFileSystemModule } from './types';\n\nexport default requireOptionalNativeModule<ExponentFileSystemModule>('ExponentFileSystem') ??\n ExponentFileSystemShim;\n"]}

View File

@@ -0,0 +1,3 @@
import ExponentFileSystemShim from './ExponentFileSystemShim';
export default ExponentFileSystemShim;
//# sourceMappingURL=ExponentFileSystem.web.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentFileSystem.web.d.ts","sourceRoot":"","sources":["../src/ExponentFileSystem.web.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAC9D,eAAe,sBAAsB,CAAC"}

View File

@@ -0,0 +1,3 @@
import ExponentFileSystemShim from './ExponentFileSystemShim';
export default ExponentFileSystemShim;
//# sourceMappingURL=ExponentFileSystem.web.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentFileSystem.web.js","sourceRoot":"","sources":["../src/ExponentFileSystem.web.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAC9D,eAAe,sBAAsB,CAAC","sourcesContent":["import ExponentFileSystemShim from './ExponentFileSystemShim';\nexport default ExponentFileSystemShim;\n"]}

View File

@@ -0,0 +1,4 @@
import { ExponentFileSystemModule } from './types';
declare const platformModule: ExponentFileSystemModule;
export default platformModule;
//# sourceMappingURL=ExponentFileSystemShim.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentFileSystemShim.d.ts","sourceRoot":"","sources":["../src/ExponentFileSystemShim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAEnD,QAAA,MAAM,cAAc,EAAE,wBAYrB,CAAC;AAEF,eAAe,cAAc,CAAC"}

View File

@@ -0,0 +1,15 @@
const platformModule = {
get documentDirectory() {
return null;
},
get cacheDirectory() {
return null;
},
get bundleDirectory() {
return null;
},
addListener(eventName) { },
removeListeners(count) { },
};
export default platformModule;
//# sourceMappingURL=ExponentFileSystemShim.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentFileSystemShim.js","sourceRoot":"","sources":["../src/ExponentFileSystemShim.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAA6B;IAC/C,IAAI,iBAAiB;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,WAAW,CAAC,SAAiB,IAAS,CAAC;IACvC,eAAe,CAAC,KAAa,IAAS,CAAC;CACxC,CAAC;AAEF,eAAe,cAAc,CAAC","sourcesContent":["import { ExponentFileSystemModule } from './types';\n\nconst platformModule: ExponentFileSystemModule = {\n get documentDirectory(): string | null {\n return null;\n },\n get cacheDirectory(): string | null {\n return null;\n },\n get bundleDirectory(): string | null {\n return null;\n },\n addListener(eventName: string): void {},\n removeListeners(count: number): void {},\n};\n\nexport default platformModule;\n"]}

View File

@@ -0,0 +1,364 @@
import { DownloadOptions, DownloadPauseState, FileSystemNetworkTaskProgressCallback, DownloadProgressData, UploadProgressData, FileInfo, FileSystemDownloadResult, FileSystemRequestDirectoryPermissionsResult, FileSystemUploadOptions, FileSystemUploadResult, ReadingOptions, WritingOptions, DeletingOptions, InfoOptions, RelocatingOptions, MakeDirectoryOptions } from './FileSystem.types';
/**
* `file://` URI pointing to the directory where user documents for this app will be stored.
* Files stored here will remain until explicitly deleted by the app. Ends with a trailing `/`.
* Example uses are for files the user saves that they expect to see again.
*/
export declare const documentDirectory: string | null;
/**
* `file://` URI pointing to the directory where temporary files used by this app will be stored.
* Files stored here may be automatically deleted by the system when low on storage.
* Example uses are for downloaded or generated files that the app just needs for one-time usage.
*/
export declare const cacheDirectory: string | null;
/**
* URI to the directory where assets bundled with the application are stored.
*/
export declare const bundleDirectory: string | null;
/**
* Get metadata information about a file, directory or external content/asset.
* @param fileUri URI to the file or directory. See [supported URI schemes](#supported-uri-schemes).
* @param options A map of options represented by [`InfoOptions`](#infooptions) type.
* @return A Promise that resolves to a `FileInfo` object. If no item exists at this URI,
* the returned Promise resolves to `FileInfo` object in form of `{ exists: false, isDirectory: false }`.
*/
export declare function getInfoAsync(fileUri: string, options?: InfoOptions): Promise<FileInfo>;
/**
* Read the entire contents of a file as a string. Binary will be returned in raw format, you will need to append `data:image/png;base64,` to use it as Base64.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* @param options A map of read options represented by [`ReadingOptions`](#readingoptions) type.
* @return A Promise that resolves to a string containing the entire contents of the file.
*/
export declare function readAsStringAsync(fileUri: string, options?: ReadingOptions): Promise<string>;
/**
* Takes a `file://` URI and converts it into content URI (`content://`) so that it can be accessed by other applications outside of Expo.
* @param fileUri The local URI of the file. If there is no file at this URI, an exception will be thrown.
* @example
* ```js
* FileSystem.getContentUriAsync(uri).then(cUri => {
* console.log(cUri);
* IntentLauncher.startActivityAsync('android.intent.action.VIEW', {
* data: cUri,
* flags: 1,
* });
* });
* ```
* @return Returns a Promise that resolves to a `string` containing a `content://` URI pointing to the file.
* The URI is the same as the `fileUri` input parameter but in a different format.
* @platform android
*/
export declare function getContentUriAsync(fileUri: string): Promise<string>;
/**
* Write the entire contents of a file as a string.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* > Note: when you're using SAF URI the file needs to exist. You can't create a new file.
* @param contents The string to replace the contents of the file with.
* @param options A map of write options represented by [`WritingOptions`](#writingoptions) type.
*/
export declare function writeAsStringAsync(fileUri: string, contents: string, options?: WritingOptions): Promise<void>;
/**
* Delete a file or directory. If the URI points to a directory, the directory and all its contents are recursively deleted.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* @param options A map of write options represented by [`DeletingOptions`](#deletingoptions) type.
*/
export declare function deleteAsync(fileUri: string, options?: DeletingOptions): Promise<void>;
export declare function deleteLegacyDocumentDirectoryAndroid(): Promise<void>;
/**
* Move a file or directory to a new location.
* @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
*/
export declare function moveAsync(options: RelocatingOptions): Promise<void>;
/**
* Create a copy of a file or directory. Directories are recursively copied with all of their contents.
* It can be also used to copy content shared by other apps to local filesystem.
* @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
*/
export declare function copyAsync(options: RelocatingOptions): Promise<void>;
/**
* Create a new empty directory.
* @param fileUri `file://` URI to the new directory to create.
* @param options A map of create directory options represented by [`MakeDirectoryOptions`](#makedirectoryoptions) type.
*/
export declare function makeDirectoryAsync(fileUri: string, options?: MakeDirectoryOptions): Promise<void>;
/**
* Enumerate the contents of a directory.
* @param fileUri `file://` URI to the directory.
* @return A Promise that resolves to an array of strings, each containing the name of a file or directory contained in the directory at `fileUri`.
*/
export declare function readDirectoryAsync(fileUri: string): Promise<string[]>;
/**
* Gets the available internal disk storage size, in bytes. This returns the free space on the data partition that hosts all of the internal storage for all apps on the device.
* @return Returns a Promise that resolves to the number of bytes available on the internal disk, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* if the capacity is greater than 2^53^ - 1 bytes.
*/
export declare function getFreeDiskStorageAsync(): Promise<number>;
/**
* Gets total internal disk storage size, in bytes. This is the total capacity of the data partition that hosts all the internal storage for all apps on the device.
* @return Returns a Promise that resolves to a number that specifies the total internal disk storage capacity in bytes, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* if the capacity is greater than 2^53^ - 1 bytes.
*/
export declare function getTotalDiskCapacityAsync(): Promise<number>;
/**
* Download the contents at a remote URI to a file in the app's file system. The directory for a local file uri must exist prior to calling this function.
* @param uri The remote URI to download from.
* @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
* If there is a file at this URI, its contents are replaced. The directory for the file must exist.
* @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
* @example
* ```js
* FileSystem.downloadAsync(
* 'http://techslides.com/demos/sample-videos/small.mp4',
* FileSystem.documentDirectory + 'small.mp4'
* )
* .then(({ uri }) => {
* console.log('Finished downloading to ', uri);
* })
* .catch(error => {
* console.error(error);
* });
* ```
* @return Returns a Promise that resolves to a `FileSystemDownloadResult` object.
*/
export declare function downloadAsync(uri: string, fileUri: string, options?: DownloadOptions): Promise<FileSystemDownloadResult>;
/**
* Upload the contents of the file pointed by `fileUri` to the remote url.
* @param url The remote URL, where the file will be sent.
* @param fileUri The local URI of the file to send. The file must exist.
* @param options A map of download options represented by [`FileSystemUploadOptions`](#filesystemuploadoptions) type.
* @example
* **Client**
*
* ```js
* import * as FileSystem from 'expo-file-system';
*
* try {
* const response = await FileSystem.uploadAsync(`http://192.168.0.1:1234/binary-upload`, fileUri, {
* fieldName: 'file',
* httpMethod: 'PATCH',
* uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
* });
* console.log(JSON.stringify(response, null, 4));
* } catch (error) {
* console.log(error);
* }
* ```
*
* **Server**
*
* Please refer to the "[Server: Handling multipart requests](#server-handling-multipart-requests)" example - there is code for a simple Node.js server.
* @return Returns a Promise that resolves to `FileSystemUploadResult` object.
*/
export declare function uploadAsync(url: string, fileUri: string, options?: FileSystemUploadOptions): Promise<FileSystemUploadResult>;
/**
* Create a `DownloadResumable` object which can start, pause, and resume a download of contents at a remote URI to a file in the app's file system.
* > Note: You need to call `downloadAsync()`, on a `DownloadResumable` instance to initiate the download.
* The `DownloadResumable` object has a callback that provides download progress updates.
* Downloads can be resumed across app restarts by using `AsyncStorage` to store the `DownloadResumable.savable()` object for later retrieval.
* The `savable` object contains the arguments required to initialize a new `DownloadResumable` object to resume the download after an app restart.
* The directory for a local file uri must exist prior to calling this function.
* @param uri The remote URI to download from.
* @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
* If there is a file at this URI, its contents are replaced. The directory for the file must exist.
* @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
* @param callback This function is called on each data write to update the download progress.
* > **Note**: When the app has been moved to the background, this callback won't be fired until it's moved to the foreground.
* @param resumeData The string which allows the api to resume a paused download. This is set on the `DownloadResumable` object automatically when a download is paused.
* When initializing a new `DownloadResumable` this should be `null`.
*/
export declare function createDownloadResumable(uri: string, fileUri: string, options?: DownloadOptions, callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>, resumeData?: string): DownloadResumable;
export declare function createUploadTask(url: string, fileUri: string, options?: FileSystemUploadOptions, callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>): UploadTask;
export declare abstract class FileSystemCancellableNetworkTask<T extends DownloadProgressData | UploadProgressData> {
private _uuid;
protected taskWasCanceled: boolean;
private emitter;
private subscription?;
cancelAsync(): Promise<void>;
protected isTaskCancelled(): boolean;
protected get uuid(): string;
protected abstract getEventName(): string;
protected abstract getCallback(): FileSystemNetworkTaskProgressCallback<T> | undefined;
protected addSubscription(): void;
protected removeSubscription(): void;
}
export declare class UploadTask extends FileSystemCancellableNetworkTask<UploadProgressData> {
private url;
private fileUri;
private callback?;
private options;
constructor(url: string, fileUri: string, options?: FileSystemUploadOptions, callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData> | undefined);
protected getEventName(): string;
protected getCallback(): FileSystemNetworkTaskProgressCallback<UploadProgressData> | undefined;
uploadAsync(): Promise<FileSystemUploadResult | undefined>;
}
export declare class DownloadResumable extends FileSystemCancellableNetworkTask<DownloadProgressData> {
private url;
private _fileUri;
private options;
private callback?;
private resumeData?;
constructor(url: string, _fileUri: string, options?: DownloadOptions, callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData> | undefined, resumeData?: string | undefined);
get fileUri(): string;
protected getEventName(): string;
protected getCallback(): FileSystemNetworkTaskProgressCallback<DownloadProgressData> | undefined;
/**
* Download the contents at a remote URI to a file in the app's file system.
* @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
*/
downloadAsync(): Promise<FileSystemDownloadResult | undefined>;
/**
* Pause the current download operation. `resumeData` is added to the `DownloadResumable` object after a successful pause operation.
* Returns an object that can be saved with `AsyncStorage` for future retrieval (the same object that is returned from calling `FileSystem.DownloadResumable.savable()`).
* @return Returns a Promise that resolves to `DownloadPauseState` object.
*/
pauseAsync(): Promise<DownloadPauseState>;
/**
* Resume a paused download operation.
* @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
*/
resumeAsync(): Promise<FileSystemDownloadResult | undefined>;
/**
* Method to get the object which can be saved with `AsyncStorage` for future retrieval.
* @returns Returns object in shape of `DownloadPauseState` type.
*/
savable(): DownloadPauseState;
}
/**
* The `StorageAccessFramework` is a namespace inside of the `expo-file-system` module, which encapsulates all functions which can be used with [SAF URIs](#saf-uri).
* You can read more about SAF in the [Android documentation](https://developer.android.com/guide/topics/providers/document-provider).
*
* @example
* # Basic Usage
*
* ```ts
* import { StorageAccessFramework } from 'expo-file-system';
*
* // Requests permissions for external directory
* const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
*
* if (permissions.granted) {
* // Gets SAF URI from response
* const uri = permissions.directoryUri;
*
* // Gets all files inside of selected directory
* const files = await StorageAccessFramework.readDirectoryAsync(uri);
* alert(`Files inside ${uri}:\n\n${JSON.stringify(files)}`);
* }
* ```
*
* # Migrating an album
*
* ```ts
* import * as MediaLibrary from 'expo-media-library';
* import * as FileSystem from 'expo-file-system';
* const { StorageAccessFramework } = FileSystem;
*
* async function migrateAlbum(albumName: string) {
* // Gets SAF URI to the album
* const albumUri = StorageAccessFramework.getUriForDirectoryInRoot(albumName);
*
* // Requests permissions
* const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync(albumUri);
* if (!permissions.granted) {
* return;
* }
*
* const permittedUri = permissions.directoryUri;
* // Checks if users selected the correct folder
* if (!permittedUri.includes(albumName)) {
* return;
* }
*
* const mediaLibraryPermissions = await MediaLibrary.requestPermissionsAsync();
* if (!mediaLibraryPermissions.granted) {
* return;
* }
*
* // Moves files from external storage to internal storage
* await StorageAccessFramework.moveAsync({
* from: permittedUri,
* to: FileSystem.documentDirectory!,
* });
*
* const outputDir = FileSystem.documentDirectory! + albumName;
* const migratedFiles = await FileSystem.readDirectoryAsync(outputDir);
*
* // Creates assets from local files
* const [newAlbumCreator, ...assets] = await Promise.all(
* migratedFiles.map<Promise<MediaLibrary.Asset>>(
* async fileName => await MediaLibrary.createAssetAsync(outputDir + '/' + fileName)
* )
* );
*
* // Album was empty
* if (!newAlbumCreator) {
* return;
* }
*
* // Creates a new album in the scoped directory
* const newAlbum = await MediaLibrary.createAlbumAsync(albumName, newAlbumCreator, false);
* if (assets.length) {
* await MediaLibrary.addAssetsToAlbumAsync(assets, newAlbum, false);
* }
* }
* ```
* @platform Android
*/
export declare namespace StorageAccessFramework {
/**
* Gets a [SAF URI](#saf-uri) pointing to a folder in the Android root directory. You can use this function to get URI for
* `StorageAccessFramework.requestDirectoryPermissionsAsync()` when you trying to migrate an album. In that case, the name of the album is the folder name.
* @param folderName The name of the folder which is located in the Android root directory.
* @return Returns a [SAF URI](#saf-uri) to a folder.
*/
function getUriForDirectoryInRoot(folderName: string): string;
/**
* Allows users to select a specific directory, granting your app access to all of the files and sub-directories within that directory.
* @param initialFileUrl The [SAF URI](#saf-uri) of the directory that the file picker should display when it first loads.
* If URI is incorrect or points to a non-existing folder, it's ignored.
* @platform android 11+
* @return Returns a Promise that resolves to `FileSystemRequestDirectoryPermissionsResult` object.
*/
function requestDirectoryPermissionsAsync(initialFileUrl?: string | null): Promise<FileSystemRequestDirectoryPermissionsResult>;
/**
* Enumerate the contents of a directory.
* @param dirUri [SAF](#saf-uri) URI to the directory.
* @return A Promise that resolves to an array of strings, each containing the full [SAF URI](#saf-uri) of a file or directory contained in the directory at `fileUri`.
*/
function readDirectoryAsync(dirUri: string): Promise<string[]>;
/**
* Creates a new empty directory.
* @param parentUri The [SAF](#saf-uri) URI to the parent directory.
* @param dirName The name of new directory.
* @return A Promise that resolves to a [SAF URI](#saf-uri) to the created directory.
*/
function makeDirectoryAsync(parentUri: string, dirName: string): Promise<string>;
/**
* Creates a new empty file.
* @param parentUri The [SAF](#saf-uri) URI to the parent directory.
* @param fileName The name of new file **without the extension**.
* @param mimeType The MIME type of new file.
* @return A Promise that resolves to a [SAF URI](#saf-uri) to the created file.
*/
function createFileAsync(parentUri: string, fileName: string, mimeType: string): Promise<string>;
/**
* Alias for [`writeAsStringAsync`](#filesystemwriteasstringasyncfileuri-contents-options) method.
*/
const writeAsStringAsync: typeof import("./FileSystem").writeAsStringAsync;
/**
* Alias for [`readAsStringAsync`](#filesystemreadasstringasyncfileuri-options) method.
*/
const readAsStringAsync: typeof import("./FileSystem").readAsStringAsync;
/**
* Alias for [`deleteAsync`](#filesystemdeleteasyncfileuri-options) method.
*/
const deleteAsync: typeof import("./FileSystem").deleteAsync;
/**
* Alias for [`moveAsync`](#filesystemmoveasyncoptions) method.
*/
const moveAsync: typeof import("./FileSystem").moveAsync;
/**
* Alias for [`copyAsync`](#filesystemcopyasyncoptions) method.
*/
const copyAsync: typeof import("./FileSystem").copyAsync;
}
//# sourceMappingURL=FileSystem.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FileSystem.d.ts","sourceRoot":"","sources":["../src/FileSystem.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qCAAqC,EACrC,oBAAoB,EACpB,kBAAkB,EAClB,QAAQ,EAER,wBAAwB,EACxB,2CAA2C,EAE3C,uBAAuB,EACvB,sBAAsB,EAGtB,cAAc,EACd,cAAc,EACd,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,oBAAoB,CAAC;AAiB5B;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,eAA6D,CAAC;AAE5F;;;;GAIG;AACH,eAAO,MAAM,cAAc,eAA0D,CAAC;AAEtF;;GAEG;AACH,eAAO,MAAM,eAAe,eAA2D,CAAC;AAExF;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAKhG;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASzE;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/F;AAED,wBAAsB,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1E;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzE;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzE;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAK3E;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC,CAK/D;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,MAAM,CAAC,CAKjE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,wBAAwB,CAAC,CASnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,sBAAsB,CAAC,CAWjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,qCAAqC,CAAC,oBAAoB,CAAC,EACtE,UAAU,CAAC,EAAE,MAAM,GAClB,iBAAiB,CAEnB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,uBAAuB,EACjC,QAAQ,CAAC,EAAE,qCAAqC,CAAC,kBAAkB,CAAC,GACnE,UAAU,CAEZ;AAED,8BAAsB,gCAAgC,CACpD,CAAC,SAAS,oBAAoB,GAAG,kBAAkB;IAEnD,OAAO,CAAC,KAAK,CAAa;IAC1B,SAAS,CAAC,eAAe,UAAS;IAClC,OAAO,CAAC,OAAO,CAAwC;IACvD,OAAO,CAAC,YAAY,CAAC,CAAsB;IAG9B,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAUzC,SAAS,CAAC,eAAe,IAAI,OAAO;IASpC,SAAS,KAAK,IAAI,IAAI,MAAM,CAE3B;IAED,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,MAAM;IAEzC,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,qCAAqC,CAAC,CAAC,CAAC,GAAG,SAAS;IAEtF,SAAS,CAAC,eAAe;IAezB,SAAS,CAAC,kBAAkB;CAO7B;AAED,qBAAa,UAAW,SAAQ,gCAAgC,CAAC,kBAAkB,CAAC;IAIhF,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,OAAO;IAEf,OAAO,CAAC,QAAQ,CAAC;IANnB,OAAO,CAAC,OAAO,CAA0B;gBAG/B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE,uBAAuB,EACzB,QAAQ,CAAC,uEAA2D;IAe9E,SAAS,CAAC,YAAY,IAAI,MAAM;IAGhC,SAAS,CAAC,WAAW,IAAI,qCAAqC,CAAC,kBAAkB,CAAC,GAAG,SAAS;IAKjF,WAAW,IAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;CAoBxE;AAED,qBAAa,iBAAkB,SAAQ,gCAAgC,CAAC,oBAAoB,CAAC;IAEzF,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,QAAQ,CAAC;IACjB,OAAO,CAAC,UAAU,CAAC;gBAJX,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,EAC7B,QAAQ,CAAC,yEAA6D,EACtE,UAAU,CAAC,oBAAQ;IAK7B,IAAW,OAAO,IAAI,MAAM,CAE3B;IAED,SAAS,CAAC,YAAY,IAAI,MAAM;IAIhC,SAAS,CAAC,WAAW,IAAI,qCAAqC,CAAC,oBAAoB,CAAC,GAAG,SAAS;IAIhG;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAmBpE;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAuB/C;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAmBlE;;;OAGG;IACH,OAAO,IAAI,kBAAkB;CAQ9B;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,yBAAiB,sBAAsB,CAAC;IACtC;;;;;OAKG;IACH,SAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,UAE1D;IAED;;;;;;OAMG;IACH,SAAsB,gCAAgC,CACpD,cAAc,GAAE,MAAM,GAAG,IAAW,GACnC,OAAO,CAAC,2CAA2C,CAAC,CAStD;IAED;;;;OAIG;IACH,SAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQ1E;IAED;;;;;OAKG;IACH,SAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5F;IAED;;;;;;OAMG;IACH,SAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAKjB;IAED;;OAEG;IACI,MAAM,kBAAkB,kDAAyB,CAAC;IACzD;;OAEG;IACI,MAAM,iBAAiB,iDAAwB,CAAC;IACvD;;OAEG;IACI,MAAM,WAAW,2CAAkB,CAAC;IAC3C;;OAEG;IACI,MAAM,SAAS,yCAAgB,CAAC;IACvC;;OAEG;IACI,MAAM,SAAS,yCAAgB,CAAC;CACxC"}

View File

@@ -0,0 +1,618 @@
import { EventEmitter, UnavailabilityError, uuid } from 'expo-modules-core';
import { Platform } from 'react-native';
import ExponentFileSystem from './ExponentFileSystem';
import { FileSystemSessionType, FileSystemUploadType, } from './FileSystem.types';
if (!ExponentFileSystem) {
console.warn("No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?");
}
// Prevent webpack from pruning this.
const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line
function normalizeEndingSlash(p) {
if (p != null) {
return p.replace(/\/*$/, '') + '/';
}
return null;
}
/**
* `file://` URI pointing to the directory where user documents for this app will be stored.
* Files stored here will remain until explicitly deleted by the app. Ends with a trailing `/`.
* Example uses are for files the user saves that they expect to see again.
*/
export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory);
/**
* `file://` URI pointing to the directory where temporary files used by this app will be stored.
* Files stored here may be automatically deleted by the system when low on storage.
* Example uses are for downloaded or generated files that the app just needs for one-time usage.
*/
export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory);
/**
* URI to the directory where assets bundled with the application are stored.
*/
export const bundleDirectory = normalizeEndingSlash(ExponentFileSystem.bundleDirectory);
/**
* Get metadata information about a file, directory or external content/asset.
* @param fileUri URI to the file or directory. See [supported URI schemes](#supported-uri-schemes).
* @param options A map of options represented by [`InfoOptions`](#infooptions) type.
* @return A Promise that resolves to a `FileInfo` object. If no item exists at this URI,
* the returned Promise resolves to `FileInfo` object in form of `{ exists: false, isDirectory: false }`.
*/
export async function getInfoAsync(fileUri, options = {}) {
if (!ExponentFileSystem.getInfoAsync) {
throw new UnavailabilityError('expo-file-system', 'getInfoAsync');
}
return await ExponentFileSystem.getInfoAsync(fileUri, options);
}
/**
* Read the entire contents of a file as a string. Binary will be returned in raw format, you will need to append `data:image/png;base64,` to use it as Base64.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* @param options A map of read options represented by [`ReadingOptions`](#readingoptions) type.
* @return A Promise that resolves to a string containing the entire contents of the file.
*/
export async function readAsStringAsync(fileUri, options = {}) {
if (!ExponentFileSystem.readAsStringAsync) {
throw new UnavailabilityError('expo-file-system', 'readAsStringAsync');
}
return await ExponentFileSystem.readAsStringAsync(fileUri, options);
}
/**
* Takes a `file://` URI and converts it into content URI (`content://`) so that it can be accessed by other applications outside of Expo.
* @param fileUri The local URI of the file. If there is no file at this URI, an exception will be thrown.
* @example
* ```js
* FileSystem.getContentUriAsync(uri).then(cUri => {
* console.log(cUri);
* IntentLauncher.startActivityAsync('android.intent.action.VIEW', {
* data: cUri,
* flags: 1,
* });
* });
* ```
* @return Returns a Promise that resolves to a `string` containing a `content://` URI pointing to the file.
* The URI is the same as the `fileUri` input parameter but in a different format.
* @platform android
*/
export async function getContentUriAsync(fileUri) {
if (Platform.OS === 'android') {
if (!ExponentFileSystem.getContentUriAsync) {
throw new UnavailabilityError('expo-file-system', 'getContentUriAsync');
}
return await ExponentFileSystem.getContentUriAsync(fileUri);
}
else {
return fileUri;
}
}
/**
* Write the entire contents of a file as a string.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* > Note: when you're using SAF URI the file needs to exist. You can't create a new file.
* @param contents The string to replace the contents of the file with.
* @param options A map of write options represented by [`WritingOptions`](#writingoptions) type.
*/
export async function writeAsStringAsync(fileUri, contents, options = {}) {
if (!ExponentFileSystem.writeAsStringAsync) {
throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync');
}
return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options);
}
/**
* Delete a file or directory. If the URI points to a directory, the directory and all its contents are recursively deleted.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* @param options A map of write options represented by [`DeletingOptions`](#deletingoptions) type.
*/
export async function deleteAsync(fileUri, options = {}) {
if (!ExponentFileSystem.deleteAsync) {
throw new UnavailabilityError('expo-file-system', 'deleteAsync');
}
return await ExponentFileSystem.deleteAsync(fileUri, options);
}
export async function deleteLegacyDocumentDirectoryAndroid() {
if (Platform.OS !== 'android' || documentDirectory == null) {
return;
}
const legacyDocumentDirectory = `${documentDirectory}ExperienceData/`;
return await deleteAsync(legacyDocumentDirectory, { idempotent: true });
}
/**
* Move a file or directory to a new location.
* @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
*/
export async function moveAsync(options) {
if (!ExponentFileSystem.moveAsync) {
throw new UnavailabilityError('expo-file-system', 'moveAsync');
}
return await ExponentFileSystem.moveAsync(options);
}
/**
* Create a copy of a file or directory. Directories are recursively copied with all of their contents.
* It can be also used to copy content shared by other apps to local filesystem.
* @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
*/
export async function copyAsync(options) {
if (!ExponentFileSystem.copyAsync) {
throw new UnavailabilityError('expo-file-system', 'copyAsync');
}
return await ExponentFileSystem.copyAsync(options);
}
/**
* Create a new empty directory.
* @param fileUri `file://` URI to the new directory to create.
* @param options A map of create directory options represented by [`MakeDirectoryOptions`](#makedirectoryoptions) type.
*/
export async function makeDirectoryAsync(fileUri, options = {}) {
if (!ExponentFileSystem.makeDirectoryAsync) {
throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync');
}
return await ExponentFileSystem.makeDirectoryAsync(fileUri, options);
}
/**
* Enumerate the contents of a directory.
* @param fileUri `file://` URI to the directory.
* @return A Promise that resolves to an array of strings, each containing the name of a file or directory contained in the directory at `fileUri`.
*/
export async function readDirectoryAsync(fileUri) {
if (!ExponentFileSystem.readDirectoryAsync) {
throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync');
}
return await ExponentFileSystem.readDirectoryAsync(fileUri);
}
/**
* Gets the available internal disk storage size, in bytes. This returns the free space on the data partition that hosts all of the internal storage for all apps on the device.
* @return Returns a Promise that resolves to the number of bytes available on the internal disk, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* if the capacity is greater than 2^53^ - 1 bytes.
*/
export async function getFreeDiskStorageAsync() {
if (!ExponentFileSystem.getFreeDiskStorageAsync) {
throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync');
}
return await ExponentFileSystem.getFreeDiskStorageAsync();
}
/**
* Gets total internal disk storage size, in bytes. This is the total capacity of the data partition that hosts all the internal storage for all apps on the device.
* @return Returns a Promise that resolves to a number that specifies the total internal disk storage capacity in bytes, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* if the capacity is greater than 2^53^ - 1 bytes.
*/
export async function getTotalDiskCapacityAsync() {
if (!ExponentFileSystem.getTotalDiskCapacityAsync) {
throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync');
}
return await ExponentFileSystem.getTotalDiskCapacityAsync();
}
/**
* Download the contents at a remote URI to a file in the app's file system. The directory for a local file uri must exist prior to calling this function.
* @param uri The remote URI to download from.
* @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
* If there is a file at this URI, its contents are replaced. The directory for the file must exist.
* @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
* @example
* ```js
* FileSystem.downloadAsync(
* 'http://techslides.com/demos/sample-videos/small.mp4',
* FileSystem.documentDirectory + 'small.mp4'
* )
* .then(({ uri }) => {
* console.log('Finished downloading to ', uri);
* })
* .catch(error => {
* console.error(error);
* });
* ```
* @return Returns a Promise that resolves to a `FileSystemDownloadResult` object.
*/
export async function downloadAsync(uri, fileUri, options = {}) {
if (!ExponentFileSystem.downloadAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadAsync');
}
return await ExponentFileSystem.downloadAsync(uri, fileUri, {
sessionType: FileSystemSessionType.BACKGROUND,
...options,
});
}
/**
* Upload the contents of the file pointed by `fileUri` to the remote url.
* @param url The remote URL, where the file will be sent.
* @param fileUri The local URI of the file to send. The file must exist.
* @param options A map of download options represented by [`FileSystemUploadOptions`](#filesystemuploadoptions) type.
* @example
* **Client**
*
* ```js
* import * as FileSystem from 'expo-file-system';
*
* try {
* const response = await FileSystem.uploadAsync(`http://192.168.0.1:1234/binary-upload`, fileUri, {
* fieldName: 'file',
* httpMethod: 'PATCH',
* uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
* });
* console.log(JSON.stringify(response, null, 4));
* } catch (error) {
* console.log(error);
* }
* ```
*
* **Server**
*
* Please refer to the "[Server: Handling multipart requests](#server-handling-multipart-requests)" example - there is code for a simple Node.js server.
* @return Returns a Promise that resolves to `FileSystemUploadResult` object.
*/
export async function uploadAsync(url, fileUri, options = {}) {
if (!ExponentFileSystem.uploadAsync) {
throw new UnavailabilityError('expo-file-system', 'uploadAsync');
}
return await ExponentFileSystem.uploadAsync(url, fileUri, {
sessionType: FileSystemSessionType.BACKGROUND,
uploadType: FileSystemUploadType.BINARY_CONTENT,
...options,
httpMethod: (options.httpMethod || 'POST').toUpperCase(),
});
}
/**
* Create a `DownloadResumable` object which can start, pause, and resume a download of contents at a remote URI to a file in the app's file system.
* > Note: You need to call `downloadAsync()`, on a `DownloadResumable` instance to initiate the download.
* The `DownloadResumable` object has a callback that provides download progress updates.
* Downloads can be resumed across app restarts by using `AsyncStorage` to store the `DownloadResumable.savable()` object for later retrieval.
* The `savable` object contains the arguments required to initialize a new `DownloadResumable` object to resume the download after an app restart.
* The directory for a local file uri must exist prior to calling this function.
* @param uri The remote URI to download from.
* @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
* If there is a file at this URI, its contents are replaced. The directory for the file must exist.
* @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
* @param callback This function is called on each data write to update the download progress.
* > **Note**: When the app has been moved to the background, this callback won't be fired until it's moved to the foreground.
* @param resumeData The string which allows the api to resume a paused download. This is set on the `DownloadResumable` object automatically when a download is paused.
* When initializing a new `DownloadResumable` this should be `null`.
*/
export function createDownloadResumable(uri, fileUri, options, callback, resumeData) {
return new DownloadResumable(uri, fileUri, options, callback, resumeData);
}
export function createUploadTask(url, fileUri, options, callback) {
return new UploadTask(url, fileUri, options, callback);
}
export class FileSystemCancellableNetworkTask {
_uuid = uuid.v4();
taskWasCanceled = false;
emitter = new EventEmitter(ExponentFileSystem);
subscription;
// @docsMissing
async cancelAsync() {
if (!ExponentFileSystem.networkTaskCancelAsync) {
throw new UnavailabilityError('expo-file-system', 'networkTaskCancelAsync');
}
this.removeSubscription();
this.taskWasCanceled = true;
return await ExponentFileSystem.networkTaskCancelAsync(this.uuid);
}
isTaskCancelled() {
if (this.taskWasCanceled) {
console.warn('This task was already canceled.');
return true;
}
return false;
}
get uuid() {
return this._uuid;
}
addSubscription() {
if (this.subscription) {
return;
}
this.subscription = this.emitter.addListener(this.getEventName(), (event) => {
if (event.uuid === this.uuid) {
const callback = this.getCallback();
if (callback) {
callback(event.data);
}
}
});
}
removeSubscription() {
if (!this.subscription) {
return;
}
this.emitter.removeSubscription(this.subscription);
this.subscription = null;
}
}
export class UploadTask extends FileSystemCancellableNetworkTask {
url;
fileUri;
callback;
options;
constructor(url, fileUri, options, callback) {
super();
this.url = url;
this.fileUri = fileUri;
this.callback = callback;
const httpMethod = (options?.httpMethod?.toUpperCase() ||
'POST');
this.options = {
sessionType: FileSystemSessionType.BACKGROUND,
uploadType: FileSystemUploadType.BINARY_CONTENT,
...options,
httpMethod,
};
}
getEventName() {
return 'expo-file-system.uploadProgress';
}
getCallback() {
return this.callback;
}
// @docsMissing
async uploadAsync() {
if (!ExponentFileSystem.uploadTaskStartAsync) {
throw new UnavailabilityError('expo-file-system', 'uploadTaskStartAsync');
}
if (this.isTaskCancelled()) {
return;
}
this.addSubscription();
const result = await ExponentFileSystem.uploadTaskStartAsync(this.url, this.fileUri, this.uuid, this.options);
this.removeSubscription();
return result;
}
}
export class DownloadResumable extends FileSystemCancellableNetworkTask {
url;
_fileUri;
options;
callback;
resumeData;
constructor(url, _fileUri, options = {}, callback, resumeData) {
super();
this.url = url;
this._fileUri = _fileUri;
this.options = options;
this.callback = callback;
this.resumeData = resumeData;
}
get fileUri() {
return this._fileUri;
}
getEventName() {
return 'expo-file-system.downloadProgress';
}
getCallback() {
return this.callback;
}
/**
* Download the contents at a remote URI to a file in the app's file system.
* @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
*/
async downloadAsync() {
if (!ExponentFileSystem.downloadResumableStartAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
}
if (this.isTaskCancelled()) {
return;
}
this.addSubscription();
return await ExponentFileSystem.downloadResumableStartAsync(this.url, this._fileUri, this.uuid, this.options, this.resumeData);
}
/**
* Pause the current download operation. `resumeData` is added to the `DownloadResumable` object after a successful pause operation.
* Returns an object that can be saved with `AsyncStorage` for future retrieval (the same object that is returned from calling `FileSystem.DownloadResumable.savable()`).
* @return Returns a Promise that resolves to `DownloadPauseState` object.
*/
async pauseAsync() {
if (!ExponentFileSystem.downloadResumablePauseAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
}
if (this.isTaskCancelled()) {
return {
fileUri: this._fileUri,
options: this.options,
url: this.url,
};
}
const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this.uuid);
this.removeSubscription();
if (pauseResult) {
this.resumeData = pauseResult.resumeData;
return this.savable();
}
else {
throw new Error('Unable to generate a savable pause state');
}
}
/**
* Resume a paused download operation.
* @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
*/
async resumeAsync() {
if (!ExponentFileSystem.downloadResumableStartAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
}
if (this.isTaskCancelled()) {
return;
}
this.addSubscription();
return await ExponentFileSystem.downloadResumableStartAsync(this.url, this.fileUri, this.uuid, this.options, this.resumeData);
}
/**
* Method to get the object which can be saved with `AsyncStorage` for future retrieval.
* @returns Returns object in shape of `DownloadPauseState` type.
*/
savable() {
return {
url: this.url,
fileUri: this.fileUri,
options: this.options,
resumeData: this.resumeData,
};
}
}
const baseReadAsStringAsync = readAsStringAsync;
const baseWriteAsStringAsync = writeAsStringAsync;
const baseDeleteAsync = deleteAsync;
const baseMoveAsync = moveAsync;
const baseCopyAsync = copyAsync;
/**
* The `StorageAccessFramework` is a namespace inside of the `expo-file-system` module, which encapsulates all functions which can be used with [SAF URIs](#saf-uri).
* You can read more about SAF in the [Android documentation](https://developer.android.com/guide/topics/providers/document-provider).
*
* @example
* # Basic Usage
*
* ```ts
* import { StorageAccessFramework } from 'expo-file-system';
*
* // Requests permissions for external directory
* const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
*
* if (permissions.granted) {
* // Gets SAF URI from response
* const uri = permissions.directoryUri;
*
* // Gets all files inside of selected directory
* const files = await StorageAccessFramework.readDirectoryAsync(uri);
* alert(`Files inside ${uri}:\n\n${JSON.stringify(files)}`);
* }
* ```
*
* # Migrating an album
*
* ```ts
* import * as MediaLibrary from 'expo-media-library';
* import * as FileSystem from 'expo-file-system';
* const { StorageAccessFramework } = FileSystem;
*
* async function migrateAlbum(albumName: string) {
* // Gets SAF URI to the album
* const albumUri = StorageAccessFramework.getUriForDirectoryInRoot(albumName);
*
* // Requests permissions
* const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync(albumUri);
* if (!permissions.granted) {
* return;
* }
*
* const permittedUri = permissions.directoryUri;
* // Checks if users selected the correct folder
* if (!permittedUri.includes(albumName)) {
* return;
* }
*
* const mediaLibraryPermissions = await MediaLibrary.requestPermissionsAsync();
* if (!mediaLibraryPermissions.granted) {
* return;
* }
*
* // Moves files from external storage to internal storage
* await StorageAccessFramework.moveAsync({
* from: permittedUri,
* to: FileSystem.documentDirectory!,
* });
*
* const outputDir = FileSystem.documentDirectory! + albumName;
* const migratedFiles = await FileSystem.readDirectoryAsync(outputDir);
*
* // Creates assets from local files
* const [newAlbumCreator, ...assets] = await Promise.all(
* migratedFiles.map<Promise<MediaLibrary.Asset>>(
* async fileName => await MediaLibrary.createAssetAsync(outputDir + '/' + fileName)
* )
* );
*
* // Album was empty
* if (!newAlbumCreator) {
* return;
* }
*
* // Creates a new album in the scoped directory
* const newAlbum = await MediaLibrary.createAlbumAsync(albumName, newAlbumCreator, false);
* if (assets.length) {
* await MediaLibrary.addAssetsToAlbumAsync(assets, newAlbum, false);
* }
* }
* ```
* @platform Android
*/
export var StorageAccessFramework;
(function (StorageAccessFramework) {
/**
* Gets a [SAF URI](#saf-uri) pointing to a folder in the Android root directory. You can use this function to get URI for
* `StorageAccessFramework.requestDirectoryPermissionsAsync()` when you trying to migrate an album. In that case, the name of the album is the folder name.
* @param folderName The name of the folder which is located in the Android root directory.
* @return Returns a [SAF URI](#saf-uri) to a folder.
*/
function getUriForDirectoryInRoot(folderName) {
return `content://com.android.externalstorage.documents/tree/primary:${folderName}/document/primary:${folderName}`;
}
StorageAccessFramework.getUriForDirectoryInRoot = getUriForDirectoryInRoot;
/**
* Allows users to select a specific directory, granting your app access to all of the files and sub-directories within that directory.
* @param initialFileUrl The [SAF URI](#saf-uri) of the directory that the file picker should display when it first loads.
* If URI is incorrect or points to a non-existing folder, it's ignored.
* @platform android 11+
* @return Returns a Promise that resolves to `FileSystemRequestDirectoryPermissionsResult` object.
*/
async function requestDirectoryPermissionsAsync(initialFileUrl = null) {
if (!ExponentFileSystem.requestDirectoryPermissionsAsync) {
throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.requestDirectoryPermissionsAsync');
}
return await ExponentFileSystem.requestDirectoryPermissionsAsync(initialFileUrl);
}
StorageAccessFramework.requestDirectoryPermissionsAsync = requestDirectoryPermissionsAsync;
/**
* Enumerate the contents of a directory.
* @param dirUri [SAF](#saf-uri) URI to the directory.
* @return A Promise that resolves to an array of strings, each containing the full [SAF URI](#saf-uri) of a file or directory contained in the directory at `fileUri`.
*/
async function readDirectoryAsync(dirUri) {
if (!ExponentFileSystem.readSAFDirectoryAsync) {
throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.readDirectoryAsync');
}
return await ExponentFileSystem.readSAFDirectoryAsync(dirUri);
}
StorageAccessFramework.readDirectoryAsync = readDirectoryAsync;
/**
* Creates a new empty directory.
* @param parentUri The [SAF](#saf-uri) URI to the parent directory.
* @param dirName The name of new directory.
* @return A Promise that resolves to a [SAF URI](#saf-uri) to the created directory.
*/
async function makeDirectoryAsync(parentUri, dirName) {
if (!ExponentFileSystem.makeSAFDirectoryAsync) {
throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.makeDirectoryAsync');
}
return await ExponentFileSystem.makeSAFDirectoryAsync(parentUri, dirName);
}
StorageAccessFramework.makeDirectoryAsync = makeDirectoryAsync;
/**
* Creates a new empty file.
* @param parentUri The [SAF](#saf-uri) URI to the parent directory.
* @param fileName The name of new file **without the extension**.
* @param mimeType The MIME type of new file.
* @return A Promise that resolves to a [SAF URI](#saf-uri) to the created file.
*/
async function createFileAsync(parentUri, fileName, mimeType) {
if (!ExponentFileSystem.createSAFFileAsync) {
throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.createFileAsync');
}
return await ExponentFileSystem.createSAFFileAsync(parentUri, fileName, mimeType);
}
StorageAccessFramework.createFileAsync = createFileAsync;
/**
* Alias for [`writeAsStringAsync`](#filesystemwriteasstringasyncfileuri-contents-options) method.
*/
StorageAccessFramework.writeAsStringAsync = baseWriteAsStringAsync;
/**
* Alias for [`readAsStringAsync`](#filesystemreadasstringasyncfileuri-options) method.
*/
StorageAccessFramework.readAsStringAsync = baseReadAsStringAsync;
/**
* Alias for [`deleteAsync`](#filesystemdeleteasyncfileuri-options) method.
*/
StorageAccessFramework.deleteAsync = baseDeleteAsync;
/**
* Alias for [`moveAsync`](#filesystemmoveasyncoptions) method.
*/
StorageAccessFramework.moveAsync = baseMoveAsync;
/**
* Alias for [`copyAsync`](#filesystemcopyasyncoptions) method.
*/
StorageAccessFramework.copyAsync = baseCopyAsync;
})(StorageAccessFramework || (StorageAccessFramework = {}));
//# sourceMappingURL=FileSystem.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,304 @@
/**
* These values can be used to define how sessions work on iOS.
* @platform ios
*/
export declare enum FileSystemSessionType {
/**
* Using this mode means that the downloading/uploading session on the native side will work even if the application is moved to background.
* If the task completes while the application is in background, the Promise will be either resolved immediately or (if the application execution has already been stopped) once the app is moved to foreground again.
* > Note: The background session doesn't fail if the server or your connection is down. Rather, it continues retrying until the task succeeds or is canceled manually.
*/
BACKGROUND = 0,
/**
* Using this mode means that downloading/uploading session on the native side will be terminated once the application becomes inactive (e.g. when it goes to background).
* Bringing the application to foreground again would trigger Promise rejection.
*/
FOREGROUND = 1
}
export declare enum FileSystemUploadType {
/**
* The file will be sent as a request's body. The request can't contain additional data.
*/
BINARY_CONTENT = 0,
/**
* An [RFC 2387-compliant](https://www.ietf.org/rfc/rfc2387.txt) request body. The provided file will be encoded into HTTP request.
* This request can contain additional data represented by [`UploadOptionsMultipart`](#uploadoptionsmultipart) type.
*/
MULTIPART = 1
}
export type DownloadOptions = {
/**
* If `true`, include the MD5 hash of the file in the returned object. Provided for convenience since it is common to check the integrity of a file immediately after downloading.
* @default false
*/
md5?: boolean;
cache?: boolean;
/**
* An object containing all the HTTP header fields and their values for the download network request. The keys and values of the object are the header names and values respectively.
*/
headers?: Record<string, string>;
/**
* A session type. Determines if tasks can be handled in the background. On Android, sessions always work in the background and you can't change it.
* @default FileSystemSessionType.BACKGROUND
* @platform ios
*/
sessionType?: FileSystemSessionType;
};
export type FileSystemHttpResult = {
/**
* An object containing all the HTTP response header fields and their values for the download network request.
* The keys and values of the object are the header names and values respectively.
*/
headers: Record<string, string>;
/**
* The HTTP response status code for the download network request.
*/
status: number;
mimeType: string | null;
};
export type FileSystemDownloadResult = FileSystemHttpResult & {
/**
* A `file://` URI pointing to the file. This is the same as the `fileUri` input parameter.
*/
uri: string;
/**
* Present if the `md5` option was truthy. Contains the MD5 hash of the file.
*/
md5?: string;
};
/**
* @deprecated Use `FileSystemDownloadResult` instead.
*/
export type DownloadResult = FileSystemDownloadResult;
export type FileSystemUploadOptions = (UploadOptionsBinary | UploadOptionsMultipart) & {
/**
* An object containing all the HTTP header fields and their values for the upload network request.
* The keys and values of the object are the header names and values respectively.
*/
headers?: Record<string, string>;
/**
* The request method.
* @default FileSystemAcceptedUploadHttpMethod.POST
*/
httpMethod?: FileSystemAcceptedUploadHttpMethod;
/**
* A session type. Determines if tasks can be handled in the background. On Android, sessions always work in the background and you can't change it.
* @default FileSystemSessionType.BACKGROUND
* @platform ios
*/
sessionType?: FileSystemSessionType;
};
/**
* Upload options when upload type is set to binary.
*/
export type UploadOptionsBinary = {
/**
* Upload type determines how the file will be sent to the server.
* Value will be `FileSystemUploadType.BINARY_CONTENT`.
*/
uploadType?: FileSystemUploadType;
};
/**
* Upload options when upload type is set to multipart.
*/
export type UploadOptionsMultipart = {
/**
* Upload type determines how the file will be sent to the server.
* Value will be `FileSystemUploadType.MULTIPART`.
*/
uploadType: FileSystemUploadType;
/**
* The name of the field which will hold uploaded file. Defaults to the file name without an extension.
*/
fieldName?: string;
/**
* The MIME type of the provided file. If not provided, the module will try to guess it based on the extension.
*/
mimeType?: string;
/**
* Additional form properties. They will be located in the request body.
*/
parameters?: Record<string, string>;
};
export type FileSystemUploadResult = FileSystemHttpResult & {
/**
* The body of the server response.
*/
body: string;
};
export type FileSystemNetworkTaskProgressCallback<T extends DownloadProgressData | UploadProgressData> = (data: T) => void;
/**
* @deprecated use `FileSystemNetworkTaskProgressCallback<DownloadProgressData>` instead.
*/
export type DownloadProgressCallback = FileSystemNetworkTaskProgressCallback<DownloadProgressData>;
export type DownloadProgressData = {
/**
* The total bytes written by the download operation.
*/
totalBytesWritten: number;
/**
* The total bytes expected to be written by the download operation. A value of `-1` means that the server did not return the `Content-Length` header
* and the total size is unknown. Without this header, you won't be able to track the download progress.
*/
totalBytesExpectedToWrite: number;
};
export type UploadProgressData = {
/**
* The total bytes sent by the upload operation.
*/
totalBytesSent: number;
/**
* The total bytes expected to be sent by the upload operation.
*/
totalBytesExpectedToSend: number;
};
export type DownloadPauseState = {
/**
* The remote URI to download from.
*/
url: string;
/**
* The local URI of the file to download to. If there is no file at this URI, a new one is created. If there is a file at this URI, its contents are replaced.
*/
fileUri: string;
/**
* Object representing the file download options.
*/
options: DownloadOptions;
/**
* The string which allows the API to resume a paused download.
*/
resumeData?: string;
};
export type FileInfo =
/**
* Object returned when file exist.
*/
{
/**
* Signifies that the requested file exist.
*/
exists: true;
/**
* A `file://` URI pointing to the file. This is the same as the `fileUri` input parameter.
*/
uri: string;
/**
* The size of the file in bytes. If operating on a source such as an iCloud file, only present if the `size` option was truthy.
*/
size: number;
/**
* Boolean set to `true` if this is a directory and `false` if it is a file.
*/
isDirectory: boolean;
/**
* The last modification time of the file expressed in seconds since epoch.
*/
modificationTime: number;
/**
* Present if the `md5` option was truthy. Contains the MD5 hash of the file.
*/
md5?: string;
} |
/**
* Object returned when file do not exist.
*/
{
exists: false;
uri: string;
isDirectory: false;
};
/**
* These values can be used to define how file system data is read / written.
*/
export declare enum EncodingType {
/**
* Standard encoding format.
*/
UTF8 = "utf8",
/**
* Binary, radix-64 representation.
*/
Base64 = "base64"
}
export type FileSystemAcceptedUploadHttpMethod = 'POST' | 'PUT' | 'PATCH';
export type ReadingOptions = {
/**
* The encoding format to use when reading the file.
* @default EncodingType.UTF8
*/
encoding?: EncodingType | 'utf8' | 'base64';
/**
* Optional number of bytes to skip. This option is only used when `encoding: FileSystem.EncodingType.Base64` and `length` is defined.
* */
position?: number;
/**
* Optional number of bytes to read. This option is only used when `encoding: FileSystem.EncodingType.Base64` and `position` is defined.
*/
length?: number;
};
export type WritingOptions = {
/**
* The encoding format to use when writing the file.
* @default FileSystem.EncodingType.UTF8
*/
encoding?: EncodingType | 'utf8' | 'base64';
};
export type DeletingOptions = {
/**
* If `true`, don't throw an error if there is no file or directory at this URI.
* @default false
*/
idempotent?: boolean;
};
export type InfoOptions = {
/**
* Whether to return the MD5 hash of the file.
* @default false
*/
md5?: boolean;
/**
* Explicitly specify that the file size should be included. For example, skipping this can prevent downloading the file if it's stored in iCloud.
* The size is always returned for `file://` locations.
*/
size?: boolean;
};
export type RelocatingOptions = {
/**
* URI or [SAF](#saf-uri) URI to the asset, file, or directory. See [supported URI schemes](#supported-uri-schemes-1).
*/
from: string;
/**
* `file://` URI to the file or directory which should be its new location.
*/
to: string;
};
export type MakeDirectoryOptions = {
/**
* If `true`, don't throw an error if there is no file or directory at this URI.
* @default false
*/
intermediates?: boolean;
};
export type ProgressEvent<T> = {
uuid: string;
data: T;
};
export type FileSystemRequestDirectoryPermissionsResult =
/**
* If the permissions were not granted.
*/
{
granted: false;
} |
/**
* If the permissions were granted.
*/
{
granted: true;
/**
* The [SAF URI](#saf-uri) to the user's selected directory. Available only if permissions were granted.
*/
directoryUri: string;
};
//# sourceMappingURL=FileSystem.types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FileSystem.types.d.ts","sourceRoot":"","sources":["../src/FileSystem.types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,oBAAY,qBAAqB;IAC/B;;;;OAIG;IACH,UAAU,IAAI;IACd;;;OAGG;IACH,UAAU,IAAI;CACf;AAED,oBAAY,oBAAoB;IAC9B;;OAEG;IACH,cAAc,IAAI;IAClB;;;OAGG;IACH,SAAS,IAAI;CACd;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IAEd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,oBAAoB,GAAG;IAC5D;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEtD,MAAM,MAAM,uBAAuB,GAAG,CAAC,mBAAmB,GAAG,sBAAsB,CAAC,GAAG;IACrF;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,kCAAkC,CAAC;IAChD;;;;OAIG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;OAGG;IACH,UAAU,CAAC,EAAE,oBAAoB,CAAC;CACnC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,UAAU,EAAE,oBAAoB,CAAC;IACjC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,GAAG;IAC1D;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAGF,MAAM,MAAM,qCAAqC,CAC/C,CAAC,SAAS,oBAAoB,GAAG,kBAAkB,IACjD,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEtB;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,qCAAqC,CAAC,oBAAoB,CAAC,CAAC;AAEnG,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,yBAAyB,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,wBAAwB,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,OAAO,EAAE,eAAe,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAGF,MAAM,MAAM,QAAQ;AAClB;;GAEG;AACH;IACE;;OAEG;IACH,MAAM,EAAE,IAAI,CAAC;IACb;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AACD;;GAEG;AACH;IACE,MAAM,EAAE,KAAK,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,KAAK,CAAC;CACpB,CAAC;AAGJ;;GAEG;AACH,oBAAY,YAAY;IACtB;;OAEG;IACH,IAAI,SAAS;IACb;;OAEG;IACH,MAAM,WAAW;CAClB;AAGD,MAAM,MAAM,kCAAkC,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC5C;;SAEK;IACL,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAGF,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,CAAC;CACT,CAAC;AAGF,MAAM,MAAM,2CAA2C;AACrD;;GAEG;AACH;IACE,OAAO,EAAE,KAAK,CAAC;CAChB;AACD;;GAEG;AACH;IACE,OAAO,EAAE,IAAI,CAAC;IACd;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC"}

View File

@@ -0,0 +1,47 @@
/**
* These values can be used to define how sessions work on iOS.
* @platform ios
*/
export var FileSystemSessionType;
(function (FileSystemSessionType) {
/**
* Using this mode means that the downloading/uploading session on the native side will work even if the application is moved to background.
* If the task completes while the application is in background, the Promise will be either resolved immediately or (if the application execution has already been stopped) once the app is moved to foreground again.
* > Note: The background session doesn't fail if the server or your connection is down. Rather, it continues retrying until the task succeeds or is canceled manually.
*/
FileSystemSessionType[FileSystemSessionType["BACKGROUND"] = 0] = "BACKGROUND";
/**
* Using this mode means that downloading/uploading session on the native side will be terminated once the application becomes inactive (e.g. when it goes to background).
* Bringing the application to foreground again would trigger Promise rejection.
*/
FileSystemSessionType[FileSystemSessionType["FOREGROUND"] = 1] = "FOREGROUND";
})(FileSystemSessionType || (FileSystemSessionType = {}));
export var FileSystemUploadType;
(function (FileSystemUploadType) {
/**
* The file will be sent as a request's body. The request can't contain additional data.
*/
FileSystemUploadType[FileSystemUploadType["BINARY_CONTENT"] = 0] = "BINARY_CONTENT";
/**
* An [RFC 2387-compliant](https://www.ietf.org/rfc/rfc2387.txt) request body. The provided file will be encoded into HTTP request.
* This request can contain additional data represented by [`UploadOptionsMultipart`](#uploadoptionsmultipart) type.
*/
FileSystemUploadType[FileSystemUploadType["MULTIPART"] = 1] = "MULTIPART";
})(FileSystemUploadType || (FileSystemUploadType = {}));
/* eslint-enable */
/**
* These values can be used to define how file system data is read / written.
*/
export var EncodingType;
(function (EncodingType) {
/**
* Standard encoding format.
*/
EncodingType["UTF8"] = "utf8";
/**
* Binary, radix-64 representation.
*/
EncodingType["Base64"] = "base64";
})(EncodingType || (EncodingType = {}));
/* eslint-enable */
//# sourceMappingURL=FileSystem.types.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
export * from './FileSystem';
export * from './FileSystem.types';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC"}

View File

@@ -0,0 +1,3 @@
export * from './FileSystem';
export * from './FileSystem.types';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC","sourcesContent":["export * from './FileSystem';\nexport * from './FileSystem.types';\n"]}

View File

@@ -0,0 +1,33 @@
type PlatformMethod = (...args: any[]) => Promise<any>;
export interface ExponentFileSystemModule {
readonly documentDirectory: string | null;
readonly cacheDirectory: string | null;
readonly bundleDirectory: string | null;
readonly getInfoAsync?: PlatformMethod;
readonly readAsStringAsync?: PlatformMethod;
readonly writeAsStringAsync?: PlatformMethod;
readonly deleteAsync?: PlatformMethod;
readonly moveAsync?: PlatformMethod;
readonly copyAsync?: PlatformMethod;
readonly makeDirectoryAsync?: PlatformMethod;
readonly readDirectoryAsync?: PlatformMethod;
readonly downloadAsync?: PlatformMethod;
readonly uploadAsync?: PlatformMethod;
readonly downloadResumableStartAsync?: PlatformMethod;
readonly downloadResumablePauseAsync?: PlatformMethod;
readonly getContentUriAsync?: PlatformMethod;
readonly getFreeDiskStorageAsync?: PlatformMethod;
readonly getTotalDiskCapacityAsync?: PlatformMethod;
readonly requestDirectoryPermissionsAsync?: PlatformMethod;
readonly readSAFDirectoryAsync?: PlatformMethod;
readonly makeSAFDirectoryAsync?: PlatformMethod;
readonly createSAFFileAsync?: PlatformMethod;
readonly networkTaskCancelAsync?: PlatformMethod;
readonly uploadTaskStartAsync?: PlatformMethod;
startObserving?: () => void;
stopObserving?: () => void;
addListener: (eventName: string) => void;
removeListeners: (count: number) => void;
}
export {};
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAEvD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC;IACvC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,cAAc,CAAC;IAC5C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,cAAc,CAAC;IAC7C,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IACpC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,cAAc,CAAC;IAC7C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,cAAc,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;IACtC,QAAQ,CAAC,2BAA2B,CAAC,EAAE,cAAc,CAAC;IACtD,QAAQ,CAAC,2BAA2B,CAAC,EAAE,cAAc,CAAC;IACtD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,cAAc,CAAC;IAC7C,QAAQ,CAAC,uBAAuB,CAAC,EAAE,cAAc,CAAC;IAClD,QAAQ,CAAC,yBAAyB,CAAC,EAAE,cAAc,CAAC;IACpD,QAAQ,CAAC,gCAAgC,CAAC,EAAE,cAAc,CAAC;IAC3D,QAAQ,CAAC,qBAAqB,CAAC,EAAE,cAAc,CAAC;IAChD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,cAAc,CAAC;IAChD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,cAAc,CAAC;IAC7C,QAAQ,CAAC,sBAAsB,CAAC,EAAE,cAAc,CAAC;IACjD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,cAAc,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["type PlatformMethod = (...args: any[]) => Promise<any>;\n\nexport interface ExponentFileSystemModule {\n readonly documentDirectory: string | null;\n readonly cacheDirectory: string | null;\n readonly bundleDirectory: string | null;\n readonly getInfoAsync?: PlatformMethod;\n readonly readAsStringAsync?: PlatformMethod;\n readonly writeAsStringAsync?: PlatformMethod;\n readonly deleteAsync?: PlatformMethod;\n readonly moveAsync?: PlatformMethod;\n readonly copyAsync?: PlatformMethod;\n readonly makeDirectoryAsync?: PlatformMethod;\n readonly readDirectoryAsync?: PlatformMethod;\n readonly downloadAsync?: PlatformMethod;\n readonly uploadAsync?: PlatformMethod;\n readonly downloadResumableStartAsync?: PlatformMethod;\n readonly downloadResumablePauseAsync?: PlatformMethod;\n readonly getContentUriAsync?: PlatformMethod;\n readonly getFreeDiskStorageAsync?: PlatformMethod;\n readonly getTotalDiskCapacityAsync?: PlatformMethod;\n readonly requestDirectoryPermissionsAsync?: PlatformMethod;\n readonly readSAFDirectoryAsync?: PlatformMethod;\n readonly makeSAFDirectoryAsync?: PlatformMethod;\n readonly createSAFFileAsync?: PlatformMethod;\n readonly networkTaskCancelAsync?: PlatformMethod;\n readonly uploadTaskStartAsync?: PlatformMethod;\n startObserving?: () => void;\n stopObserving?: () => void;\n addListener: (eventName: string) => void;\n removeListeners: (count: number) => void;\n}\n"]}

View File

@@ -0,0 +1,10 @@
{
"platforms": ["apple", "android"],
"apple": {
"modules": ["FileSystemModule"],
"appDelegateSubscribers": ["FileSystemBackgroundSessionHandler"]
},
"android": {
"modules": ["expo.modules.filesystem.FileSystemModule"]
}
}

View File

@@ -0,0 +1,6 @@
#import <ExpoFileSystem/EXFileSystemHandler.h>
@interface EXFileSystemAssetLibraryHandler : NSObject <EXFileSystemHandler>
@end

View File

@@ -0,0 +1,145 @@
#import <ExpoFileSystem/EXFileSystemAssetLibraryHandler.h>
#import <ExpoFileSystem/NSData+EXFileSystem.h>
#import <Photos/Photos.h>
@implementation EXFileSystemAssetLibraryHandler
+ (void)getInfoForFile:(NSURL *)fileUri
withOptions:(NSDictionary *)options
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject
{
NSError *error;
PHFetchResult<PHAsset *> *fetchResult = [self fetchResultForUri:fileUri error:&error];
if (error) {
reject(@"E_UNSUPPORTED_ARG", error.description, error);
return;
}
if (fetchResult.count > 0) {
PHAsset *asset = fetchResult[0];
NSMutableDictionary *result = [NSMutableDictionary dictionary];
result[@"exists"] = @(YES);
result[@"isDirectory"] = @(NO);
result[@"uri"] = fileUri;
// Uses required reason API based on the following reason: 3B52.1
result[@"modificationTime"] = @(asset.modificationDate.timeIntervalSince1970);
if (options[@"md5"] || options[@"size"]) {
[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, CGImagePropertyOrientation orientation, NSDictionary * _Nullable info) {
result[@"size"] = @(imageData.length);
if (options[@"md5"]) {
result[@"md5"] = [imageData md5String];
}
resolve(result);
}];
} else {
resolve(result);
}
} else {
resolve(@{@"exists": @(NO), @"isDirectory": @(NO)});
}
}
+ (void)copyFrom:(NSURL *)from
to:(NSURL *)to
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject
{
NSString *toPath = [to.path stringByStandardizingPath];
// NOTE: The destination-delete and the copy should happen atomically, but we hope for the best for now
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:toPath]) {
if (![[NSFileManager defaultManager] removeItemAtPath:toPath error:&error]) {
reject(@"E_FILE_NOT_COPIED",
[NSString stringWithFormat:@"File '%@' could not be copied to '%@' because a file already exists at "
"the destination and could not be deleted.", from, to],
error);
return;
}
}
PHFetchResult<PHAsset *> *fetchResult = [self fetchResultForUri:from error:&error];
if (error) {
reject(@"E_UNSUPPORTED_ARG", error.description, error);
return;
}
if (fetchResult.count > 0) {
PHAsset *asset = fetchResult[0];
if (asset.mediaType == PHAssetMediaTypeVideo) {
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:nil resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if (![asset isKindOfClass:[AVURLAsset class]]) {
reject(@"ERR_INCORRECT_ASSET_TYPE",
[NSString stringWithFormat:@"File '%@' has incorrect asset type.", from],
nil);
return;
}
AVURLAsset* urlAsset = (AVURLAsset*)asset;
NSNumber *size;
[urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil];
NSData *data = [NSData dataWithContentsOfURL:urlAsset.URL];
[EXFileSystemAssetLibraryHandler copyData:data toPath:toPath resolver:resolve rejecter:reject];
}];
} else {
[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, CGImagePropertyOrientation orientation, NSDictionary * _Nullable info) {
[EXFileSystemAssetLibraryHandler copyData:imageData toPath:toPath resolver:resolve rejecter:reject];
}];
}
} else {
reject(@"E_FILE_NOT_COPIED",
[NSString stringWithFormat:@"File '%@' could not be found.", from],
error);
}
}
// adapted from RCTImageLoader.m
+ (PHFetchResult<PHAsset *> *)fetchResultForUri:(NSURL *)url error:(NSError **)error
{
if ([url.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) {
// Fetch assets using PHAsset localIdentifier (recommended)
NSString *const localIdentifier = [url.absoluteString substringFromIndex:@"ph://".length];
return [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil];
} else if ([url.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
#if TARGET_OS_MACCATALYST
static BOOL hasWarned = NO;
if (!hasWarned) {
NSLog(@"assets-library:// URLs have been deprecated and cannot be accessed in macOS Catalyst. Returning nil (future warnings will be suppressed).");
hasWarned = YES;
}
return nil;
#elif TARGET_OS_IOS || TARGET_OS_TV
// This is the older, deprecated way of fetching assets from assets-library
// using the "assets-library://" protocol
return [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil];
#elif TARGET_OS_OSX
return nil;
#endif
}
NSString *description = [NSString stringWithFormat:@"Invalid URL provided, expected scheme to be either 'ph' or 'assets-library', was '%@'.", url.scheme];
*error = [[NSError alloc] initWithDomain:NSURLErrorDomain
code:NSURLErrorUnsupportedURL
userInfo:@{NSLocalizedDescriptionKey: description}];
return nil;
}
+ (void)copyData:(NSData *)data
toPath:(NSString *)path
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject
{
if ([data writeToFile:path atomically:YES]) {
resolve(nil);
} else {
reject(@"E_FILE_NOT_COPIED",
[NSString stringWithFormat:@"File could not be copied to '%@'.", path],
nil);
}
}
@end

View File

@@ -0,0 +1,17 @@
// Copyright 2023-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/ExpoModulesCore.h>
@protocol EXFileSystemHandler
+ (void)getInfoForFile:(NSURL *)fileUri
withOptions:(NSDictionary *)optionxs
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject;
+ (void)copyFrom:(NSURL *)from
to:(NSURL *)to
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject;
@end

View File

@@ -0,0 +1,7 @@
// Copyright 2023-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXFileSystemHandler.h>
@interface EXFileSystemLocalFileHandler : NSObject <EXFileSystemHandler>
@end

View File

@@ -0,0 +1,80 @@
#import <ExpoFileSystem/EXFileSystemLocalFileHandler.h>
#import <ExpoFileSystem/NSData+EXFileSystem.h>
@implementation EXFileSystemLocalFileHandler
+ (void)getInfoForFile:(NSURL *)fileUri
withOptions:(NSDictionary *)options
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject
{
NSString *path = fileUri.path;
BOOL isDirectory;
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
NSMutableDictionary *result = [NSMutableDictionary dictionary];
result[@"exists"] = @(YES);
result[@"isDirectory"] = @(isDirectory);
result[@"uri"] = [NSURL fileURLWithPath:path].absoluteString;
if (options[@"md5"]) {
result[@"md5"] = [[NSData dataWithContentsOfFile:path] md5String];
}
result[@"size"] = @([EXFileSystemLocalFileHandler getFileSize:path attributes:attributes]);
// Uses required reason API based on the following reason: 0A2A.1
result[@"modificationTime"] = @(attributes.fileModificationDate.timeIntervalSince1970);
resolve(result);
} else {
resolve(@{@"exists": @(NO), @"isDirectory": @(NO)});
}
}
+ (unsigned long long)getFileSize:(NSString *)path attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
{
if (attributes.fileType != NSFileTypeDirectory) {
return attributes.fileSize;
}
// The path is pointing to the folder
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];
NSEnumerator *contentsEnumurator = [contents objectEnumerator];
NSString *file;
unsigned long long folderSize = 0;
while (file = [contentsEnumurator nextObject]) {
NSString *filePath = [path stringByAppendingPathComponent:file];
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
folderSize += [EXFileSystemLocalFileHandler getFileSize:filePath attributes:fileAttributes];
}
return folderSize;
}
+ (void)copyFrom:(NSURL *)from
to:(NSURL *)to
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject
{
NSString *fromPath = [from.path stringByStandardizingPath];
NSString *toPath = [to.path stringByStandardizingPath];
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:toPath]) {
if (![[NSFileManager defaultManager] removeItemAtPath:toPath error:&error]) {
reject(@"E_FILE_NOT_COPIED",
[NSString stringWithFormat:@"File '%@' could not be copied to '%@' because a file already exists at "
"the destination and could not be deleted.", from, to],
error);
return;
}
}
if ([[NSFileManager defaultManager] copyItemAtPath:fromPath toPath:toPath error:&error]) {
resolve(nil);
} else {
reject(@"E_FILE_NOT_COPIED",
[NSString stringWithFormat:@"File '%@' could not be copied to '%@'.", from, to],
error);
}
}
@end

View File

@@ -0,0 +1,16 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionUploadTaskDelegate.h>
#import <ExpoFileSystem/EXTaskHandlersManager.h>
typedef void (^EXUploadDelegateOnSendCallback)(NSURLSessionUploadTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend);
@interface EXSessionCancelableUploadTaskDelegate : EXSessionUploadTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
onSendCallback:(EXUploadDelegateOnSendCallback)onSendCallback
resumableManager:(EXTaskHandlersManager *)manager
uuid:(NSString *)uuid;
@end

View File

@@ -0,0 +1,55 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionCancelableUploadTaskDelegate.h>
@interface EXSessionCancelableUploadTaskDelegate ()
@property (strong, nonatomic, readonly) EXUploadDelegateOnSendCallback onSendCallback;
@property (weak, nonatomic) EXTaskHandlersManager *manager;
@property (strong, nonatomic) NSString *uuid;
@end
@implementation EXSessionCancelableUploadTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
onSendCallback:(EXUploadDelegateOnSendCallback)onSendCallback
resumableManager:(EXTaskHandlersManager *)manager
uuid:(NSString *)uuid;
{
if (self = [super initWithResolve:resolve
reject:reject]) {
_onSendCallback = onSendCallback;
_manager = manager;
_uuid = uuid;
}
return self;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error) {
// The task was paused by us. So, we shouldn't throw.
if (error.code == NSURLErrorCancelled) {
self.resolve([NSNull null]);
[_manager unregisterTask:_uuid];
return;
}
}
[super URLSession:session task:task didCompleteWithError:error];
[_manager unregisterTask:_uuid];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
if (_onSendCallback && bytesSent > 0) {
_onSendCallback(task, bytesSent, totalBytesSent, totalBytesExpectedToSend);
}
}
@end

View File

@@ -0,0 +1,13 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionTaskDelegate.h>
@interface EXSessionDownloadTaskDelegate : EXSessionTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
localUrl:(NSURL *)localUrl
shouldCalculateMd5:(BOOL)shouldCalculateMd5;
@end

View File

@@ -0,0 +1,64 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionDownloadTaskDelegate.h>
#import <ExpoFileSystem/NSData+EXFileSystem.h>
@interface EXSessionDownloadTaskDelegate ()
@property (strong, nonatomic) NSURL *localUrl;
@property (nonatomic) BOOL shouldCalculateMd5;
@end
@implementation EXSessionDownloadTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
localUrl:(NSURL *)localUrl
shouldCalculateMd5:(BOOL)shouldCalculateMd5
{
if (self = [super initWithResolve:resolve reject:reject])
{
_localUrl = localUrl;
_shouldCalculateMd5 = shouldCalculateMd5;
}
return self;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:_localUrl.path]) {
[fileManager removeItemAtURL:_localUrl error:&error];
if (error) {
self.reject(@"ERR_FILESYSTEM_CANNOT_REMOVE",
[NSString stringWithFormat:@"Unable to remove file from local URI: '%@'", error.description],
error);
return;
}
}
[fileManager moveItemAtURL:location toURL:_localUrl error:&error];
if (error) {
self.reject(@"ERR_FILESYSTEM_CANNOT_SAVE",
[NSString stringWithFormat:@"Unable to save file to local URI: '%@'", error.description],
error);
return;
}
self.resolve([self parseServerResponse:downloadTask.response]);
}
- (NSDictionary *)parseServerResponse:(NSURLResponse *)response
{
NSMutableDictionary *result = [[super parseServerResponse:response] mutableCopy];
result[@"uri"] = _localUrl.absoluteString;
if (_shouldCalculateMd5) {
NSData *data = [NSData dataWithContentsOfURL:_localUrl];
result[@"md5"] = [data md5String];
}
return result;
}
@end

View File

@@ -0,0 +1,19 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <ExpoModulesCore/Platform.h>
#import <ExpoModulesCore/EXSingletonModule.h>
NS_ASSUME_NONNULL_BEGIN
@protocol EXSessionHandler
- (void)invokeCompletionHandlerForSessionIdentifier:(NSString *)identifier;
@end
@interface EXSessionHandler : EXSingletonModule <UIApplicationDelegate, EXSessionHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,49 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionHandler.h>
#import <ExpoModulesCore/EXDefines.h>
@interface EXSessionHandler ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, void (^)(void)> *completionHandlers;
@end
@implementation EXSessionHandler
EX_REGISTER_SINGLETON_MODULE(SessionHandler);
- (instancetype)init
{
if (self = [super init]) {
_completionHandlers = [NSMutableDictionary dictionary];
}
return self;
}
- (void)invokeCompletionHandlerForSessionIdentifier:(NSString *)identifier
{
if (!identifier) {
return;
}
void (^completionHandler)(void) = _completionHandlers[identifier];
if (completionHandler) {
// We need to run completionHandler explicite on the main thread because is's part of UIKit
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler();
});
[_completionHandlers removeObjectForKey:identifier];
}
}
#pragma mark - AppDelegate
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
_completionHandlers[identifier] = completionHandler;
}
@end

View File

@@ -0,0 +1,18 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionDownloadTaskDelegate.h>
#import <ExpoFileSystem/EXTaskHandlersManager.h>
typedef void (^EXDownloadDelegateOnWriteCallback)(NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite);
@interface EXSessionResumableDownloadTaskDelegate : EXSessionDownloadTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
localUrl:(NSURL *)localUrl
shouldCalculateMd5:(BOOL)shouldCalculateMd5
onWriteCallback:(EXDownloadDelegateOnWriteCallback)onWriteCallback
resumableManager:(EXTaskHandlersManager *)manager
uuid:(NSString *)uuid;
@end

View File

@@ -0,0 +1,66 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionResumableDownloadTaskDelegate.h>
@interface EXSessionResumableDownloadTaskDelegate ()
@property (strong, nonatomic, readonly) EXDownloadDelegateOnWriteCallback onWriteCallback;
@property (weak, nonatomic) EXTaskHandlersManager *manager;
@property (strong, nonatomic) NSString *uuid;
@end
@implementation EXSessionResumableDownloadTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
localUrl:(NSURL *)localUrl
shouldCalculateMd5:(BOOL)shouldCalculateMd5
onWriteCallback:(EXDownloadDelegateOnWriteCallback)onWriteCallback
resumableManager:(EXTaskHandlersManager *)manager
uuid:(NSString *)uuid;
{
if (self = [super initWithResolve:resolve
reject:reject
localUrl:localUrl
shouldCalculateMd5:shouldCalculateMd5]) {
_onWriteCallback = onWriteCallback;
_manager = manager;
_uuid = uuid;
}
return self;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
[super URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:location];
[_manager unregisterTask:_uuid];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error) {
// The task was paused by us. So, we shouldn't throw.
if (error.code == NSURLErrorCancelled) {
self.resolve([NSNull null]);
} else {
self.reject(@"ERR_FILESYSTEM_CANNOT_DOWNLOAD",
[NSString stringWithFormat:@"Unable to download file: %@", error.description],
error);
}
}
[_manager unregisterTask:_uuid];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
if (_onWriteCallback && bytesWritten > 0) {
_onWriteCallback(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}
}
@end

View File

@@ -0,0 +1,32 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <ExpoModulesCore/EXDefines.h>
@interface EXSessionTaskDelegate : NSObject
@property (nonatomic, strong, readonly) EXPromiseResolveBlock resolve;
@property (nonatomic, strong, readonly) EXPromiseRejectBlock reject;
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject;
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;
- (NSDictionary *)parseServerResponse:(NSURLResponse *)response;
@end

View File

@@ -0,0 +1,58 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionTaskDelegate.h>
@implementation EXSessionTaskDelegate
- (nonnull instancetype)initWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject
{
if (self = [super init]) {
_resolve = resolve;
_reject = reject;
}
return self;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error) {
self.reject(@"ERR_FILESYSTEM_CANNOT_DOWNLOAD",
[NSString stringWithFormat:@"Unable to download file: %@", error.description],
error);
}
}
- (NSDictionary *)parseServerResponse:(NSURLResponse *)response
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
return @{
@"status": @([httpResponse statusCode]),
@"headers": [httpResponse allHeaderFields],
@"mimeType": EXNullIfNil([httpResponse MIMEType])
};
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
}
@end

View File

@@ -0,0 +1,19 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <ExpoFileSystem/EXSessionTaskDelegate.h>
#import <ExpoFileSystem/EXSessionHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXSessionTaskDispatcher : NSObject <NSURLSessionDelegate>
- (instancetype)initWithSessionHandler:(nullable id<EXSessionHandler>)sessionHandler;
- (void)registerTaskDelegate:(EXSessionTaskDelegate *)delegate forTask:(NSURLSessionTask *)task;
- (void)deactivate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,96 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionTaskDispatcher.h>
#import <ExpoFileSystem/EXSessionResumableDownloadTaskDelegate.h>
@interface EXSessionTaskDispatcher ()
@property (nonatomic, strong) NSMutableDictionary<NSURLSessionTask *, EXSessionTaskDelegate *> *tasks;
@property (nonatomic) BOOL isActive;
@property (nonatomic, weak, nullable) id<EXSessionHandler> sessionHandler;
@end
@implementation EXSessionTaskDispatcher
- (instancetype)initWithSessionHandler:(nullable id<EXSessionHandler>)sessionHandler;
{
if (self = [super init]) {
_tasks = [NSMutableDictionary dictionary];
_isActive = true;
_sessionHandler = sessionHandler;
}
return self;
}
#pragma mark - public methods
- (void)registerTaskDelegate:(EXSessionTaskDelegate *)delegate forTask:(NSURLSessionTask *)task
{
_tasks[task] = delegate;
}
- (void)deactivate
{
_isActive = false;
}
#pragma mark - dispatcher
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
if (_isActive) {
EXSessionTaskDelegate *exTask = _tasks[downloadTask];
if (exTask) {
[exTask URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:location];
[_tasks removeObjectForKey:downloadTask];
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (_isActive) {
EXSessionTaskDelegate *exTask = _tasks[task];
if (exTask) {
[exTask URLSession:session task:task didCompleteWithError:error];
[_tasks removeObjectForKey:task];
}
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
if (_isActive) {
EXSessionTaskDelegate *exTask = _tasks[downloadTask];
[exTask URLSession:session downloadTask:downloadTask didWriteData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
if (_isActive) {
EXSessionTaskDelegate *exTask = _tasks[dataTask];
[exTask URLSession:session dataTask:dataTask didReceiveData:data];
}
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
[_sessionHandler invokeCompletionHandlerForSessionIdentifier:session.configuration.identifier];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
if (_isActive) {
EXSessionTaskDelegate *exTask = _tasks[task];
[exTask URLSession:session task:task didSendBodyData:bytesSent totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend];
}
}
@end

View File

@@ -0,0 +1,8 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionTaskDelegate.h>
@interface EXSessionUploadTaskDelegate : EXSessionTaskDelegate
@end

View File

@@ -0,0 +1,52 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXSessionUploadTaskDelegate.h>
@interface EXSessionUploadTaskDelegate ()
@property (strong, nonatomic) NSMutableData *responseData;
@end
@implementation EXSessionUploadTaskDelegate
- (instancetype)initWithResolve:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject
{
if (self = [super initWithResolve:resolve reject:reject]) {
_responseData = [NSMutableData new];
}
return self;
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
if (!data.length) {
return;
}
[_responseData appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error) {
self.reject(@"ERR_FILESYSTEM_CANNOT_UPLOAD",
[NSString stringWithFormat:@"Unable to upload the file: '%@'", error.description],
error);
return;
}
// We only set EXSessionUploadTaskDelegates as delegates of upload tasks
// so it should be safe to assume that this is what we will receive here.
NSURLSessionUploadTask *uploadTask = (NSURLSessionUploadTask *)task;
self.resolve([self parseServerResponse:uploadTask.response]);
}
- (NSDictionary *)parseServerResponse:(NSURLResponse *)response
{
NSMutableDictionary *result = [[super parseServerResponse:response] mutableCopy];
// TODO: add support for others response types (different encodings, files)
result[@"body"] = EXNullIfNil([[NSString alloc] initWithData:_responseData encoding:NSUTF8StringEncoding]);
return result;
}
@end

View File

@@ -0,0 +1,21 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXTaskHandlersManager : NSObject
- (NSURLSessionTask * _Nullable)taskForId:(NSString *)uuid;
- (NSURLSessionDownloadTask * _Nullable)downloadTaskForId:(NSString *)uuid;
- (NSURLSessionUploadTask * _Nullable)uploadTaskForId:(NSString *)uuid;
- (void)registerTask:(NSURLSessionTask *)task uuid:(NSString *)uuid;
- (void)unregisterTask:(NSString *)uuid;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,56 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/EXTaskHandlersManager.h>
@interface EXTaskHandlersManager ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSURLSessionTask *> *resumableDownloads;
@end
@implementation EXTaskHandlersManager
- (instancetype)init
{
if (self = [super init]) {
_resumableDownloads = [NSMutableDictionary dictionary];
}
return self;
}
- (void)registerTask:(NSURLSessionTask *)task uuid:(NSString *)uuid
{
_resumableDownloads[uuid] = task;
}
- (NSURLSessionTask * _Nullable)taskForId:(NSString *)uuid
{
return _resumableDownloads[uuid];
}
- (NSURLSessionDownloadTask * _Nullable)downloadTaskForId:(NSString *)uuid
{
NSURLSessionTask *task = [self taskForId:uuid];
if ([task isKindOfClass:[NSURLSessionDownloadTask class]]) {
return (NSURLSessionDownloadTask *)task;
}
return nil;
}
- (NSURLSessionUploadTask * _Nullable)uploadTaskForId:(NSString *)uuid
{
NSURLSessionTask *task = [self taskForId:uuid];
if ([task isKindOfClass:[NSURLSessionUploadTask class]]) {
return (NSURLSessionDownloadTask *)task;
}
return nil;
}
- (void)unregisterTask:(NSString *)uuid
{
[_resumableDownloads removeObjectForKey:uuid];
}
@end

View File

@@ -0,0 +1,88 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
enum Encoding: String, Enumerable {
// Equivalents of String.Encoding
case ascii
case nextstep
case japaneseeuc
case utf8
case isolatin1
case symbol
case nonlossyascii
case shiftjis
case isolatin2
case unicode
case windowscp1251
case windowscp1252
case windowscp1253
case windowscp1254
case windowscp1250
case iso2022jp
case macosroman
case utf16
case utf16bigendian
case utf16littleendian
case utf32
case utf32bigendian
case utf32littleendian
// Without equivalents in String.Encoding
case base64
func toStringEncoding() -> String.Encoding? {
switch self {
case .ascii:
return .ascii
case .nextstep:
return .nextstep
case .japaneseeuc:
return .japaneseEUC
case .utf8:
return .utf8
case .isolatin1:
return .isoLatin1
case .symbol:
return .symbol
case .nonlossyascii:
return .nonLossyASCII
case .shiftjis:
return .shiftJIS
case .isolatin2:
return .isoLatin2
case .unicode:
return .unicode
case .windowscp1251:
return .windowsCP1251
case .windowscp1252:
return .windowsCP1252
case .windowscp1253:
return .windowsCP1253
case .windowscp1254:
return .windowsCP1254
case .windowscp1250:
return .windowsCP1250
case .iso2022jp:
return .iso2022JP
case .macosroman:
return .macOSRoman
case .utf16:
return .utf16
case .utf16bigendian:
return .utf16BigEndian
case .utf16littleendian:
return .utf16LittleEndian
case .utf32:
return .utf32
case .utf32bigendian:
return .utf32BigEndian
case .utf32littleendian:
return .utf32LittleEndian
// Cases that don't have their own equivalent in String.Encoding
case .base64:
return nil
}
}
}

View File

@@ -0,0 +1 @@
// Copyright 2023-present 650 Industries. All rights reserved.

View File

@@ -0,0 +1,38 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoFileSystem'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = {
:ios => '13.4',
:osx => '10.15',
:tvos => '13.4'
}
s.swift_version = '5.4'
s.source = { :git => 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES'
}
s.resource_bundles = {'ExpoFileSystem_privacy' => ['PrivacyInfo.xcprivacy']}
s.source_files = "**/*.{h,m,swift}"
s.exclude_files = 'Tests/'
s.test_spec 'Tests' do |test_spec|
test_spec.dependency 'ExpoModulesTestCore'
test_spec.source_files = 'Tests/**/*.{m,swift}'
end
end

View File

@@ -0,0 +1,27 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
public final class FileSystemBackgroundSessionHandler: ExpoAppDelegateSubscriber, EXSessionHandlerProtocol {
public typealias BackgroundSessionCompletionHandler = () -> Void
private var completionHandlers: [String: BackgroundSessionCompletionHandler] = [:]
public func invokeCompletionHandler(forSessionIdentifier identifier: String) {
guard let completionHandler = completionHandlers[identifier] else {
return
}
DispatchQueue.main.async {
completionHandler()
}
completionHandlers.removeValue(forKey: identifier)
}
// MARK: - ExpoAppDelegateSubscriber
#if os(iOS) || os(tvOS)
public func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
completionHandlers[identifier] = completionHandler
}
#endif
}

View File

@@ -0,0 +1,99 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
final class FileNotExistsException: GenericException<String> {
override var reason: String {
"File '\(param)' does not exist"
}
}
final class FileAlreadyExistsException: GenericException<String> {
override var reason: String {
"File '\(param)' already exists"
}
}
final class DirectoryNotExistsException: GenericException<String> {
override var reason: String {
"Directory '\(param)' does not exist"
}
}
final class FileNotReadableException: GenericException<String> {
override var reason: String {
"File '\(param)' is not readable"
}
}
final class FileNotWritableException: GenericException<String> {
override var reason: String {
"File '\(param)' is not writable"
}
}
final class FileWriteFailedException: GenericException<String> {
override var reason: String {
"Writing to '\(param)' file has failed"
}
}
final class FileCannotDeleteException: GenericException<String> {
override var reason: String {
"File '\(param)' could not be deleted"
}
}
final class InvalidFileUrlException: GenericException<URL> {
override var reason: String {
"'\(param.absoluteString)' is not a file URL"
}
}
final class UnsupportedSchemeException: GenericException<String?> {
override var reason: String {
"Unsupported URI scheme: '\(String(describing: param))'"
}
}
final class HeaderEncodingFailedException: GenericException<String> {
override var reason: String {
"Unable to encode headers for request '\(param)' to UTF8"
}
}
final class DownloadTaskNotFoundException: GenericException<String> {
override var reason: String {
"Cannot find a download task with id: '\(param)'"
}
}
final class CannotDetermineDiskCapacity: Exception {
override var reason: String {
"Unable to determine free disk storage capacity"
}
}
final class FailedToCreateBodyException: Exception {
override var reason: String {
"Unable to create multipart body"
}
}
final class FailedToAccessDirectoryException: Exception {
override var reason: String {
"Failed to access `Caches` directory"
}
}
final class FailedToCopyAssetException: GenericException<String> {
override var reason: String {
"Failed to copy photo library asset: \(param)"
}
}
final class FailedToFindAssetException: GenericException<String> {
override var reason: String {
"Failed to find photo library asset: \(param)"
}
}

View File

@@ -0,0 +1,109 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
import Photos
private let assetIdentifier = "ph://"
internal func ensureFileDirectoryExists(_ fileUrl: URL) throws {
let directoryPath = fileUrl.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: directoryPath.path) {
throw DirectoryNotExistsException(directoryPath.path)
}
}
internal func readFileAsBase64(path: String, options: ReadingOptions) throws -> String {
let file = FileHandle(forReadingAtPath: path)
guard let file else {
throw FileNotExistsException(path)
}
if let position = options.position, position != 0 {
// TODO: Handle these errors?
try? file.seek(toOffset: UInt64(position))
}
if let length = options.length {
return file.readData(ofLength: length).base64EncodedString(options: .endLineWithLineFeed)
}
return file.readDataToEndOfFile().base64EncodedString(options: .endLineWithLineFeed)
}
internal func writeFileAsBase64(path: String, string: String) throws {
let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters)
if !FileManager.default.createFile(atPath: path, contents: data) {
throw FileWriteFailedException(path)
}
}
internal func removeFile(path: String, idempotent: Bool = false) throws {
if FileManager.default.fileExists(atPath: path) {
do {
try FileManager.default.removeItem(atPath: path)
} catch {
throw FileCannotDeleteException(path)
.causedBy(error)
}
} else if !idempotent {
throw FileCannotDeleteException(path)
.causedBy(FileNotExistsException(path))
}
}
internal func getResourceValues(from directory: URL?, forKeys: Set<URLResourceKey>) throws -> URLResourceValues? {
do {
return try directory?.resourceValues(forKeys: forKeys)
} catch {
throw CannotDetermineDiskCapacity().causedBy(error)
}
}
internal func ensurePathPermission(_ appContext: AppContext?, path: String, flag: EXFileSystemPermissionFlags) throws {
guard let permissionsManager: EXFilePermissionModuleInterface = appContext?.legacyModule(implementing: EXFilePermissionModuleInterface.self) else {
throw Exceptions.PermissionsModuleNotFound()
}
guard permissionsManager.getPathPermissions(path).contains(flag) else {
throw flag == .read ? FileNotReadableException(path) : FileNotWritableException(path)
}
}
internal func isPHAsset(path: String) -> Bool {
return path.contains(assetIdentifier)
}
internal func copyPHAsset(fromUrl: URL, toUrl: URL, with resourceManager: PHAssetResourceManager, promise: Promise) {
if isPhotoLibraryStatusAuthorized() {
if FileManager.default.fileExists(atPath: toUrl.path) {
promise.reject(FileAlreadyExistsException(toUrl.path))
return
}
let identifier = fromUrl.absoluteString.replacingOccurrences(of: assetIdentifier, with: "")
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else {
promise.reject(FailedToFindAssetException(fromUrl.absoluteString))
return
}
let firstResource = PHAssetResource.assetResources(for: asset).first
if let firstResource {
resourceManager.writeData(for: firstResource, toFile: toUrl, options: nil) { error in
if let error {
promise.reject(FailedToCopyAssetException(fromUrl.absoluteString))
}
promise.resolve()
}
} else {
promise.reject(FailedToCopyAssetException(fromUrl.absoluteString))
}
}
}
internal func isPhotoLibraryStatusAuthorized() -> Bool {
if #available(iOS 14, tvOS 14, *) {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
return status == .authorized || status == .limited
}
return PHPhotoLibrary.authorizationStatus() == .authorized
}

View File

@@ -0,0 +1,278 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
import Photos
private let EVENT_DOWNLOAD_PROGRESS = "expo-file-system.downloadProgress"
private let EVENT_UPLOAD_PROGRESS = "expo-file-system.uploadProgress"
public final class FileSystemModule: Module {
private lazy var sessionTaskDispatcher = EXSessionTaskDispatcher(sessionHandler: ExpoAppDelegate.getSubscriberOfType(FileSystemBackgroundSessionHandler.self))
private lazy var taskHandlersManager = EXTaskHandlersManager()
private lazy var resourceManager = PHAssetResourceManager()
private lazy var backgroundSession = createUrlSession(type: .background, delegate: sessionTaskDispatcher)
private lazy var foregroundSession = createUrlSession(type: .foreground, delegate: sessionTaskDispatcher)
private var documentDirectory: URL? {
return appContext?.config.documentDirectory
}
private var cacheDirectory: URL? {
return appContext?.config.cacheDirectory
}
// swiftlint:disable:next cyclomatic_complexity
public func definition() -> ModuleDefinition {
Name("ExponentFileSystem")
Constants {
return [
"documentDirectory": documentDirectory?.absoluteString,
"cacheDirectory": cacheDirectory?.absoluteString,
"bundleDirectory": Bundle.main.bundlePath
]
}
Events(EVENT_DOWNLOAD_PROGRESS, EVENT_UPLOAD_PROGRESS)
AsyncFunction("getInfoAsync") { (url: URL, options: InfoOptions, promise: Promise) in
switch url.scheme {
case "file":
EXFileSystemLocalFileHandler.getInfoForFile(url, withOptions: options.toDictionary(), resolver: promise.resolver, rejecter: promise.legacyRejecter)
case "assets-library", "ph":
EXFileSystemAssetLibraryHandler.getInfoForFile(url, withOptions: options.toDictionary(), resolver: promise.resolver, rejecter: promise.legacyRejecter)
default:
throw UnsupportedSchemeException(url.scheme)
}
}
AsyncFunction("readAsStringAsync") { (url: URL, options: ReadingOptions) -> String in
try ensurePathPermission(appContext, path: url.path, flag: .read)
if options.encoding == .base64 {
return try readFileAsBase64(path: url.path, options: options)
}
do {
return try String(contentsOfFile: url.path, encoding: options.encoding.toStringEncoding() ?? .utf8)
} catch {
throw FileNotReadableException(url.path)
}
}
AsyncFunction("writeAsStringAsync") { (url: URL, string: String, options: WritingOptions) in
try ensurePathPermission(appContext, path: url.path, flag: .write)
if options.encoding == .base64 {
try writeFileAsBase64(path: url.path, string: string)
return
}
do {
try string.write(toFile: url.path, atomically: true, encoding: options.encoding.toStringEncoding() ?? .utf8)
} catch {
throw FileNotWritableException(url.path)
.causedBy(error)
}
}
AsyncFunction("deleteAsync") { (url: URL, options: DeletingOptions) in
guard url.isFileURL else {
throw InvalidFileUrlException(url)
}
try ensurePathPermission(appContext, path: url.appendingPathComponent("..").path, flag: .write)
try removeFile(path: url.path, idempotent: options.idempotent)
}
AsyncFunction("moveAsync") { (options: RelocatingOptions) in
let (fromUrl, toUrl) = try options.asTuple()
guard fromUrl.isFileURL else {
throw InvalidFileUrlException(fromUrl)
}
guard toUrl.isFileURL else {
throw InvalidFileUrlException(toUrl)
}
try ensurePathPermission(appContext, path: fromUrl.appendingPathComponent("..").path, flag: .write)
try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
try removeFile(path: toUrl.path, idempotent: true)
try FileManager.default.moveItem(atPath: fromUrl.path, toPath: toUrl.path)
}
AsyncFunction("copyAsync") { (options: RelocatingOptions, promise: Promise) in
let (fromUrl, toUrl) = try options.asTuple()
if isPHAsset(path: fromUrl.absoluteString) {
copyPHAsset(fromUrl: fromUrl, toUrl: toUrl, with: resourceManager, promise: promise)
return
}
try ensurePathPermission(appContext, path: fromUrl.path, flag: .read)
try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
if fromUrl.scheme == "file" {
EXFileSystemLocalFileHandler.copy(from: fromUrl, to: toUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
} else if ["ph", "assets-library"].contains(fromUrl.scheme) {
EXFileSystemAssetLibraryHandler.copy(from: fromUrl, to: toUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
} else {
throw InvalidFileUrlException(fromUrl)
}
}
AsyncFunction("makeDirectoryAsync") { (url: URL, options: MakeDirectoryOptions) in
guard url.isFileURL else {
throw InvalidFileUrlException(url)
}
try ensurePathPermission(appContext, path: url.path, flag: .write)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: options.intermediates, attributes: nil)
}
AsyncFunction("readDirectoryAsync") { (url: URL) -> [String] in
guard url.isFileURL else {
throw InvalidFileUrlException(url)
}
try ensurePathPermission(appContext, path: url.path, flag: .read)
return try FileManager.default.contentsOfDirectory(atPath: url.path)
}
AsyncFunction("downloadAsync") { (sourceUrl: URL, localUrl: URL, options: DownloadOptions, promise: Promise) in
try ensureFileDirectoryExists(localUrl)
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
if sourceUrl.isFileURL {
try ensurePathPermission(appContext, path: sourceUrl.path, flag: .read)
EXFileSystemLocalFileHandler.copy(from: sourceUrl, to: localUrl, resolver: promise.resolver, rejecter: promise.legacyRejecter)
return
}
let session = options.sessionType == .background ? backgroundSession : foregroundSession
let request = createUrlRequest(url: sourceUrl, headers: options.headers)
let downloadTask = session.downloadTask(with: request)
let taskDelegate = EXSessionDownloadTaskDelegate(
resolve: promise.resolver,
reject: promise.legacyRejecter,
localUrl: localUrl,
shouldCalculateMd5: options.md5
)
sessionTaskDispatcher.register(taskDelegate, for: downloadTask)
downloadTask.resume()
}
AsyncFunction("uploadAsync") { (targetUrl: URL, localUrl: URL, options: UploadOptions, promise: Promise) in
guard localUrl.isFileURL else {
throw InvalidFileUrlException(localUrl)
}
guard FileManager.default.fileExists(atPath: localUrl.path) else {
throw FileNotExistsException(localUrl.path)
}
let session = options.sessionType == .background ? backgroundSession : foregroundSession
let task = try createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
let taskDelegate = EXSessionUploadTaskDelegate(resolve: promise.resolver, reject: promise.legacyRejecter)
sessionTaskDispatcher.register(taskDelegate, for: task)
task.resume()
}
AsyncFunction("uploadTaskStartAsync") { (targetUrl: URL, localUrl: URL, uuid: String, options: UploadOptions, promise: Promise) in
let session = options.sessionType == .background ? backgroundSession : foregroundSession
let task = try createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
let onSend: EXUploadDelegateOnSendCallback = { [weak self] _, _, totalBytesSent, totalBytesExpectedToSend in
self?.sendEvent(EVENT_UPLOAD_PROGRESS, [
"uuid": uuid,
"data": [
"totalBytesSent": totalBytesSent,
"totalBytesExpectedToSend": totalBytesExpectedToSend
]
])
}
let taskDelegate = EXSessionCancelableUploadTaskDelegate(
resolve: promise.resolver,
reject: promise.legacyRejecter,
onSendCallback: onSend,
resumableManager: taskHandlersManager,
uuid: uuid
)
sessionTaskDispatcher.register(taskDelegate, for: task)
taskHandlersManager.register(task, uuid: uuid)
task.resume()
}
// swiftlint:disable:next line_length closure_body_length
AsyncFunction("downloadResumableStartAsync") { (sourceUrl: URL, localUrl: URL, uuid: String, options: DownloadOptions, resumeDataString: String?, promise: Promise) in
try ensureFileDirectoryExists(localUrl)
try ensurePathPermission(appContext, path: localUrl.path, flag: .write)
let session = options.sessionType == .background ? backgroundSession : foregroundSession
let resumeData = resumeDataString != nil ? Data(base64Encoded: resumeDataString ?? "") : nil
let onWrite: EXDownloadDelegateOnWriteCallback = { [weak self] _, _, totalBytesWritten, totalBytesExpectedToWrite in
self?.sendEvent(EVENT_DOWNLOAD_PROGRESS, [
"uuid": uuid,
"data": [
"totalBytesWritten": totalBytesWritten,
"totalBytesExpectedToWrite": totalBytesExpectedToWrite
]
])
}
let task: URLSessionDownloadTask
if let resumeDataString, let resumeData = Data(base64Encoded: resumeDataString) {
task = session.downloadTask(withResumeData: resumeData)
} else {
let request = createUrlRequest(url: sourceUrl, headers: options.headers)
task = session.downloadTask(with: request)
}
let taskDelegate = EXSessionResumableDownloadTaskDelegate(
resolve: promise.resolver,
reject: promise.legacyRejecter,
localUrl: localUrl,
shouldCalculateMd5: options.md5,
onWriteCallback: onWrite,
resumableManager: taskHandlersManager,
uuid: uuid
)
sessionTaskDispatcher.register(taskDelegate, for: task)
taskHandlersManager.register(task, uuid: uuid)
task.resume()
}
AsyncFunction("downloadResumablePauseAsync") { (id: String) -> [String: String?] in
guard let task = taskHandlersManager.downloadTask(forId: id) else {
throw DownloadTaskNotFoundException(id)
}
let resumeData = await task.cancelByProducingResumeData()
return [
"resumeData": resumeData?.base64EncodedString()
]
}
AsyncFunction("networkTaskCancelAsync") { (id: String) in
taskHandlersManager.task(forId: id)?.cancel()
}
AsyncFunction("getFreeDiskStorageAsync") { () -> Int in
// Uses required reason API based on the following reason: E174.1 85F4.1
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeAvailableCapacityKey])
guard let availableCapacity = resourceValues?.volumeAvailableCapacity else {
throw CannotDetermineDiskCapacity()
}
return availableCapacity
}
AsyncFunction("getTotalDiskCapacityAsync") { () -> Int in
// Uses required reason API based on the following reason: E174.1 85F4.1
let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeTotalCapacityKey])
guard let totalCapacity = resourceValues?.volumeTotalCapacity else {
throw CannotDetermineDiskCapacity()
}
return totalCapacity
}
}
}

View File

@@ -0,0 +1,74 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
struct InfoOptions: Record {
@Field var md5: Bool = false
@Field var size: Bool = false
}
struct ReadingOptions: Record {
@Field var encoding: Encoding = .utf8
@Field var position: Int?
@Field var length: Int?
}
struct WritingOptions: Record {
@Field var encoding: Encoding = .utf8
}
struct DeletingOptions: Record {
@Field var idempotent: Bool = false
}
struct RelocatingOptions: Record {
@Field var from: URL?
@Field var to: URL?
func asTuple() throws -> (URL, URL) {
guard let from, let to else {
let missingOptionName = from == nil ? "from" : "to"
throw Exception(name: "MissingParameterException", description: "Missing option '\(missingOptionName)'")
}
return (from, to)
}
}
struct MakeDirectoryOptions: Record {
@Field var intermediates: Bool = false
}
struct DownloadOptions: Record {
@Field var md5: Bool = false
@Field var cache: Bool = false
@Field var headers: [String: String]?
@Field var sessionType: SessionType = .background
}
struct UploadOptions: Record {
@Field var headers: [String: String]?
@Field var httpMethod: HttpMethod = .post
@Field var sessionType: SessionType = .background
@Field var uploadType: UploadType = .binaryContent
// Multipart
@Field var fieldName: String?
@Field var mimeType: String?
@Field var parameters: [String: String]?
}
enum SessionType: Int, Enumerable {
case background = 0
case foreground = 1
}
enum HttpMethod: String, Enumerable {
case post = "POST"
case put = "PUT"
case patch = "PATCH"
}
enum UploadType: Int, Enumerable {
case binaryContent = 0
case multipart = 1
}

View File

@@ -0,0 +1,9 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
@interface NSData (EXFileSystem)
- (NSString *)md5String;
@end

View File

@@ -0,0 +1,19 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <ExpoFileSystem/NSData+EXFileSystem.h>
#import <CommonCrypto/CommonDigest.h>
@implementation NSData (EXFileSystem)
- (NSString *)md5String
{
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(self.bytes, (CC_LONG) self.length, digest);
NSMutableString *md5 = [NSMutableString stringWithCapacity:2 * CC_MD5_DIGEST_LENGTH];
for (unsigned int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) {
[md5 appendFormat:@"%02x", digest[i]];
}
return md5;
}
@end

View File

@@ -0,0 +1,98 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import CoreServices
import ExpoModulesCore
func findMimeType(forAttachment attachment: URL) -> String {
if let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, attachment.pathExtension as CFString, nil)?.takeRetainedValue() {
if let type = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() {
return type as String
}
}
return "application/octet-stream"
}
func createUrlSession(type: SessionType, delegate: URLSessionDelegate) -> URLSession {
let configuration = type == .foreground ? URLSessionConfiguration.default : URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
return URLSession(configuration: configuration, delegate: delegate, delegateQueue: .main)
}
func createUrlRequest(url: URL, headers: [String: String]?) -> URLRequest {
var request = URLRequest(url: url)
if let headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
return request
}
func createUploadTask(session: URLSession, targetUrl: URL, sourceUrl: URL, options: UploadOptions) throws -> URLSessionUploadTask {
var request = createUrlRequest(url: targetUrl, headers: options.headers)
request.httpMethod = options.httpMethod.rawValue
switch options.uploadType {
case .binaryContent:
return session.uploadTask(with: request, fromFile: sourceUrl)
case .multipart:
let boundaryString = UUID().uuidString
guard let data = createMultipartBody(boundary: boundaryString, sourceUrl: sourceUrl, options: options) else {
throw FailedToCreateBodyException()
}
request.setValue("multipart/form-data; boundary=\(boundaryString)", forHTTPHeaderField: "Content-Type")
let localURL = try createLocalUrl(from: sourceUrl)
try? data.write(to: localURL)
return session.uploadTask(with: request, fromFile: localURL)
}
}
func createLocalUrl(from sourceUrl: URL) throws -> URL {
guard let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
throw FailedToAccessDirectoryException()
}
let tempDir = cachesDir.appendingPathComponent("uploads")
FileSystemUtilities.ensureDirExists(at: tempDir)
return tempDir.appendingPathComponent(sourceUrl.lastPathComponent)
}
func createMultipartBody(boundary: String, sourceUrl: URL, options: UploadOptions) -> Data? {
let fieldName = options.fieldName ?? sourceUrl.lastPathComponent
var mimeType = options.mimeType ?? findMimeType(forAttachment: sourceUrl)
guard let data = try? Data(contentsOf: sourceUrl) else {
return nil
}
var body = Data()
headersForMultipartParams(options.parameters, boundary: boundary, body: &body)
body.append("--\(boundary)\r\n".data)
body.append("Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(sourceUrl.lastPathComponent)\"\r\n".data)
body.append("Content-Type: \(mimeType)\r\n\r\n".data)
body.append(data)
body.append("\r\n".data)
body.append("--\(boundary)--\r\n".data)
return body
}
func headersForMultipartParams(_ params: [String: String]?, boundary: String, body: inout Data) {
if let params {
for (key, value) in params {
body.append("--\(boundary)\r\n".data)
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data)
body.append("\(value)\r\n".data)
}
}
}
// All swift strings are unicode correct.
// This avoids the optional created by string.data(using: .utf8)
private extension String {
var data: Data { Data(self.utf8) }
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyCollectedDataTypes</key>
<array>
</array>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
<string>3B52.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
<string>85F4.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,55 @@
import ExpoModulesTestCore
@testable import ExpoModulesCore
@testable import ExpoFileSystem
class EXFileSystemSpec: ExpoSpec {
override class func spec() {
let fileSystem = EXFileSystem()
describe("percentEncodedURLFromURIString") {
it("should handle encoded URIs") {
let encodedUriInput = "file:///var/mobile/@username/branch"
let encodedUriExpectedOutput = "file:///var/mobile/@username/branch"
let encodedUri = fileSystem.percentEncodedURL(fromURIString: encodedUriInput)
expect(encodedUri?.absoluteString) == encodedUriExpectedOutput
expect(encodedUri?.scheme) == "file"
}
it("should handle UTF-8 characters") {
let utf8UriInput = "file:///var/mobile/中文"
let utf8UriExpectedOutput = "file:///var/mobile/%E4%B8%AD%E6%96%87"
let utf8Uri = fileSystem.percentEncodedURL(fromURIString: utf8UriInput)
expect(utf8Uri?.absoluteString) == utf8UriExpectedOutput
expect(utf8Uri?.scheme) == "file"
}
it("should handle URI with percent, numbers and UTF-8 characters") {
let input = "file:///document/directory/%40%2F中文"
let expectedOutput = "file:///document/directory/%40%2F%E4%B8%AD%E6%96%87"
let uri = fileSystem.percentEncodedURL(fromURIString: input)
expect(uri?.absoluteString) == expectedOutput
}
it("should not decode percentages in URI") {
let input = "file:///document/hello%2Fworld.txt"
let unexpectedOutput = "file:///document/hello/world.txt"
let uri = fileSystem.percentEncodedURL(fromURIString: input)
// Should not create a directory named "hello"
expect(uri?.absoluteString) != unexpectedOutput
}
it("should handle assets-library URIs") {
let assetsLibraryUriInput = "assets-library://asset/asset.JPG?id=3C1D9C54-9521-488F-BB27-AA1EA0F8AF04/L0/001&ext=JPG"
let assetsLibraryUri = fileSystem.percentEncodedURL(fromURIString: assetsLibraryUriInput)
expect(assetsLibraryUri?.absoluteString) == assetsLibraryUriInput
expect(assetsLibraryUri?.scheme) == "assets-library"
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"name": "expo-file-system",
"version": "17.0.1",
"description": "Provides access to the local file system on the device.",
"main": "build/index.js",
"types": "build/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "expo-module build",
"clean": "expo-module clean",
"lint": "expo-module lint",
"test": "expo-module test",
"prepare": "expo-module prepare",
"prepublishOnly": "expo-module prepublishOnly",
"expo-module": "expo-module"
},
"keywords": [
"react-native",
"expo",
"file-system",
"file"
],
"repository": {
"type": "git",
"url": "https://github.com/expo/expo.git",
"directory": "packages/expo-file-system"
},
"bugs": {
"url": "https://github.com/expo/expo/issues"
},
"author": "650 Industries, Inc.",
"license": "MIT",
"homepage": "https://docs.expo.dev/versions/latest/sdk/filesystem/",
"jest": {
"preset": "expo-module-scripts"
},
"devDependencies": {
"expo-module-scripts": "^3.0.0",
"jest-expo": "~51.0.0-unreleased"
},
"peerDependencies": {
"expo": "*"
},
"gitHead": "ee4f30ef3b5fa567ad1bf94794197f7683fdd481"
}

View File

@@ -0,0 +1,3 @@
import { ConfigPlugin } from 'expo/config-plugins';
declare const _default: ConfigPlugin<void>;
export default _default;

View File

@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const config_plugins_1 = require("expo/config-plugins");
const pkg = require('expo-file-system/package.json');
const withFileSystem = (config) => {
return config_plugins_1.AndroidConfig.Permissions.withPermissions(config, [
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE',
'android.permission.INTERNET',
]);
};
exports.default = (0, config_plugins_1.createRunOncePlugin)(withFileSystem, pkg.name, pkg.version);

View File

@@ -0,0 +1,13 @@
import { AndroidConfig, ConfigPlugin, createRunOncePlugin } from 'expo/config-plugins';
const pkg = require('expo-file-system/package.json');
const withFileSystem: ConfigPlugin = (config) => {
return AndroidConfig.Permissions.withPermissions(config, [
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE',
'android.permission.INTERNET',
]);
};
export default createRunOncePlugin(withFileSystem, pkg.name, pkg.version);

View File

@@ -0,0 +1,9 @@
{
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}

View File

@@ -0,0 +1,7 @@
import { requireOptionalNativeModule } from 'expo-modules-core';
import ExponentFileSystemShim from './ExponentFileSystemShim';
import { ExponentFileSystemModule } from './types';
export default requireOptionalNativeModule<ExponentFileSystemModule>('ExponentFileSystem') ??
ExponentFileSystemShim;

View File

@@ -0,0 +1,2 @@
import ExponentFileSystemShim from './ExponentFileSystemShim';
export default ExponentFileSystemShim;

View File

@@ -0,0 +1,17 @@
import { ExponentFileSystemModule } from './types';
const platformModule: ExponentFileSystemModule = {
get documentDirectory(): string | null {
return null;
},
get cacheDirectory(): string | null {
return null;
},
get bundleDirectory(): string | null {
return null;
},
addListener(eventName: string): void {},
removeListeners(count: number): void {},
};
export default platformModule;

View File

@@ -0,0 +1,758 @@
import { EventEmitter, Subscription, UnavailabilityError, uuid } from 'expo-modules-core';
import { Platform } from 'react-native';
import ExponentFileSystem from './ExponentFileSystem';
import {
DownloadOptions,
DownloadPauseState,
FileSystemNetworkTaskProgressCallback,
DownloadProgressData,
UploadProgressData,
FileInfo,
FileSystemAcceptedUploadHttpMethod,
FileSystemDownloadResult,
FileSystemRequestDirectoryPermissionsResult,
FileSystemSessionType,
FileSystemUploadOptions,
FileSystemUploadResult,
FileSystemUploadType,
ProgressEvent,
ReadingOptions,
WritingOptions,
DeletingOptions,
InfoOptions,
RelocatingOptions,
MakeDirectoryOptions,
} from './FileSystem.types';
if (!ExponentFileSystem) {
console.warn(
"No native ExponentFileSystem module found, are you sure the expo-file-system's module is linked properly?"
);
}
// Prevent webpack from pruning this.
const _unused = new EventEmitter(ExponentFileSystem); // eslint-disable-line
function normalizeEndingSlash(p: string | null): string | null {
if (p != null) {
return p.replace(/\/*$/, '') + '/';
}
return null;
}
/**
* `file://` URI pointing to the directory where user documents for this app will be stored.
* Files stored here will remain until explicitly deleted by the app. Ends with a trailing `/`.
* Example uses are for files the user saves that they expect to see again.
*/
export const documentDirectory = normalizeEndingSlash(ExponentFileSystem.documentDirectory);
/**
* `file://` URI pointing to the directory where temporary files used by this app will be stored.
* Files stored here may be automatically deleted by the system when low on storage.
* Example uses are for downloaded or generated files that the app just needs for one-time usage.
*/
export const cacheDirectory = normalizeEndingSlash(ExponentFileSystem.cacheDirectory);
/**
* URI to the directory where assets bundled with the application are stored.
*/
export const bundleDirectory = normalizeEndingSlash(ExponentFileSystem.bundleDirectory);
/**
* Get metadata information about a file, directory or external content/asset.
* @param fileUri URI to the file or directory. See [supported URI schemes](#supported-uri-schemes).
* @param options A map of options represented by [`InfoOptions`](#infooptions) type.
* @return A Promise that resolves to a `FileInfo` object. If no item exists at this URI,
* the returned Promise resolves to `FileInfo` object in form of `{ exists: false, isDirectory: false }`.
*/
export async function getInfoAsync(fileUri: string, options: InfoOptions = {}): Promise<FileInfo> {
if (!ExponentFileSystem.getInfoAsync) {
throw new UnavailabilityError('expo-file-system', 'getInfoAsync');
}
return await ExponentFileSystem.getInfoAsync(fileUri, options);
}
/**
* Read the entire contents of a file as a string. Binary will be returned in raw format, you will need to append `data:image/png;base64,` to use it as Base64.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* @param options A map of read options represented by [`ReadingOptions`](#readingoptions) type.
* @return A Promise that resolves to a string containing the entire contents of the file.
*/
export async function readAsStringAsync(
fileUri: string,
options: ReadingOptions = {}
): Promise<string> {
if (!ExponentFileSystem.readAsStringAsync) {
throw new UnavailabilityError('expo-file-system', 'readAsStringAsync');
}
return await ExponentFileSystem.readAsStringAsync(fileUri, options);
}
/**
* Takes a `file://` URI and converts it into content URI (`content://`) so that it can be accessed by other applications outside of Expo.
* @param fileUri The local URI of the file. If there is no file at this URI, an exception will be thrown.
* @example
* ```js
* FileSystem.getContentUriAsync(uri).then(cUri => {
* console.log(cUri);
* IntentLauncher.startActivityAsync('android.intent.action.VIEW', {
* data: cUri,
* flags: 1,
* });
* });
* ```
* @return Returns a Promise that resolves to a `string` containing a `content://` URI pointing to the file.
* The URI is the same as the `fileUri` input parameter but in a different format.
* @platform android
*/
export async function getContentUriAsync(fileUri: string): Promise<string> {
if (Platform.OS === 'android') {
if (!ExponentFileSystem.getContentUriAsync) {
throw new UnavailabilityError('expo-file-system', 'getContentUriAsync');
}
return await ExponentFileSystem.getContentUriAsync(fileUri);
} else {
return fileUri;
}
}
/**
* Write the entire contents of a file as a string.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* > Note: when you're using SAF URI the file needs to exist. You can't create a new file.
* @param contents The string to replace the contents of the file with.
* @param options A map of write options represented by [`WritingOptions`](#writingoptions) type.
*/
export async function writeAsStringAsync(
fileUri: string,
contents: string,
options: WritingOptions = {}
): Promise<void> {
if (!ExponentFileSystem.writeAsStringAsync) {
throw new UnavailabilityError('expo-file-system', 'writeAsStringAsync');
}
return await ExponentFileSystem.writeAsStringAsync(fileUri, contents, options);
}
/**
* Delete a file or directory. If the URI points to a directory, the directory and all its contents are recursively deleted.
* @param fileUri `file://` or [SAF](#saf-uri) URI to the file or directory.
* @param options A map of write options represented by [`DeletingOptions`](#deletingoptions) type.
*/
export async function deleteAsync(fileUri: string, options: DeletingOptions = {}): Promise<void> {
if (!ExponentFileSystem.deleteAsync) {
throw new UnavailabilityError('expo-file-system', 'deleteAsync');
}
return await ExponentFileSystem.deleteAsync(fileUri, options);
}
export async function deleteLegacyDocumentDirectoryAndroid(): Promise<void> {
if (Platform.OS !== 'android' || documentDirectory == null) {
return;
}
const legacyDocumentDirectory = `${documentDirectory}ExperienceData/`;
return await deleteAsync(legacyDocumentDirectory, { idempotent: true });
}
/**
* Move a file or directory to a new location.
* @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
*/
export async function moveAsync(options: RelocatingOptions): Promise<void> {
if (!ExponentFileSystem.moveAsync) {
throw new UnavailabilityError('expo-file-system', 'moveAsync');
}
return await ExponentFileSystem.moveAsync(options);
}
/**
* Create a copy of a file or directory. Directories are recursively copied with all of their contents.
* It can be also used to copy content shared by other apps to local filesystem.
* @param options A map of move options represented by [`RelocatingOptions`](#relocatingoptions) type.
*/
export async function copyAsync(options: RelocatingOptions): Promise<void> {
if (!ExponentFileSystem.copyAsync) {
throw new UnavailabilityError('expo-file-system', 'copyAsync');
}
return await ExponentFileSystem.copyAsync(options);
}
/**
* Create a new empty directory.
* @param fileUri `file://` URI to the new directory to create.
* @param options A map of create directory options represented by [`MakeDirectoryOptions`](#makedirectoryoptions) type.
*/
export async function makeDirectoryAsync(
fileUri: string,
options: MakeDirectoryOptions = {}
): Promise<void> {
if (!ExponentFileSystem.makeDirectoryAsync) {
throw new UnavailabilityError('expo-file-system', 'makeDirectoryAsync');
}
return await ExponentFileSystem.makeDirectoryAsync(fileUri, options);
}
/**
* Enumerate the contents of a directory.
* @param fileUri `file://` URI to the directory.
* @return A Promise that resolves to an array of strings, each containing the name of a file or directory contained in the directory at `fileUri`.
*/
export async function readDirectoryAsync(fileUri: string): Promise<string[]> {
if (!ExponentFileSystem.readDirectoryAsync) {
throw new UnavailabilityError('expo-file-system', 'readDirectoryAsync');
}
return await ExponentFileSystem.readDirectoryAsync(fileUri);
}
/**
* Gets the available internal disk storage size, in bytes. This returns the free space on the data partition that hosts all of the internal storage for all apps on the device.
* @return Returns a Promise that resolves to the number of bytes available on the internal disk, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* if the capacity is greater than 2^53^ - 1 bytes.
*/
export async function getFreeDiskStorageAsync(): Promise<number> {
if (!ExponentFileSystem.getFreeDiskStorageAsync) {
throw new UnavailabilityError('expo-file-system', 'getFreeDiskStorageAsync');
}
return await ExponentFileSystem.getFreeDiskStorageAsync();
}
/**
* Gets total internal disk storage size, in bytes. This is the total capacity of the data partition that hosts all the internal storage for all apps on the device.
* @return Returns a Promise that resolves to a number that specifies the total internal disk storage capacity in bytes, or JavaScript's [`MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* if the capacity is greater than 2^53^ - 1 bytes.
*/
export async function getTotalDiskCapacityAsync(): Promise<number> {
if (!ExponentFileSystem.getTotalDiskCapacityAsync) {
throw new UnavailabilityError('expo-file-system', 'getTotalDiskCapacityAsync');
}
return await ExponentFileSystem.getTotalDiskCapacityAsync();
}
/**
* Download the contents at a remote URI to a file in the app's file system. The directory for a local file uri must exist prior to calling this function.
* @param uri The remote URI to download from.
* @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
* If there is a file at this URI, its contents are replaced. The directory for the file must exist.
* @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
* @example
* ```js
* FileSystem.downloadAsync(
* 'http://techslides.com/demos/sample-videos/small.mp4',
* FileSystem.documentDirectory + 'small.mp4'
* )
* .then(({ uri }) => {
* console.log('Finished downloading to ', uri);
* })
* .catch(error => {
* console.error(error);
* });
* ```
* @return Returns a Promise that resolves to a `FileSystemDownloadResult` object.
*/
export async function downloadAsync(
uri: string,
fileUri: string,
options: DownloadOptions = {}
): Promise<FileSystemDownloadResult> {
if (!ExponentFileSystem.downloadAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadAsync');
}
return await ExponentFileSystem.downloadAsync(uri, fileUri, {
sessionType: FileSystemSessionType.BACKGROUND,
...options,
});
}
/**
* Upload the contents of the file pointed by `fileUri` to the remote url.
* @param url The remote URL, where the file will be sent.
* @param fileUri The local URI of the file to send. The file must exist.
* @param options A map of download options represented by [`FileSystemUploadOptions`](#filesystemuploadoptions) type.
* @example
* **Client**
*
* ```js
* import * as FileSystem from 'expo-file-system';
*
* try {
* const response = await FileSystem.uploadAsync(`http://192.168.0.1:1234/binary-upload`, fileUri, {
* fieldName: 'file',
* httpMethod: 'PATCH',
* uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
* });
* console.log(JSON.stringify(response, null, 4));
* } catch (error) {
* console.log(error);
* }
* ```
*
* **Server**
*
* Please refer to the "[Server: Handling multipart requests](#server-handling-multipart-requests)" example - there is code for a simple Node.js server.
* @return Returns a Promise that resolves to `FileSystemUploadResult` object.
*/
export async function uploadAsync(
url: string,
fileUri: string,
options: FileSystemUploadOptions = {}
): Promise<FileSystemUploadResult> {
if (!ExponentFileSystem.uploadAsync) {
throw new UnavailabilityError('expo-file-system', 'uploadAsync');
}
return await ExponentFileSystem.uploadAsync(url, fileUri, {
sessionType: FileSystemSessionType.BACKGROUND,
uploadType: FileSystemUploadType.BINARY_CONTENT,
...options,
httpMethod: (options.httpMethod || 'POST').toUpperCase(),
});
}
/**
* Create a `DownloadResumable` object which can start, pause, and resume a download of contents at a remote URI to a file in the app's file system.
* > Note: You need to call `downloadAsync()`, on a `DownloadResumable` instance to initiate the download.
* The `DownloadResumable` object has a callback that provides download progress updates.
* Downloads can be resumed across app restarts by using `AsyncStorage` to store the `DownloadResumable.savable()` object for later retrieval.
* The `savable` object contains the arguments required to initialize a new `DownloadResumable` object to resume the download after an app restart.
* The directory for a local file uri must exist prior to calling this function.
* @param uri The remote URI to download from.
* @param fileUri The local URI of the file to download to. If there is no file at this URI, a new one is created.
* If there is a file at this URI, its contents are replaced. The directory for the file must exist.
* @param options A map of download options represented by [`DownloadOptions`](#downloadoptions) type.
* @param callback This function is called on each data write to update the download progress.
* > **Note**: When the app has been moved to the background, this callback won't be fired until it's moved to the foreground.
* @param resumeData The string which allows the api to resume a paused download. This is set on the `DownloadResumable` object automatically when a download is paused.
* When initializing a new `DownloadResumable` this should be `null`.
*/
export function createDownloadResumable(
uri: string,
fileUri: string,
options?: DownloadOptions,
callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>,
resumeData?: string
): DownloadResumable {
return new DownloadResumable(uri, fileUri, options, callback, resumeData);
}
export function createUploadTask(
url: string,
fileUri: string,
options?: FileSystemUploadOptions,
callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>
): UploadTask {
return new UploadTask(url, fileUri, options, callback);
}
export abstract class FileSystemCancellableNetworkTask<
T extends DownloadProgressData | UploadProgressData,
> {
private _uuid = uuid.v4();
protected taskWasCanceled = false;
private emitter = new EventEmitter(ExponentFileSystem);
private subscription?: Subscription | null;
// @docsMissing
public async cancelAsync(): Promise<void> {
if (!ExponentFileSystem.networkTaskCancelAsync) {
throw new UnavailabilityError('expo-file-system', 'networkTaskCancelAsync');
}
this.removeSubscription();
this.taskWasCanceled = true;
return await ExponentFileSystem.networkTaskCancelAsync(this.uuid);
}
protected isTaskCancelled(): boolean {
if (this.taskWasCanceled) {
console.warn('This task was already canceled.');
return true;
}
return false;
}
protected get uuid(): string {
return this._uuid;
}
protected abstract getEventName(): string;
protected abstract getCallback(): FileSystemNetworkTaskProgressCallback<T> | undefined;
protected addSubscription() {
if (this.subscription) {
return;
}
this.subscription = this.emitter.addListener(this.getEventName(), (event: ProgressEvent<T>) => {
if (event.uuid === this.uuid) {
const callback = this.getCallback();
if (callback) {
callback(event.data);
}
}
});
}
protected removeSubscription() {
if (!this.subscription) {
return;
}
this.emitter.removeSubscription(this.subscription);
this.subscription = null;
}
}
export class UploadTask extends FileSystemCancellableNetworkTask<UploadProgressData> {
private options: FileSystemUploadOptions;
constructor(
private url: string,
private fileUri: string,
options?: FileSystemUploadOptions,
private callback?: FileSystemNetworkTaskProgressCallback<UploadProgressData>
) {
super();
const httpMethod = (options?.httpMethod?.toUpperCase() ||
'POST') as FileSystemAcceptedUploadHttpMethod;
this.options = {
sessionType: FileSystemSessionType.BACKGROUND,
uploadType: FileSystemUploadType.BINARY_CONTENT,
...options,
httpMethod,
};
}
protected getEventName(): string {
return 'expo-file-system.uploadProgress';
}
protected getCallback(): FileSystemNetworkTaskProgressCallback<UploadProgressData> | undefined {
return this.callback;
}
// @docsMissing
public async uploadAsync(): Promise<FileSystemUploadResult | undefined> {
if (!ExponentFileSystem.uploadTaskStartAsync) {
throw new UnavailabilityError('expo-file-system', 'uploadTaskStartAsync');
}
if (this.isTaskCancelled()) {
return;
}
this.addSubscription();
const result = await ExponentFileSystem.uploadTaskStartAsync(
this.url,
this.fileUri,
this.uuid,
this.options
);
this.removeSubscription();
return result;
}
}
export class DownloadResumable extends FileSystemCancellableNetworkTask<DownloadProgressData> {
constructor(
private url: string,
private _fileUri: string,
private options: DownloadOptions = {},
private callback?: FileSystemNetworkTaskProgressCallback<DownloadProgressData>,
private resumeData?: string
) {
super();
}
public get fileUri(): string {
return this._fileUri;
}
protected getEventName(): string {
return 'expo-file-system.downloadProgress';
}
protected getCallback(): FileSystemNetworkTaskProgressCallback<DownloadProgressData> | undefined {
return this.callback;
}
/**
* Download the contents at a remote URI to a file in the app's file system.
* @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
*/
async downloadAsync(): Promise<FileSystemDownloadResult | undefined> {
if (!ExponentFileSystem.downloadResumableStartAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
}
if (this.isTaskCancelled()) {
return;
}
this.addSubscription();
return await ExponentFileSystem.downloadResumableStartAsync(
this.url,
this._fileUri,
this.uuid,
this.options,
this.resumeData
);
}
/**
* Pause the current download operation. `resumeData` is added to the `DownloadResumable` object after a successful pause operation.
* Returns an object that can be saved with `AsyncStorage` for future retrieval (the same object that is returned from calling `FileSystem.DownloadResumable.savable()`).
* @return Returns a Promise that resolves to `DownloadPauseState` object.
*/
async pauseAsync(): Promise<DownloadPauseState> {
if (!ExponentFileSystem.downloadResumablePauseAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadResumablePauseAsync');
}
if (this.isTaskCancelled()) {
return {
fileUri: this._fileUri,
options: this.options,
url: this.url,
};
}
const pauseResult = await ExponentFileSystem.downloadResumablePauseAsync(this.uuid);
this.removeSubscription();
if (pauseResult) {
this.resumeData = pauseResult.resumeData;
return this.savable();
} else {
throw new Error('Unable to generate a savable pause state');
}
}
/**
* Resume a paused download operation.
* @return Returns a Promise that resolves to `FileSystemDownloadResult` object, or to `undefined` when task was cancelled.
*/
async resumeAsync(): Promise<FileSystemDownloadResult | undefined> {
if (!ExponentFileSystem.downloadResumableStartAsync) {
throw new UnavailabilityError('expo-file-system', 'downloadResumableStartAsync');
}
if (this.isTaskCancelled()) {
return;
}
this.addSubscription();
return await ExponentFileSystem.downloadResumableStartAsync(
this.url,
this.fileUri,
this.uuid,
this.options,
this.resumeData
);
}
/**
* Method to get the object which can be saved with `AsyncStorage` for future retrieval.
* @returns Returns object in shape of `DownloadPauseState` type.
*/
savable(): DownloadPauseState {
return {
url: this.url,
fileUri: this.fileUri,
options: this.options,
resumeData: this.resumeData,
};
}
}
const baseReadAsStringAsync = readAsStringAsync;
const baseWriteAsStringAsync = writeAsStringAsync;
const baseDeleteAsync = deleteAsync;
const baseMoveAsync = moveAsync;
const baseCopyAsync = copyAsync;
/**
* The `StorageAccessFramework` is a namespace inside of the `expo-file-system` module, which encapsulates all functions which can be used with [SAF URIs](#saf-uri).
* You can read more about SAF in the [Android documentation](https://developer.android.com/guide/topics/providers/document-provider).
*
* @example
* # Basic Usage
*
* ```ts
* import { StorageAccessFramework } from 'expo-file-system';
*
* // Requests permissions for external directory
* const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
*
* if (permissions.granted) {
* // Gets SAF URI from response
* const uri = permissions.directoryUri;
*
* // Gets all files inside of selected directory
* const files = await StorageAccessFramework.readDirectoryAsync(uri);
* alert(`Files inside ${uri}:\n\n${JSON.stringify(files)}`);
* }
* ```
*
* # Migrating an album
*
* ```ts
* import * as MediaLibrary from 'expo-media-library';
* import * as FileSystem from 'expo-file-system';
* const { StorageAccessFramework } = FileSystem;
*
* async function migrateAlbum(albumName: string) {
* // Gets SAF URI to the album
* const albumUri = StorageAccessFramework.getUriForDirectoryInRoot(albumName);
*
* // Requests permissions
* const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync(albumUri);
* if (!permissions.granted) {
* return;
* }
*
* const permittedUri = permissions.directoryUri;
* // Checks if users selected the correct folder
* if (!permittedUri.includes(albumName)) {
* return;
* }
*
* const mediaLibraryPermissions = await MediaLibrary.requestPermissionsAsync();
* if (!mediaLibraryPermissions.granted) {
* return;
* }
*
* // Moves files from external storage to internal storage
* await StorageAccessFramework.moveAsync({
* from: permittedUri,
* to: FileSystem.documentDirectory!,
* });
*
* const outputDir = FileSystem.documentDirectory! + albumName;
* const migratedFiles = await FileSystem.readDirectoryAsync(outputDir);
*
* // Creates assets from local files
* const [newAlbumCreator, ...assets] = await Promise.all(
* migratedFiles.map<Promise<MediaLibrary.Asset>>(
* async fileName => await MediaLibrary.createAssetAsync(outputDir + '/' + fileName)
* )
* );
*
* // Album was empty
* if (!newAlbumCreator) {
* return;
* }
*
* // Creates a new album in the scoped directory
* const newAlbum = await MediaLibrary.createAlbumAsync(albumName, newAlbumCreator, false);
* if (assets.length) {
* await MediaLibrary.addAssetsToAlbumAsync(assets, newAlbum, false);
* }
* }
* ```
* @platform Android
*/
export namespace StorageAccessFramework {
/**
* Gets a [SAF URI](#saf-uri) pointing to a folder in the Android root directory. You can use this function to get URI for
* `StorageAccessFramework.requestDirectoryPermissionsAsync()` when you trying to migrate an album. In that case, the name of the album is the folder name.
* @param folderName The name of the folder which is located in the Android root directory.
* @return Returns a [SAF URI](#saf-uri) to a folder.
*/
export function getUriForDirectoryInRoot(folderName: string) {
return `content://com.android.externalstorage.documents/tree/primary:${folderName}/document/primary:${folderName}`;
}
/**
* Allows users to select a specific directory, granting your app access to all of the files and sub-directories within that directory.
* @param initialFileUrl The [SAF URI](#saf-uri) of the directory that the file picker should display when it first loads.
* If URI is incorrect or points to a non-existing folder, it's ignored.
* @platform android 11+
* @return Returns a Promise that resolves to `FileSystemRequestDirectoryPermissionsResult` object.
*/
export async function requestDirectoryPermissionsAsync(
initialFileUrl: string | null = null
): Promise<FileSystemRequestDirectoryPermissionsResult> {
if (!ExponentFileSystem.requestDirectoryPermissionsAsync) {
throw new UnavailabilityError(
'expo-file-system',
'StorageAccessFramework.requestDirectoryPermissionsAsync'
);
}
return await ExponentFileSystem.requestDirectoryPermissionsAsync(initialFileUrl);
}
/**
* Enumerate the contents of a directory.
* @param dirUri [SAF](#saf-uri) URI to the directory.
* @return A Promise that resolves to an array of strings, each containing the full [SAF URI](#saf-uri) of a file or directory contained in the directory at `fileUri`.
*/
export async function readDirectoryAsync(dirUri: string): Promise<string[]> {
if (!ExponentFileSystem.readSAFDirectoryAsync) {
throw new UnavailabilityError(
'expo-file-system',
'StorageAccessFramework.readDirectoryAsync'
);
}
return await ExponentFileSystem.readSAFDirectoryAsync(dirUri);
}
/**
* Creates a new empty directory.
* @param parentUri The [SAF](#saf-uri) URI to the parent directory.
* @param dirName The name of new directory.
* @return A Promise that resolves to a [SAF URI](#saf-uri) to the created directory.
*/
export async function makeDirectoryAsync(parentUri: string, dirName: string): Promise<string> {
if (!ExponentFileSystem.makeSAFDirectoryAsync) {
throw new UnavailabilityError(
'expo-file-system',
'StorageAccessFramework.makeDirectoryAsync'
);
}
return await ExponentFileSystem.makeSAFDirectoryAsync(parentUri, dirName);
}
/**
* Creates a new empty file.
* @param parentUri The [SAF](#saf-uri) URI to the parent directory.
* @param fileName The name of new file **without the extension**.
* @param mimeType The MIME type of new file.
* @return A Promise that resolves to a [SAF URI](#saf-uri) to the created file.
*/
export async function createFileAsync(
parentUri: string,
fileName: string,
mimeType: string
): Promise<string> {
if (!ExponentFileSystem.createSAFFileAsync) {
throw new UnavailabilityError('expo-file-system', 'StorageAccessFramework.createFileAsync');
}
return await ExponentFileSystem.createSAFFileAsync(parentUri, fileName, mimeType);
}
/**
* Alias for [`writeAsStringAsync`](#filesystemwriteasstringasyncfileuri-contents-options) method.
*/
export const writeAsStringAsync = baseWriteAsStringAsync;
/**
* Alias for [`readAsStringAsync`](#filesystemreadasstringasyncfileuri-options) method.
*/
export const readAsStringAsync = baseReadAsStringAsync;
/**
* Alias for [`deleteAsync`](#filesystemdeleteasyncfileuri-options) method.
*/
export const deleteAsync = baseDeleteAsync;
/**
* Alias for [`moveAsync`](#filesystemmoveasyncoptions) method.
*/
export const moveAsync = baseMoveAsync;
/**
* Alias for [`copyAsync`](#filesystemcopyasyncoptions) method.
*/
export const copyAsync = baseCopyAsync;
}

View File

@@ -0,0 +1,339 @@
/**
* These values can be used to define how sessions work on iOS.
* @platform ios
*/
export enum FileSystemSessionType {
/**
* Using this mode means that the downloading/uploading session on the native side will work even if the application is moved to background.
* If the task completes while the application is in background, the Promise will be either resolved immediately or (if the application execution has already been stopped) once the app is moved to foreground again.
* > Note: The background session doesn't fail if the server or your connection is down. Rather, it continues retrying until the task succeeds or is canceled manually.
*/
BACKGROUND = 0,
/**
* Using this mode means that downloading/uploading session on the native side will be terminated once the application becomes inactive (e.g. when it goes to background).
* Bringing the application to foreground again would trigger Promise rejection.
*/
FOREGROUND = 1,
}
export enum FileSystemUploadType {
/**
* The file will be sent as a request's body. The request can't contain additional data.
*/
BINARY_CONTENT = 0,
/**
* An [RFC 2387-compliant](https://www.ietf.org/rfc/rfc2387.txt) request body. The provided file will be encoded into HTTP request.
* This request can contain additional data represented by [`UploadOptionsMultipart`](#uploadoptionsmultipart) type.
*/
MULTIPART = 1,
}
export type DownloadOptions = {
/**
* If `true`, include the MD5 hash of the file in the returned object. Provided for convenience since it is common to check the integrity of a file immediately after downloading.
* @default false
*/
md5?: boolean;
// @docsMissing
cache?: boolean;
/**
* An object containing all the HTTP header fields and their values for the download network request. The keys and values of the object are the header names and values respectively.
*/
headers?: Record<string, string>;
/**
* A session type. Determines if tasks can be handled in the background. On Android, sessions always work in the background and you can't change it.
* @default FileSystemSessionType.BACKGROUND
* @platform ios
*/
sessionType?: FileSystemSessionType;
};
export type FileSystemHttpResult = {
/**
* An object containing all the HTTP response header fields and their values for the download network request.
* The keys and values of the object are the header names and values respectively.
*/
headers: Record<string, string>;
/**
* The HTTP response status code for the download network request.
*/
status: number;
// @docsMissing
mimeType: string | null;
};
export type FileSystemDownloadResult = FileSystemHttpResult & {
/**
* A `file://` URI pointing to the file. This is the same as the `fileUri` input parameter.
*/
uri: string;
/**
* Present if the `md5` option was truthy. Contains the MD5 hash of the file.
*/
md5?: string;
};
/**
* @deprecated Use `FileSystemDownloadResult` instead.
*/
export type DownloadResult = FileSystemDownloadResult;
export type FileSystemUploadOptions = (UploadOptionsBinary | UploadOptionsMultipart) & {
/**
* An object containing all the HTTP header fields and their values for the upload network request.
* The keys and values of the object are the header names and values respectively.
*/
headers?: Record<string, string>;
/**
* The request method.
* @default FileSystemAcceptedUploadHttpMethod.POST
*/
httpMethod?: FileSystemAcceptedUploadHttpMethod;
/**
* A session type. Determines if tasks can be handled in the background. On Android, sessions always work in the background and you can't change it.
* @default FileSystemSessionType.BACKGROUND
* @platform ios
*/
sessionType?: FileSystemSessionType;
};
/**
* Upload options when upload type is set to binary.
*/
export type UploadOptionsBinary = {
/**
* Upload type determines how the file will be sent to the server.
* Value will be `FileSystemUploadType.BINARY_CONTENT`.
*/
uploadType?: FileSystemUploadType;
};
/**
* Upload options when upload type is set to multipart.
*/
export type UploadOptionsMultipart = {
/**
* Upload type determines how the file will be sent to the server.
* Value will be `FileSystemUploadType.MULTIPART`.
*/
uploadType: FileSystemUploadType;
/**
* The name of the field which will hold uploaded file. Defaults to the file name without an extension.
*/
fieldName?: string;
/**
* The MIME type of the provided file. If not provided, the module will try to guess it based on the extension.
*/
mimeType?: string;
/**
* Additional form properties. They will be located in the request body.
*/
parameters?: Record<string, string>;
};
export type FileSystemUploadResult = FileSystemHttpResult & {
/**
* The body of the server response.
*/
body: string;
};
// @docsMissing
export type FileSystemNetworkTaskProgressCallback<
T extends DownloadProgressData | UploadProgressData,
> = (data: T) => void;
/**
* @deprecated use `FileSystemNetworkTaskProgressCallback<DownloadProgressData>` instead.
*/
export type DownloadProgressCallback = FileSystemNetworkTaskProgressCallback<DownloadProgressData>;
export type DownloadProgressData = {
/**
* The total bytes written by the download operation.
*/
totalBytesWritten: number;
/**
* The total bytes expected to be written by the download operation. A value of `-1` means that the server did not return the `Content-Length` header
* and the total size is unknown. Without this header, you won't be able to track the download progress.
*/
totalBytesExpectedToWrite: number;
};
export type UploadProgressData = {
/**
* The total bytes sent by the upload operation.
*/
totalBytesSent: number;
/**
* The total bytes expected to be sent by the upload operation.
*/
totalBytesExpectedToSend: number;
};
export type DownloadPauseState = {
/**
* The remote URI to download from.
*/
url: string;
/**
* The local URI of the file to download to. If there is no file at this URI, a new one is created. If there is a file at this URI, its contents are replaced.
*/
fileUri: string;
/**
* Object representing the file download options.
*/
options: DownloadOptions;
/**
* The string which allows the API to resume a paused download.
*/
resumeData?: string;
};
/* eslint-disable */
export type FileInfo =
/**
* Object returned when file exist.
*/
{
/**
* Signifies that the requested file exist.
*/
exists: true;
/**
* A `file://` URI pointing to the file. This is the same as the `fileUri` input parameter.
*/
uri: string;
/**
* The size of the file in bytes. If operating on a source such as an iCloud file, only present if the `size` option was truthy.
*/
size: number;
/**
* Boolean set to `true` if this is a directory and `false` if it is a file.
*/
isDirectory: boolean;
/**
* The last modification time of the file expressed in seconds since epoch.
*/
modificationTime: number;
/**
* Present if the `md5` option was truthy. Contains the MD5 hash of the file.
*/
md5?: string;
} |
/**
* Object returned when file do not exist.
*/
{
exists: false;
uri: string;
isDirectory: false;
};
/* eslint-enable */
/**
* These values can be used to define how file system data is read / written.
*/
export enum EncodingType {
/**
* Standard encoding format.
*/
UTF8 = 'utf8',
/**
* Binary, radix-64 representation.
*/
Base64 = 'base64',
}
// @docsMissing
export type FileSystemAcceptedUploadHttpMethod = 'POST' | 'PUT' | 'PATCH';
export type ReadingOptions = {
/**
* The encoding format to use when reading the file.
* @default EncodingType.UTF8
*/
encoding?: EncodingType | 'utf8' | 'base64';
/**
* Optional number of bytes to skip. This option is only used when `encoding: FileSystem.EncodingType.Base64` and `length` is defined.
* */
position?: number;
/**
* Optional number of bytes to read. This option is only used when `encoding: FileSystem.EncodingType.Base64` and `position` is defined.
*/
length?: number;
};
export type WritingOptions = {
/**
* The encoding format to use when writing the file.
* @default FileSystem.EncodingType.UTF8
*/
encoding?: EncodingType | 'utf8' | 'base64';
};
export type DeletingOptions = {
/**
* If `true`, don't throw an error if there is no file or directory at this URI.
* @default false
*/
idempotent?: boolean;
};
export type InfoOptions = {
/**
* Whether to return the MD5 hash of the file.
* @default false
*/
md5?: boolean;
/**
* Explicitly specify that the file size should be included. For example, skipping this can prevent downloading the file if it's stored in iCloud.
* The size is always returned for `file://` locations.
*/
size?: boolean;
};
export type RelocatingOptions = {
/**
* URI or [SAF](#saf-uri) URI to the asset, file, or directory. See [supported URI schemes](#supported-uri-schemes-1).
*/
from: string;
/**
* `file://` URI to the file or directory which should be its new location.
*/
to: string;
};
export type MakeDirectoryOptions = {
/**
* If `true`, don't throw an error if there is no file or directory at this URI.
* @default false
*/
intermediates?: boolean;
};
// @docsMissing
export type ProgressEvent<T> = {
uuid: string;
data: T;
};
/* eslint-disable */
export type FileSystemRequestDirectoryPermissionsResult =
/**
* If the permissions were not granted.
*/
{
granted: false;
} |
/**
* If the permissions were granted.
*/
{
granted: true;
/**
* The [SAF URI](#saf-uri) to the user's selected directory. Available only if permissions were granted.
*/
directoryUri: string;
};
/* eslint-enable */

View File

@@ -0,0 +1,2 @@
export * from './FileSystem';
export * from './FileSystem.types';

View File

@@ -0,0 +1,32 @@
type PlatformMethod = (...args: any[]) => Promise<any>;
export interface ExponentFileSystemModule {
readonly documentDirectory: string | null;
readonly cacheDirectory: string | null;
readonly bundleDirectory: string | null;
readonly getInfoAsync?: PlatformMethod;
readonly readAsStringAsync?: PlatformMethod;
readonly writeAsStringAsync?: PlatformMethod;
readonly deleteAsync?: PlatformMethod;
readonly moveAsync?: PlatformMethod;
readonly copyAsync?: PlatformMethod;
readonly makeDirectoryAsync?: PlatformMethod;
readonly readDirectoryAsync?: PlatformMethod;
readonly downloadAsync?: PlatformMethod;
readonly uploadAsync?: PlatformMethod;
readonly downloadResumableStartAsync?: PlatformMethod;
readonly downloadResumablePauseAsync?: PlatformMethod;
readonly getContentUriAsync?: PlatformMethod;
readonly getFreeDiskStorageAsync?: PlatformMethod;
readonly getTotalDiskCapacityAsync?: PlatformMethod;
readonly requestDirectoryPermissionsAsync?: PlatformMethod;
readonly readSAFDirectoryAsync?: PlatformMethod;
readonly makeSAFDirectoryAsync?: PlatformMethod;
readonly createSAFFileAsync?: PlatformMethod;
readonly networkTaskCancelAsync?: PlatformMethod;
readonly uploadTaskStartAsync?: PlatformMethod;
startObserving?: () => void;
stopObserving?: () => void;
addListener: (eventName: string) => void;
removeListeners: (count: number) => void;
}

View File

@@ -0,0 +1,9 @@
// @generated by expo-module-scripts
{
"extends": "expo-module-scripts/tsconfig.base",
"compilerOptions": {
"outDir": "./build"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}