From 94df55b0fd5eba222a1bf1655d5699fc0ace5532 Mon Sep 17 00:00:00 2001 From: Martin Mazein Date: Mon, 19 Apr 2021 21:20:25 +0200 Subject: [PATCH] feat(android): add File object wrapper for supporting URI based paths Most popular RN 3rd party libraries handling files (e.g. image/photo handling) are returning file:// URIs on Android. To save handling this in JS several times, we can simply try to detect if we are handling an URI and parse them accordingly. Regardless of the input we always return an File object to handle files. Signed-off-by: Martin Mazein --- .../java/com/alpha0010/fs/FileAccessModule.kt | 54 ++++++++++++------- example/src/App.tsx | 29 +++++++++- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt index 6af539f..fabef21 100644 --- a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt +++ b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt @@ -49,9 +49,9 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase ioScope.launch { try { if (encoding == "base64") { - File(path).appendBytes(Base64.decode(data, Base64.DEFAULT)) + parsePathToFile(path).appendBytes(Base64.decode(data, Base64.DEFAULT)) } else { - File(path).appendText(data) + parsePathToFile(path).appendText(data) } promise.resolve(null) } catch (e: Throwable) { @@ -64,7 +64,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase fun concatFiles(source: String, target: String, promise: Promise) { try { openForReading(source).use { input -> - FileOutputStream(File(target), true).use { + FileOutputStream(parsePathToFile(target), true).use { promise.resolve(input.copyTo(it).toInt()) } } @@ -78,7 +78,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase ioScope.launch { try { openForReading(source).use { input -> - File(target).outputStream().use { input.copyTo(it) } + parsePathToFile(target).outputStream().use { input.copyTo(it) } } promise.resolve(null) } catch (e: Throwable) { @@ -91,7 +91,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase fun cpAsset(asset: String, target: String, promise: Promise) { try { reactApplicationContext.assets.open(asset).use { assetStream -> - File(target).outputStream().use { assetStream.copyTo(it) } + parsePathToFile(target).outputStream().use { assetStream.copyTo(it) } } promise.resolve(null) } catch (e: Throwable) { @@ -183,7 +183,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase fun exists(path: String, promise: Promise) { ioScope.launch { try { - promise.resolve(File(path).exists()) + promise.resolve(parsePathToFile(path).exists()) } catch (e: Throwable) { promise.reject(e) } @@ -232,7 +232,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase try { response.use { if (init.hasKey("path")) { - File(init.getString("path")!!) + parsePathToFile(init.getString("path")!!) .outputStream() .use { response.body()!!.byteStream().copyTo(it) } } @@ -278,7 +278,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase fun isDir(path: String, promise: Promise) { ioScope.launch { try { - promise.resolve(File(path).isDirectory) + promise.resolve(parsePathToFile(path).isDirectory) } catch (e: Throwable) { promise.reject(e) } @@ -290,7 +290,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase ioScope.launch { try { val fileList = Arguments.createArray() - File(path).list()?.forEach { fileList.pushString(it) } + parsePathToFile(path).list()?.forEach { fileList.pushString(it) } promise.resolve(fileList) } catch (e: Throwable) { promise.reject(e) @@ -301,13 +301,13 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase @ReactMethod fun mkdir(path: String, promise: Promise) { ioScope.launch { - val file = File(path) + val file = parsePathToFile(path) try { when { file.exists() -> { promise.reject("EEXIST", "'$path' already exists.") } - File(path).mkdirs() -> { + parsePathToFile(path).mkdirs() -> { promise.resolve(null) } else -> { @@ -324,8 +324,8 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase fun mv(source: String, target: String, promise: Promise) { ioScope.launch { try { - if (!File(source).renameTo(File(target))) { - File(source).also { it.copyTo(File(target), overwrite = true) }.delete() + if (!parsePathToFile(source).renameTo(parsePathToFile(target))) { + parsePathToFile(source).also { it.copyTo(parsePathToFile(target), overwrite = true) }.delete() } promise.resolve(null) } catch (e: Throwable) { @@ -351,7 +351,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase @ReactMethod fun stat(path: String, promise: Promise) { try { - val file = File(path) + val file = parsePathToFile(path) if (file.exists()) { promise.resolve(Arguments.makeNativeMap(mapOf( "filename" to file.name, @@ -371,7 +371,7 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase @ReactMethod fun unlink(path: String, promise: Promise) { try { - if (File(path).delete()) { + if (parsePathToFile(path).delete()) { promise.resolve(null) } else { promise.reject("ERR", "Failed to unlink '$path'.") @@ -386,9 +386,9 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase ioScope.launch { try { if (encoding == "base64") { - File(path).writeBytes(Base64.decode(data, Base64.DEFAULT)) + parsePathToFile(path).writeBytes(Base64.decode(data, Base64.DEFAULT)) } else { - File(path).writeText(data) + parsePathToFile(path).writeText(data) } promise.resolve(null) } catch (e: Throwable) { @@ -398,14 +398,30 @@ class FileAccessModule(reactContext: ReactApplicationContext) : ReactContextBase } /** - * Open a file. Supports both standard file system paths and Storage Access + * Open a file. Supports standard file system paths, file URIs and Storage Access * Framework content URIs. */ private fun openForReading(path: String): InputStream { return if (path.startsWith("content://")) { reactApplicationContext.contentResolver.openInputStream(Uri.parse(path))!! } else { - File(path).inputStream() + parsePathToFile(path).inputStream() + } + } + + /** + * Return a File object and do some basic sanitization of the passed path. + */ + private fun parsePathToFile(path: String): File { + return if (path.contains("://")) { + try { + var pathUri = Uri.parse(path) + File(pathUri.path) + } catch (e: Throwable) { + File(path) + } + } else { + File(path) } } } diff --git a/example/src/App.tsx b/example/src/App.tsx index a59ecfe..566e19c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,5 +1,12 @@ import React, { useEffect, useState } from 'react'; -import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { + Platform, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; import { Dirs, FileSystem } from 'react-native-file-access'; export function App() { @@ -128,6 +135,26 @@ export function App() { }) ); + const sourcFile = + Platform.OS === 'ios' + ? `${Dirs.CacheDir}/renamed.txt` + : `file://${Dirs.CacheDir}/renamed.txt`; + + FileSystem.unlink(Dirs.CacheDir + '/3.txt') + .then(() => console.log('Deleted 3.txt')) + .catch(() => console.log('Did not delete 3.txt')) + .then(() => FileSystem.cp(sourcFile, Dirs.CacheDir + '/3.txt')) + .then(() => FileSystem.readFile(Dirs.CacheDir + '/3.txt')) + .then((res) => + setInfo((prev) => { + prev.push({ + key: 'cp(fileUris)', + value: JSON.stringify(res), + }); + return prev.slice(); + }) + ); + // Network access. FileSystem.fetch('https://example.com', { path: Dirs.CacheDir + '/download.html',