@amazon-devices/expo-file-system 2.0.1758683737
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +2 -0
- package/CHANGELOG.md +317 -0
- package/CMakeLists.txt +52 -0
- package/LICENSE +7 -0
- package/README.kepler.md +260 -0
- package/README.md +46 -0
- package/THIRD_PARTY_NOTICES.txt +24 -0
- package/android/build.gradle +118 -0
- package/android/src/main/AndroidManifest.xml +17 -0
- package/android/src/main/java/expo/modules/filesystem/AppDirectoriesModule.kt +26 -0
- package/android/src/main/java/expo/modules/filesystem/CountingRequestBody.kt +52 -0
- package/android/src/main/java/expo/modules/filesystem/FilePermissionModule.kt +48 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt +48 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemFileProvider.kt +5 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +1110 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemPackage.kt +10 -0
- package/android/src/main/java/expo/modules/filesystem/FileSystemRecords.kt +92 -0
- package/android/src/main/res/xml/file_system_provider_paths.xml +5 -0
- package/app.plugin.js +1 -0
- package/build/ExponentFileSystem.d.ts +4 -0
- package/build/ExponentFileSystem.d.ts.map +1 -0
- package/build/ExponentFileSystem.js +5 -0
- package/build/ExponentFileSystem.js.map +1 -0
- package/build/ExponentFileSystem.kepler.d.ts +4 -0
- package/build/ExponentFileSystem.kepler.d.ts.map +1 -0
- package/build/ExponentFileSystem.kepler.js +112 -0
- package/build/ExponentFileSystem.kepler.js.map +1 -0
- package/build/ExponentFileSystem.web.d.ts +3 -0
- package/build/ExponentFileSystem.web.d.ts.map +1 -0
- package/build/ExponentFileSystem.web.js +3 -0
- package/build/ExponentFileSystem.web.js.map +1 -0
- package/build/ExponentFileSystemShim.d.ts +4 -0
- package/build/ExponentFileSystemShim.d.ts.map +1 -0
- package/build/ExponentFileSystemShim.js +21 -0
- package/build/ExponentFileSystemShim.js.map +1 -0
- package/build/FileSystem.d.ts +361 -0
- package/build/FileSystem.d.ts.map +1 -0
- package/build/FileSystem.js +616 -0
- package/build/FileSystem.js.map +1 -0
- package/build/FileSystem.types.d.ts +304 -0
- package/build/FileSystem.types.d.ts.map +1 -0
- package/build/FileSystem.types.js +47 -0
- package/build/FileSystem.types.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +35 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +11 -0
- package/ios/EXFilePermissionModule.h +16 -0
- package/ios/EXFilePermissionModule.m +68 -0
- package/ios/EXFileSystem.h +25 -0
- package/ios/EXFileSystem.m +1084 -0
- package/ios/EXFileSystemAssetLibraryHandler.h +6 -0
- package/ios/EXFileSystemAssetLibraryHandler.m +142 -0
- package/ios/EXFileSystemHandler.h +17 -0
- package/ios/EXFileSystemLocalFileHandler.h +7 -0
- package/ios/EXFileSystemLocalFileHandler.m +79 -0
- package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.h +16 -0
- package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.m +55 -0
- package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.h +13 -0
- package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.m +64 -0
- package/ios/EXSessionTasks/EXSessionHandler.h +19 -0
- package/ios/EXSessionTasks/EXSessionHandler.m +49 -0
- package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.h +18 -0
- package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.m +66 -0
- package/ios/EXSessionTasks/EXSessionTaskDelegate.h +32 -0
- package/ios/EXSessionTasks/EXSessionTaskDelegate.m +58 -0
- package/ios/EXSessionTasks/EXSessionTaskDispatcher.h +19 -0
- package/ios/EXSessionTasks/EXSessionTaskDispatcher.m +96 -0
- package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.h +8 -0
- package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.m +52 -0
- package/ios/EXSessionTasks/EXTaskHandlersManager.h +21 -0
- package/ios/EXSessionTasks/EXTaskHandlersManager.m +56 -0
- package/ios/Encoding.swift +88 -0
- package/ios/ExpoFileSystem.h +1 -0
- package/ios/ExpoFileSystem.podspec +32 -0
- package/ios/FileSystemBackgroundSessionHandler.swift +25 -0
- package/ios/FileSystemExceptions.swift +69 -0
- package/ios/FileSystemHelpers.swift +66 -0
- package/ios/FileSystemModule.swift +263 -0
- package/ios/FileSystemRecords.swift +74 -0
- package/ios/NSData+EXFileSystem.h +9 -0
- package/ios/NSData+EXFileSystem.m +19 -0
- package/ios/NetworkingHelpers.swift +84 -0
- package/ios/Tests/EXFileSystemSpec.swift +55 -0
- package/jest.config.js +3 -0
- package/kepler/ExponentFileSystem.cpp +498 -0
- package/kepler/ExponentFileSystem.h +60 -0
- package/kepler/KeplerInit.cpp +28 -0
- package/kepler/NetworkingRequestHandler.cpp +47 -0
- package/kepler/test/FileSystem.cpp +425 -0
- package/package.json +1 -0
- package/plugin/build/withFileSystem.d.ts +3 -0
- package/plugin/build/withFileSystem.js +12 -0
- package/plugin/src/withFileSystem.ts +13 -0
- package/plugin/tsconfig.json +9 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/react-native.config.js +21 -0
- package/src/ExponentFileSystem.kepler.ts +167 -0
- package/src/ExponentFileSystem.ts +7 -0
- package/src/ExponentFileSystem.web.ts +2 -0
- package/src/ExponentFileSystemShim.ts +23 -0
- package/src/FileSystem.ts +756 -0
- package/src/FileSystem.types.ts +339 -0
- package/src/index.ts +2 -0
- package/src/types.ts +34 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
package expo.modules.filesystem
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import android.os.Environment
|
|
11
|
+
import android.os.StatFs
|
|
12
|
+
import android.provider.DocumentsContract
|
|
13
|
+
import android.util.Base64
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import androidx.core.content.FileProvider
|
|
16
|
+
import androidx.documentfile.provider.DocumentFile
|
|
17
|
+
import expo.modules.core.errors.ModuleDestroyedException
|
|
18
|
+
import expo.modules.interfaces.filesystem.Permission
|
|
19
|
+
import expo.modules.kotlin.Promise
|
|
20
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
21
|
+
import expo.modules.kotlin.modules.Module
|
|
22
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
23
|
+
import kotlinx.coroutines.CoroutineScope
|
|
24
|
+
import kotlinx.coroutines.Dispatchers
|
|
25
|
+
import kotlinx.coroutines.cancel
|
|
26
|
+
import kotlinx.coroutines.launch
|
|
27
|
+
import kotlinx.coroutines.withContext
|
|
28
|
+
import okhttp3.Call
|
|
29
|
+
import okhttp3.Callback
|
|
30
|
+
import okhttp3.Headers
|
|
31
|
+
import okhttp3.JavaNetCookieJar
|
|
32
|
+
import okhttp3.MediaType
|
|
33
|
+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
34
|
+
import okhttp3.MultipartBody
|
|
35
|
+
import okhttp3.OkHttpClient
|
|
36
|
+
import okhttp3.Request
|
|
37
|
+
import okhttp3.RequestBody
|
|
38
|
+
import okhttp3.RequestBody.Companion.asRequestBody
|
|
39
|
+
import okhttp3.Response
|
|
40
|
+
import okhttp3.ResponseBody
|
|
41
|
+
import okio.*
|
|
42
|
+
import org.apache.commons.codec.binary.Hex
|
|
43
|
+
import org.apache.commons.codec.digest.DigestUtils
|
|
44
|
+
import org.apache.commons.io.FileUtils
|
|
45
|
+
import org.apache.commons.io.IOUtils
|
|
46
|
+
import java.io.BufferedInputStream
|
|
47
|
+
import java.io.ByteArrayOutputStream
|
|
48
|
+
import java.io.File
|
|
49
|
+
import java.io.FileInputStream
|
|
50
|
+
import java.io.FileNotFoundException
|
|
51
|
+
import java.io.FileOutputStream
|
|
52
|
+
import java.io.IOException
|
|
53
|
+
import java.io.InputStream
|
|
54
|
+
import java.io.OutputStream
|
|
55
|
+
import java.io.OutputStreamWriter
|
|
56
|
+
import java.math.BigInteger
|
|
57
|
+
import java.net.CookieHandler
|
|
58
|
+
import java.net.URLConnection
|
|
59
|
+
import java.util.*
|
|
60
|
+
import java.util.concurrent.TimeUnit
|
|
61
|
+
import java.util.regex.Pattern
|
|
62
|
+
import kotlin.math.pow
|
|
63
|
+
|
|
64
|
+
private val TAG = FileSystemModule::class.java.simpleName
|
|
65
|
+
private const val EXDownloadProgressEventName = "expo-file-system.downloadProgress"
|
|
66
|
+
private const val EXUploadProgressEventName = "expo-file-system.uploadProgress"
|
|
67
|
+
private const val MIN_EVENT_DT_MS: Long = 100
|
|
68
|
+
private const val DIR_PERMISSIONS_REQUEST_CODE = 5394
|
|
69
|
+
|
|
70
|
+
private fun slashifyFilePath(path: String?): String? {
|
|
71
|
+
return if (path == null) {
|
|
72
|
+
null
|
|
73
|
+
} else if (path.startsWith("file:///")) {
|
|
74
|
+
path
|
|
75
|
+
} else {
|
|
76
|
+
// Ensure leading schema with a triple slash
|
|
77
|
+
Pattern.compile("^file:/*").matcher(path).replaceAll("file:///")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The class needs to be 'open', because it's inherited in expoview
|
|
82
|
+
open class FileSystemModule : Module() {
|
|
83
|
+
private val context: Context
|
|
84
|
+
get() = appContext.reactContext ?: throw Exceptions.AppContextLost()
|
|
85
|
+
private var client: OkHttpClient? = null
|
|
86
|
+
private var dirPermissionsRequest: Promise? = null
|
|
87
|
+
private val taskHandlers: MutableMap<String, TaskHandler> = HashMap()
|
|
88
|
+
private val moduleCoroutineScope = CoroutineScope(Dispatchers.Default)
|
|
89
|
+
|
|
90
|
+
@SuppressLint("WrongConstant", "DiscouragedApi")
|
|
91
|
+
override fun definition() = ModuleDefinition {
|
|
92
|
+
Name("ExponentFileSystem")
|
|
93
|
+
|
|
94
|
+
Constants(
|
|
95
|
+
"documentDirectory" to Uri.fromFile(context.filesDir).toString() + "/",
|
|
96
|
+
"cacheDirectory" to Uri.fromFile(context.cacheDir).toString() + "/",
|
|
97
|
+
"bundleDirectory" to "asset:///"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
Events(
|
|
101
|
+
EXDownloadProgressEventName,
|
|
102
|
+
EXUploadProgressEventName
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
OnCreate {
|
|
106
|
+
try {
|
|
107
|
+
ensureDirExists(context.filesDir)
|
|
108
|
+
ensureDirExists(context.cacheDir)
|
|
109
|
+
} catch (e: Exception) {
|
|
110
|
+
e.printStackTrace()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
AsyncFunction("getInfoAsync") { _uriStr: String, options: InfoOptions ->
|
|
115
|
+
var uriStr = slashifyFilePath(_uriStr)
|
|
116
|
+
|
|
117
|
+
val uri = Uri.parse(uriStr)
|
|
118
|
+
var absoluteUri = uri
|
|
119
|
+
|
|
120
|
+
if (uri.scheme == "file") {
|
|
121
|
+
uriStr = parseFileUri(uriStr as String)
|
|
122
|
+
absoluteUri = Uri.parse(uriStr)
|
|
123
|
+
}
|
|
124
|
+
ensurePermission(absoluteUri, Permission.READ)
|
|
125
|
+
|
|
126
|
+
if (uri.scheme == "file") {
|
|
127
|
+
val file = absoluteUri.toFile()
|
|
128
|
+
if (file.exists()) {
|
|
129
|
+
return@AsyncFunction Bundle().apply {
|
|
130
|
+
putBoolean("exists", true)
|
|
131
|
+
putBoolean("isDirectory", file.isDirectory)
|
|
132
|
+
putString("uri", Uri.fromFile(file).toString())
|
|
133
|
+
putDouble("size", getFileSize(file).toDouble())
|
|
134
|
+
putDouble("modificationTime", 0.001 * file.lastModified())
|
|
135
|
+
options.md5.takeIf { it == true }?.let { putString("md5", md5(file)) }
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
return@AsyncFunction Bundle().apply {
|
|
139
|
+
putBoolean("exists", false)
|
|
140
|
+
putBoolean("isDirectory", false)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else if (uri.scheme == "content" || uri.scheme == "asset" || uri.scheme == null) {
|
|
144
|
+
try {
|
|
145
|
+
val inputStream: InputStream = when (uri.scheme) {
|
|
146
|
+
"content" -> context.contentResolver.openInputStream(uri)
|
|
147
|
+
"asset" -> openAssetInputStream(uri)
|
|
148
|
+
else -> openResourceInputStream(uriStr)
|
|
149
|
+
} ?: throw FileNotFoundException()
|
|
150
|
+
|
|
151
|
+
return@AsyncFunction Bundle().apply {
|
|
152
|
+
putBoolean("exists", true)
|
|
153
|
+
putBoolean("isDirectory", false)
|
|
154
|
+
putString("uri", uri.toString())
|
|
155
|
+
// NOTE: `.available()` is supposedly not a reliable source of size info, but it's been
|
|
156
|
+
// more reliable than querying `OpenableColumns.SIZE` in practice in tests ¯\_(ツ)_/¯
|
|
157
|
+
putDouble("size", inputStream.available().toDouble())
|
|
158
|
+
if (options.md5 == true) {
|
|
159
|
+
val md5bytes = DigestUtils.md5(inputStream)
|
|
160
|
+
putString("md5", String(Hex.encodeHex(md5bytes)))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (e: FileNotFoundException) {
|
|
164
|
+
return@AsyncFunction Bundle().apply {
|
|
165
|
+
putBoolean("exists", false)
|
|
166
|
+
putBoolean("isDirectory", false)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw IOException("Unsupported scheme for location '$uri'.")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
AsyncFunction("readAsStringAsync") { uriStr: String, options: ReadingOptions ->
|
|
175
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
176
|
+
ensurePermission(uri, Permission.READ)
|
|
177
|
+
|
|
178
|
+
// TODO:Bacon: Add more encoding types to match iOS
|
|
179
|
+
val encoding = options.encoding
|
|
180
|
+
var contents: String?
|
|
181
|
+
if (encoding == EncodingType.BASE64) {
|
|
182
|
+
getInputStream(uri).use { inputStream ->
|
|
183
|
+
contents = if (options.length != null && options.position != null) {
|
|
184
|
+
val buffer = ByteArray(options.length)
|
|
185
|
+
inputStream.skip(options.position.toLong())
|
|
186
|
+
val bytesRead = inputStream.read(buffer, 0, options.length)
|
|
187
|
+
Base64.encodeToString(buffer, 0, bytesRead, Base64.NO_WRAP)
|
|
188
|
+
} else {
|
|
189
|
+
val inputData = getInputStreamBytes(inputStream)
|
|
190
|
+
Base64.encodeToString(inputData, Base64.NO_WRAP)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
contents = when {
|
|
195
|
+
uri.scheme == "file" -> IOUtils.toString(FileInputStream(uri.toFile()))
|
|
196
|
+
uri.scheme == "asset" -> IOUtils.toString(openAssetInputStream(uri))
|
|
197
|
+
uri.scheme == null -> IOUtils.toString(openResourceInputStream(uriStr))
|
|
198
|
+
uri.isSAFUri -> IOUtils.toString(context.contentResolver.openInputStream(uri))
|
|
199
|
+
else -> throw IOException("Unsupported scheme for location '$uri'.")
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return@AsyncFunction contents
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
AsyncFunction("writeAsStringAsync") { uriStr: String, contents: String, options: WritingOptions ->
|
|
206
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
207
|
+
ensurePermission(uri, Permission.WRITE)
|
|
208
|
+
val encoding = options.encoding
|
|
209
|
+
getOutputStream(uri).use { out ->
|
|
210
|
+
if (encoding == EncodingType.BASE64) {
|
|
211
|
+
val bytes = Base64.decode(contents, Base64.DEFAULT)
|
|
212
|
+
out.write(bytes)
|
|
213
|
+
} else {
|
|
214
|
+
OutputStreamWriter(out).use { writer -> writer.write(contents) }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
AsyncFunction("deleteAsync") { uriStr: String, options: DeletingOptions ->
|
|
220
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
221
|
+
val appendedUri = Uri.withAppendedPath(uri, "..")
|
|
222
|
+
ensurePermission(appendedUri, Permission.WRITE, "Location '$uri' isn't deletable.")
|
|
223
|
+
|
|
224
|
+
if (uri.scheme == "file") {
|
|
225
|
+
val file = uri.toFile()
|
|
226
|
+
if (file.exists()) {
|
|
227
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
228
|
+
FileUtils.forceDelete(file)
|
|
229
|
+
} else {
|
|
230
|
+
// to be removed once Android SDK 25 support is dropped
|
|
231
|
+
forceDelete(file)
|
|
232
|
+
}
|
|
233
|
+
return@AsyncFunction
|
|
234
|
+
} else {
|
|
235
|
+
if (options.idempotent) {
|
|
236
|
+
return@AsyncFunction
|
|
237
|
+
} else {
|
|
238
|
+
throw FileSystemFileNotFoundException(uri)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else if (uri.isSAFUri) {
|
|
242
|
+
val file = getNearestSAFFile(uri)
|
|
243
|
+
if (file != null && file.exists()) {
|
|
244
|
+
file.delete()
|
|
245
|
+
return@AsyncFunction
|
|
246
|
+
} else {
|
|
247
|
+
if (options.idempotent) {
|
|
248
|
+
return@AsyncFunction
|
|
249
|
+
} else {
|
|
250
|
+
throw FileSystemFileNotFoundException(uri)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
throw IOException("Unsupported scheme for location '$uri'.")
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
AsyncFunction("moveAsync") { options: RelocatingOptions ->
|
|
259
|
+
val fromUri = Uri.parse(slashifyFilePath(options.from))
|
|
260
|
+
ensurePermission(Uri.withAppendedPath(fromUri, ".."), Permission.WRITE, "Location '$fromUri' isn't movable.")
|
|
261
|
+
val toUri = Uri.parse(slashifyFilePath(options.to))
|
|
262
|
+
ensurePermission(toUri, Permission.WRITE)
|
|
263
|
+
|
|
264
|
+
if (fromUri.scheme == "file") {
|
|
265
|
+
val from = fromUri.toFile()
|
|
266
|
+
val to = toUri.toFile()
|
|
267
|
+
if (from.renameTo(to)) {
|
|
268
|
+
return@AsyncFunction
|
|
269
|
+
} else {
|
|
270
|
+
throw FileSystemCannotMoveFileException(fromUri, toUri)
|
|
271
|
+
}
|
|
272
|
+
} else if (fromUri.isSAFUri) {
|
|
273
|
+
val documentFile = getNearestSAFFile(fromUri)
|
|
274
|
+
if (documentFile == null || !documentFile.exists()) {
|
|
275
|
+
throw FileSystemCannotMoveFileException(fromUri, toUri)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
val output = toUri.toFile()
|
|
279
|
+
transformFilesFromSAF(documentFile, output, false)
|
|
280
|
+
} else {
|
|
281
|
+
throw IOException("Unsupported scheme for location '$fromUri'.")
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
AsyncFunction("copyAsync") { options: RelocatingOptions ->
|
|
286
|
+
val fromUri = Uri.parse(slashifyFilePath(options.from))
|
|
287
|
+
ensurePermission(fromUri, Permission.READ, "Location '$fromUri' isn't readable.")
|
|
288
|
+
val toUri = Uri.parse(slashifyFilePath(options.to))
|
|
289
|
+
ensurePermission(toUri, Permission.WRITE)
|
|
290
|
+
|
|
291
|
+
when {
|
|
292
|
+
fromUri.scheme == "file" -> {
|
|
293
|
+
val from = fromUri.toFile()
|
|
294
|
+
val to = toUri.toFile()
|
|
295
|
+
if (from.isDirectory) {
|
|
296
|
+
FileUtils.copyDirectory(from, to)
|
|
297
|
+
} else {
|
|
298
|
+
FileUtils.copyFile(from, to)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fromUri.isSAFUri -> {
|
|
303
|
+
val documentFile = getNearestSAFFile(fromUri)
|
|
304
|
+
if (documentFile == null || !documentFile.exists()) {
|
|
305
|
+
throw FileSystemCopyFailedException(fromUri)
|
|
306
|
+
}
|
|
307
|
+
val output = toUri.toFile()
|
|
308
|
+
transformFilesFromSAF(documentFile, output, true)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fromUri.scheme == "content" -> {
|
|
312
|
+
val inputStream = context.contentResolver.openInputStream(fromUri)
|
|
313
|
+
val out: OutputStream = FileOutputStream(toUri.toFile())
|
|
314
|
+
IOUtils.copy(inputStream, out)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fromUri.scheme == "asset" -> {
|
|
318
|
+
val inputStream = openAssetInputStream(fromUri)
|
|
319
|
+
val out: OutputStream = FileOutputStream(toUri.toFile())
|
|
320
|
+
IOUtils.copy(inputStream, out)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fromUri.scheme == null -> {
|
|
324
|
+
// this is probably an asset embedded by the packager in resources
|
|
325
|
+
val inputStream = openResourceInputStream(options.from)
|
|
326
|
+
val out: OutputStream = FileOutputStream(toUri.toFile())
|
|
327
|
+
IOUtils.copy(inputStream, out)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
else -> {
|
|
331
|
+
throw IOException("Unsupported scheme for location '$fromUri'.")
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
AsyncFunction("makeDirectoryAsync") { uriStr: String, options: MakeDirectoryOptions ->
|
|
337
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
338
|
+
ensurePermission(uri, Permission.WRITE)
|
|
339
|
+
if (uri.scheme == "file") {
|
|
340
|
+
val file = uri.toFile()
|
|
341
|
+
val previouslyCreated = file.isDirectory
|
|
342
|
+
val setIntermediates = options.intermediates
|
|
343
|
+
val success = if (setIntermediates) file.mkdirs() else file.mkdir()
|
|
344
|
+
if (success || setIntermediates && previouslyCreated) {
|
|
345
|
+
return@AsyncFunction
|
|
346
|
+
} else {
|
|
347
|
+
throw FileSystemCannotCreateDirectoryException(uri)
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
throw IOException("Unsupported scheme for location '$uri'.")
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
AsyncFunction("readDirectoryAsync") { uriStr: String? ->
|
|
355
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
356
|
+
ensurePermission(uri, Permission.READ)
|
|
357
|
+
|
|
358
|
+
if (uri.scheme == "file") {
|
|
359
|
+
val file = uri.toFile()
|
|
360
|
+
val children = file.listFiles() as Array<File?>?
|
|
361
|
+
?: throw FileSystemCannotReadDirectoryException(uri)
|
|
362
|
+
|
|
363
|
+
return@AsyncFunction children.map { it?.name }
|
|
364
|
+
} else if (uri.isSAFUri) {
|
|
365
|
+
throw FileSystemUnsupportedSchemeException()
|
|
366
|
+
} else {
|
|
367
|
+
throw IOException("Unsupported scheme for location '$uri'.")
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
AsyncFunction("getTotalDiskCapacityAsync") {
|
|
372
|
+
val root = StatFs(Environment.getDataDirectory().absolutePath)
|
|
373
|
+
val blockCount = root.blockCountLong
|
|
374
|
+
val blockSize = root.blockSizeLong
|
|
375
|
+
val capacity = BigInteger.valueOf(blockCount).multiply(BigInteger.valueOf(blockSize))
|
|
376
|
+
// cast down to avoid overflow
|
|
377
|
+
return@AsyncFunction capacity.toDouble().coerceAtMost(2.0.pow(53.0) - 1)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
AsyncFunction("getFreeDiskStorageAsync") {
|
|
381
|
+
val external = StatFs(Environment.getDataDirectory().absolutePath)
|
|
382
|
+
val availableBlocks = external.availableBlocksLong
|
|
383
|
+
val blockSize = external.blockSizeLong
|
|
384
|
+
val storage = BigInteger.valueOf(availableBlocks).multiply(BigInteger.valueOf(blockSize))
|
|
385
|
+
// cast down to avoid overflow
|
|
386
|
+
return@AsyncFunction storage.toDouble().coerceAtMost(2.0.pow(53.0) - 1)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
AsyncFunction("getContentUriAsync") { uri: String ->
|
|
390
|
+
val fileUri = Uri.parse(slashifyFilePath(uri))
|
|
391
|
+
ensurePermission(fileUri, Permission.WRITE)
|
|
392
|
+
ensurePermission(fileUri, Permission.READ)
|
|
393
|
+
fileUri.checkIfFileDirExists()
|
|
394
|
+
|
|
395
|
+
if (fileUri.scheme == "file") {
|
|
396
|
+
val file = fileUri.toFile()
|
|
397
|
+
return@AsyncFunction contentUriFromFile(file).toString()
|
|
398
|
+
} else {
|
|
399
|
+
throw FileSystemUnreadableDirectoryException(uri)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
AsyncFunction("readSAFDirectoryAsync") { uriStr: String ->
|
|
404
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
405
|
+
ensurePermission(uri, Permission.READ)
|
|
406
|
+
|
|
407
|
+
if (uri.isSAFUri) {
|
|
408
|
+
val file = DocumentFile.fromTreeUri(context, uri)
|
|
409
|
+
if (file == null || !file.exists() || !file.isDirectory) {
|
|
410
|
+
throw FileSystemCannotReadDirectoryException(uri)
|
|
411
|
+
}
|
|
412
|
+
val children = file.listFiles()
|
|
413
|
+
return@AsyncFunction children.map { it.uri.toString() }
|
|
414
|
+
} else {
|
|
415
|
+
throw IOException("The URI '$uri' is not a Storage Access Framework URI. Try using FileSystem.readDirectoryAsync instead.")
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
AsyncFunction("makeSAFDirectoryAsync") { uriStr: String, dirName: String ->
|
|
420
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
421
|
+
ensurePermission(uri, Permission.WRITE)
|
|
422
|
+
|
|
423
|
+
if (!uri.isSAFUri) {
|
|
424
|
+
throw IOException("The URI '$uri' is not a Storage Access Framework URI. Try using FileSystem.makeDirectoryAsync instead.")
|
|
425
|
+
}
|
|
426
|
+
val dir = getNearestSAFFile(uri)
|
|
427
|
+
if (dir != null) {
|
|
428
|
+
if (!dir.isDirectory) {
|
|
429
|
+
throw FileSystemCannotCreateDirectoryException(uri)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
val newDir = dirName.let { dir?.createDirectory(it) }
|
|
433
|
+
?: throw FileSystemCannotCreateDirectoryException(null)
|
|
434
|
+
|
|
435
|
+
return@AsyncFunction newDir.uri.toString()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
AsyncFunction("createSAFFileAsync") { uriStr: String, fileName: String, mimeType: String ->
|
|
439
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
440
|
+
ensurePermission(uri, Permission.WRITE)
|
|
441
|
+
if (uri.isSAFUri) {
|
|
442
|
+
val dir = getNearestSAFFile(uri)
|
|
443
|
+
if (dir == null || !dir.isDirectory) {
|
|
444
|
+
throw FileSystemCannotCreateFileException(uri)
|
|
445
|
+
}
|
|
446
|
+
val newFile = dir.createFile(mimeType, fileName)
|
|
447
|
+
?: throw FileSystemCannotCreateFileException(null)
|
|
448
|
+
|
|
449
|
+
return@AsyncFunction newFile.uri.toString()
|
|
450
|
+
} else {
|
|
451
|
+
throw IOException("The URI '$uri' is not a Storage Access Framework URI.")
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
AsyncFunction("requestDirectoryPermissionsAsync") { initialFileUrl: String?, promise: Promise ->
|
|
456
|
+
val currentActivity = appContext.currentActivity
|
|
457
|
+
?: throw Exceptions.MissingActivity()
|
|
458
|
+
if (dirPermissionsRequest != null) {
|
|
459
|
+
throw FileSystemPendingPermissionsRequestException()
|
|
460
|
+
}
|
|
461
|
+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
|
462
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
463
|
+
initialFileUrl
|
|
464
|
+
?.let { Uri.parse(slashifyFilePath(it)) }
|
|
465
|
+
?.let { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it) }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
dirPermissionsRequest = promise
|
|
469
|
+
currentActivity.startActivityForResult(intent, DIR_PERMISSIONS_REQUEST_CODE)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
AsyncFunction("uploadAsync") { url: String, fileUriString: String, options: FileSystemUploadOptions, promise: Promise ->
|
|
473
|
+
val request = createUploadRequest(
|
|
474
|
+
url, fileUriString, options
|
|
475
|
+
) { requestBody -> requestBody }
|
|
476
|
+
|
|
477
|
+
okHttpClient?.let {
|
|
478
|
+
it.newCall(request).enqueue(object : Callback {
|
|
479
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
480
|
+
Log.e(TAG, e.message.toString())
|
|
481
|
+
promise.reject(TAG, e.message, e)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
override fun onResponse(call: Call, response: Response) {
|
|
485
|
+
val result = Bundle().apply {
|
|
486
|
+
putString("body", response.body?.string())
|
|
487
|
+
putInt("status", response.code)
|
|
488
|
+
putBundle("headers", translateHeaders(response.headers))
|
|
489
|
+
}
|
|
490
|
+
response.close()
|
|
491
|
+
promise.resolve(result)
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
} ?: run {
|
|
495
|
+
promise.reject(FileSystemOkHttpNullException())
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
AsyncFunction("uploadTaskStartAsync") { url: String, fileUriString: String, uuid: String, options: FileSystemUploadOptions, promise: Promise ->
|
|
500
|
+
val progressListener: CountingRequestListener = object : CountingRequestListener {
|
|
501
|
+
private var mLastUpdate: Long = -1
|
|
502
|
+
override fun onProgress(bytesWritten: Long, contentLength: Long) {
|
|
503
|
+
val uploadProgress = Bundle()
|
|
504
|
+
val uploadProgressData = Bundle()
|
|
505
|
+
val currentTime = System.currentTimeMillis()
|
|
506
|
+
|
|
507
|
+
// Throttle events. Sending too many events will block the JS event loop.
|
|
508
|
+
// Make sure to send the last event when we're at 100%.
|
|
509
|
+
if (currentTime > mLastUpdate + MIN_EVENT_DT_MS || bytesWritten == contentLength) {
|
|
510
|
+
mLastUpdate = currentTime
|
|
511
|
+
uploadProgressData.putDouble("totalBytesSent", bytesWritten.toDouble())
|
|
512
|
+
uploadProgressData.putDouble("totalBytesExpectedToSend", contentLength.toDouble())
|
|
513
|
+
uploadProgress.putString("uuid", uuid)
|
|
514
|
+
uploadProgress.putBundle("data", uploadProgressData)
|
|
515
|
+
sendEvent(EXUploadProgressEventName, uploadProgress)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
val request = createUploadRequest(
|
|
520
|
+
url,
|
|
521
|
+
fileUriString,
|
|
522
|
+
options,
|
|
523
|
+
) { requestBody -> CountingRequestBody(requestBody, progressListener) }
|
|
524
|
+
|
|
525
|
+
val call = okHttpClient!!.newCall(request)
|
|
526
|
+
taskHandlers[uuid] = TaskHandler(call)
|
|
527
|
+
call.enqueue(object : Callback {
|
|
528
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
529
|
+
if (call.isCanceled()) {
|
|
530
|
+
promise.resolve(null)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
Log.e(TAG, e.message.toString())
|
|
534
|
+
promise.reject(TAG, e.message, e)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
override fun onResponse(call: Call, response: Response) {
|
|
538
|
+
val result = Bundle()
|
|
539
|
+
val body = response.body
|
|
540
|
+
result.apply {
|
|
541
|
+
putString("body", body?.string())
|
|
542
|
+
putInt("status", response.code)
|
|
543
|
+
putBundle("headers", translateHeaders(response.headers))
|
|
544
|
+
}
|
|
545
|
+
response.close()
|
|
546
|
+
promise.resolve(result)
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
AsyncFunction("downloadAsync") { url: String, uriStr: String?, options: DownloadOptions, promise: Promise ->
|
|
552
|
+
val uri = Uri.parse(slashifyFilePath(uriStr))
|
|
553
|
+
ensurePermission(uri, Permission.WRITE)
|
|
554
|
+
uri.checkIfFileDirExists()
|
|
555
|
+
|
|
556
|
+
when {
|
|
557
|
+
url.contains(":").not() -> {
|
|
558
|
+
val context = context
|
|
559
|
+
val resources = context.resources
|
|
560
|
+
val packageName = context.packageName
|
|
561
|
+
val resourceId = resources.getIdentifier(url, "raw", packageName)
|
|
562
|
+
val bufferedSource = context.resources.openRawResource(resourceId).source().buffer()
|
|
563
|
+
val file = uri.toFile()
|
|
564
|
+
file.delete()
|
|
565
|
+
val sink = file.sink().buffer()
|
|
566
|
+
sink.writeAll(bufferedSource)
|
|
567
|
+
sink.close()
|
|
568
|
+
val result = Bundle()
|
|
569
|
+
result.putString("uri", Uri.fromFile(file).toString())
|
|
570
|
+
options.md5.takeIf { it }?.let { result.putString("md5", md5(file)) }
|
|
571
|
+
promise.resolve(result)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
"file" == uri.scheme -> {
|
|
575
|
+
val requestBuilder = Request.Builder().url(url)
|
|
576
|
+
if (options.headers != null) {
|
|
577
|
+
val headers = options.headers
|
|
578
|
+
headers.forEach { (key, value) ->
|
|
579
|
+
requestBuilder.addHeader(key, value)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
okHttpClient?.newCall(requestBuilder.build())?.enqueue(object : Callback {
|
|
583
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
584
|
+
Log.e(TAG, e.message.toString())
|
|
585
|
+
promise.reject(TAG, e.message, e)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
@Throws(IOException::class)
|
|
589
|
+
override fun onResponse(call: Call, response: Response) {
|
|
590
|
+
val file = uri.toFile()
|
|
591
|
+
file.delete()
|
|
592
|
+
val sink = file.sink().buffer()
|
|
593
|
+
sink.writeAll(response.body!!.source())
|
|
594
|
+
sink.close()
|
|
595
|
+
val result = Bundle().apply {
|
|
596
|
+
putString("uri", Uri.fromFile(file).toString())
|
|
597
|
+
putInt("status", response.code)
|
|
598
|
+
putBundle("headers", translateHeaders(response.headers))
|
|
599
|
+
if (options.md5) {
|
|
600
|
+
putString("md5", md5(file))
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
response.close()
|
|
604
|
+
promise.resolve(result)
|
|
605
|
+
}
|
|
606
|
+
}) ?: promise.reject(FileSystemOkHttpNullException())
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
else -> throw IOException("Unsupported scheme for location '$uri'.")
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
AsyncFunction("networkTaskCancelAsync") { uuid: String ->
|
|
614
|
+
val taskHandler = taskHandlers[uuid]
|
|
615
|
+
taskHandler?.call?.cancel()
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
AsyncFunction("downloadResumableStartAsync") { url: String, fileUriStr: String, uuid: String, options: DownloadOptions, resumeData: String?, promise: Promise ->
|
|
619
|
+
val fileUri = Uri.parse(slashifyFilePath(fileUriStr))
|
|
620
|
+
fileUri.checkIfFileDirExists()
|
|
621
|
+
if (fileUri.scheme != "file") {
|
|
622
|
+
throw IOException("Unsupported scheme for location '$fileUri'.")
|
|
623
|
+
}
|
|
624
|
+
val progressListener: ProgressListener = object : ProgressListener {
|
|
625
|
+
var mLastUpdate: Long = -1
|
|
626
|
+
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
|
627
|
+
val downloadProgress = Bundle()
|
|
628
|
+
val downloadProgressData = Bundle()
|
|
629
|
+
val totalBytesWritten = bytesRead + (resumeData?.toLong() ?: 0)
|
|
630
|
+
val totalBytesExpectedToWrite = contentLength + (resumeData?.toLong() ?: 0)
|
|
631
|
+
val currentTime = System.currentTimeMillis()
|
|
632
|
+
|
|
633
|
+
// Throttle events. Sending too many events will block the JS event loop.
|
|
634
|
+
// Make sure to send the last event when we're at 100%.
|
|
635
|
+
if (currentTime > mLastUpdate + MIN_EVENT_DT_MS || totalBytesWritten == totalBytesExpectedToWrite) {
|
|
636
|
+
mLastUpdate = currentTime
|
|
637
|
+
downloadProgressData.putDouble("totalBytesWritten", totalBytesWritten.toDouble())
|
|
638
|
+
downloadProgressData.putDouble("totalBytesExpectedToWrite", totalBytesExpectedToWrite.toDouble())
|
|
639
|
+
downloadProgress.putString("uuid", uuid)
|
|
640
|
+
downloadProgress.putBundle("data", downloadProgressData)
|
|
641
|
+
sendEvent(EXDownloadProgressEventName, downloadProgress)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
val client = okHttpClient?.newBuilder()
|
|
646
|
+
?.addInterceptor { chain ->
|
|
647
|
+
val originalResponse = chain.proceed(chain.request())
|
|
648
|
+
originalResponse.newBuilder()
|
|
649
|
+
.body(ProgressResponseBody(originalResponse.body, progressListener))
|
|
650
|
+
.build()
|
|
651
|
+
}
|
|
652
|
+
?.build()
|
|
653
|
+
if (client == null) {
|
|
654
|
+
promise.reject(FileSystemOkHttpNullException())
|
|
655
|
+
return@AsyncFunction
|
|
656
|
+
}
|
|
657
|
+
val requestBuilder = Request.Builder()
|
|
658
|
+
resumeData?.let {
|
|
659
|
+
requestBuilder.addHeader("Range", "bytes=$it-")
|
|
660
|
+
}
|
|
661
|
+
if (options.headers != null) {
|
|
662
|
+
val headers = options.headers
|
|
663
|
+
headers.forEach { (key, value) ->
|
|
664
|
+
requestBuilder.addHeader(key, value)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
val request = requestBuilder.url(url).build()
|
|
669
|
+
val call = client.newCall(request)
|
|
670
|
+
taskHandlers[uuid] = DownloadTaskHandler(fileUri, call)
|
|
671
|
+
val params = DownloadResumableTaskParams(
|
|
672
|
+
options, call, fileUri.toFile(), resumeData != null, promise
|
|
673
|
+
)
|
|
674
|
+
moduleCoroutineScope.launch {
|
|
675
|
+
downloadResumableTask(params)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
AsyncFunction("downloadResumablePauseAsync") { uuid: String ->
|
|
680
|
+
val taskHandler = taskHandlers[uuid]
|
|
681
|
+
?: throw IOException("No download object available")
|
|
682
|
+
|
|
683
|
+
if (taskHandler !is DownloadTaskHandler) {
|
|
684
|
+
throw FileSystemCannotFindTaskException()
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
taskHandler.call.cancel()
|
|
688
|
+
taskHandlers.remove(uuid)
|
|
689
|
+
val file = taskHandler.fileUri.toFile()
|
|
690
|
+
val result = Bundle().apply {
|
|
691
|
+
putString("resumeData", file.length().toString())
|
|
692
|
+
}
|
|
693
|
+
return@AsyncFunction result
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
OnActivityResult { _, (requestCode, resultCode, data) ->
|
|
697
|
+
if (requestCode == DIR_PERMISSIONS_REQUEST_CODE && dirPermissionsRequest != null) {
|
|
698
|
+
val currentActivity =
|
|
699
|
+
appContext.currentActivity ?: throw Exceptions.MissingActivity()
|
|
700
|
+
val result = Bundle()
|
|
701
|
+
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
702
|
+
val treeUri = data.data
|
|
703
|
+
val takeFlags = (
|
|
704
|
+
data.flags
|
|
705
|
+
and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
706
|
+
)
|
|
707
|
+
treeUri?.let {
|
|
708
|
+
currentActivity.contentResolver.takePersistableUriPermission(it, takeFlags)
|
|
709
|
+
}
|
|
710
|
+
result.putBoolean("granted", true)
|
|
711
|
+
result.putString("directoryUri", treeUri.toString())
|
|
712
|
+
} else {
|
|
713
|
+
result.putBoolean("granted", false)
|
|
714
|
+
}
|
|
715
|
+
dirPermissionsRequest?.resolve(result)
|
|
716
|
+
dirPermissionsRequest = null
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
OnDestroy {
|
|
721
|
+
try {
|
|
722
|
+
moduleCoroutineScope.cancel(ModuleDestroyedException())
|
|
723
|
+
} catch (e: IllegalStateException) {
|
|
724
|
+
Log.e(TAG, "The scope does not have a job in it")
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
@Throws(IOException::class)
|
|
730
|
+
private fun Uri.checkIfFileExists() {
|
|
731
|
+
val file = this.toFile()
|
|
732
|
+
if (!file.exists()) {
|
|
733
|
+
throw IOException("Directory for '${file.path}' doesn't exist.")
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
@Throws(IOException::class)
|
|
738
|
+
private fun Uri.checkIfFileDirExists() {
|
|
739
|
+
val file = this.toFile()
|
|
740
|
+
val dir = file.parentFile
|
|
741
|
+
if (dir == null || !dir.exists()) {
|
|
742
|
+
throw IOException("Directory for '${file.path}' doesn't exist. Please make sure directory '${file.parent}' exists before calling downloadAsync.")
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@Throws(IOException::class)
|
|
747
|
+
private fun ensureDirExists(dir: File) {
|
|
748
|
+
if (!(dir.isDirectory || dir.mkdirs())) {
|
|
749
|
+
throw IOException("Couldn't create directory '$dir'")
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private fun permissionsForPath(path: String?): EnumSet<Permission>? {
|
|
754
|
+
return appContext.filePermission?.getPathPermissions(context, path)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private fun permissionsForUri(uri: Uri) = when {
|
|
758
|
+
uri.isSAFUri -> permissionsForSAFUri(uri)
|
|
759
|
+
uri.scheme == "content" -> EnumSet.of(Permission.READ)
|
|
760
|
+
uri.scheme == "asset" -> EnumSet.of(Permission.READ)
|
|
761
|
+
uri.scheme == "file" -> permissionsForPath(uri.path)
|
|
762
|
+
uri.scheme == null -> EnumSet.of(Permission.READ)
|
|
763
|
+
else -> EnumSet.noneOf(Permission::class.java)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private fun permissionsForSAFUri(uri: Uri): EnumSet<Permission> {
|
|
767
|
+
val documentFile = getNearestSAFFile(uri)
|
|
768
|
+
return EnumSet.noneOf(Permission::class.java).apply {
|
|
769
|
+
if (documentFile != null) {
|
|
770
|
+
if (documentFile.canRead()) {
|
|
771
|
+
add(Permission.READ)
|
|
772
|
+
}
|
|
773
|
+
if (documentFile.canWrite()) {
|
|
774
|
+
add(Permission.WRITE)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// For now we only need to ensure one permission at a time, this allows easier error message strings,
|
|
781
|
+
// we can generalize this when needed later
|
|
782
|
+
@Throws(IOException::class)
|
|
783
|
+
private fun ensurePermission(uri: Uri, permission: Permission, errorMsg: String) {
|
|
784
|
+
if (permissionsForUri(uri)?.contains(permission) != true) {
|
|
785
|
+
throw IOException(errorMsg)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
@Throws(IOException::class)
|
|
790
|
+
private fun ensurePermission(uri: Uri, permission: Permission) {
|
|
791
|
+
if (permission == Permission.READ) {
|
|
792
|
+
ensurePermission(uri, permission, "Location '$uri' isn't readable.")
|
|
793
|
+
}
|
|
794
|
+
if (permission == Permission.WRITE) {
|
|
795
|
+
ensurePermission(uri, permission, "Location '$uri' isn't writable.")
|
|
796
|
+
}
|
|
797
|
+
ensurePermission(uri, permission, "Location '$uri' doesn't have permission '${permission.name}'.")
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
@Throws(IOException::class)
|
|
801
|
+
private fun openAssetInputStream(uri: Uri): InputStream {
|
|
802
|
+
// AssetManager expects no leading slash.
|
|
803
|
+
val asset = requireNotNull(uri.path).substring(1)
|
|
804
|
+
return context.assets.open(asset)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
@SuppressLint("DiscouragedApi")
|
|
808
|
+
@Throws(IOException::class)
|
|
809
|
+
private fun openResourceInputStream(resourceName: String?): InputStream {
|
|
810
|
+
var resourceId = context.resources.getIdentifier(resourceName, "raw", context.packageName)
|
|
811
|
+
if (resourceId == 0) {
|
|
812
|
+
// this resource doesn't exist in the raw folder, so try drawable
|
|
813
|
+
resourceId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
|
|
814
|
+
if (resourceId == 0) {
|
|
815
|
+
throw FileNotFoundException("No resource found with the name '$resourceName'")
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return context.resources.openRawResource(resourceId)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@Throws(IOException::class)
|
|
822
|
+
private fun transformFilesFromSAF(documentFile: DocumentFile, outputDir: File, copy: Boolean) {
|
|
823
|
+
if (!documentFile.exists()) {
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
if (!outputDir.exists() && !outputDir.mkdirs()) {
|
|
827
|
+
throw IOException("Couldn't create folder in output dir.")
|
|
828
|
+
}
|
|
829
|
+
if (documentFile.isDirectory) {
|
|
830
|
+
for (file in documentFile.listFiles()) {
|
|
831
|
+
documentFile.name?.let {
|
|
832
|
+
transformFilesFromSAF(file, File(outputDir, it), copy)
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (!copy) {
|
|
836
|
+
documentFile.delete()
|
|
837
|
+
}
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
documentFile.name?.let {
|
|
841
|
+
val newFile = File(outputDir.path, it)
|
|
842
|
+
context.contentResolver.openInputStream(documentFile.uri).use { `in` -> FileOutputStream(newFile).use { out -> IOUtils.copy(`in`, out) } }
|
|
843
|
+
if (!copy) {
|
|
844
|
+
documentFile.delete()
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private fun contentUriFromFile(file: File): Uri {
|
|
850
|
+
val currentActivity = appContext.currentActivity
|
|
851
|
+
?: throw Exceptions.MissingActivity()
|
|
852
|
+
|
|
853
|
+
return FileProvider.getUriForFile(
|
|
854
|
+
currentActivity.application,
|
|
855
|
+
"${currentActivity.application.packageName}.FileSystemFileProvider",
|
|
856
|
+
file
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
@Throws(IOException::class)
|
|
861
|
+
private fun createUploadRequest(url: String, fileUriString: String, options: FileSystemUploadOptions, decorator: RequestBodyDecorator): Request {
|
|
862
|
+
val fileUri = Uri.parse(slashifyFilePath(fileUriString))
|
|
863
|
+
ensurePermission(fileUri, Permission.READ)
|
|
864
|
+
fileUri.checkIfFileExists()
|
|
865
|
+
|
|
866
|
+
val requestBuilder = Request.Builder().url(url)
|
|
867
|
+
options.headers?.let {
|
|
868
|
+
it.forEach { (key, value) -> requestBuilder.addHeader(key, value) }
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
val body = createRequestBody(options, decorator, fileUri.toFile())
|
|
872
|
+
return options.httpMethod.let { requestBuilder.method(it.value, body).build() }
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private fun createRequestBody(options: FileSystemUploadOptions, decorator: RequestBodyDecorator, file: File): RequestBody {
|
|
876
|
+
return when (options.uploadType) {
|
|
877
|
+
FileSystemUploadType.BINARY_CONTENT -> {
|
|
878
|
+
decorator.decorate(file.asRequestBody(null))
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
FileSystemUploadType.MULTIPART -> {
|
|
882
|
+
val bodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
|
883
|
+
options.parameters?.let {
|
|
884
|
+
(it as Map<String, Any>)
|
|
885
|
+
.forEach { (key, value) -> bodyBuilder.addFormDataPart(key, value.toString()) }
|
|
886
|
+
}
|
|
887
|
+
val mimeType: String = options.mimeType ?: URLConnection.guessContentTypeFromName(file.name)
|
|
888
|
+
|
|
889
|
+
val fieldName = options.fieldName ?: file.name
|
|
890
|
+
bodyBuilder.addFormDataPart(fieldName, file.name, decorator.decorate(file.asRequestBody(mimeType.toMediaTypeOrNull())))
|
|
891
|
+
bodyBuilder.build()
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private data class DownloadResumableTaskParams(
|
|
897
|
+
val options: DownloadOptions,
|
|
898
|
+
val call: Call,
|
|
899
|
+
val file: File,
|
|
900
|
+
val isResume: Boolean,
|
|
901
|
+
val promise: Promise
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
private suspend fun downloadResumableTask(params: DownloadResumableTaskParams) = withContext(Dispatchers.IO) {
|
|
905
|
+
val (options, call, file, isResume, promise) = params
|
|
906
|
+
try {
|
|
907
|
+
val response = call.execute()
|
|
908
|
+
val responseBody = response.body
|
|
909
|
+
val input = BufferedInputStream(responseBody!!.byteStream())
|
|
910
|
+
val output = FileOutputStream(file, isResume)
|
|
911
|
+
val data = ByteArray(1024)
|
|
912
|
+
var count: Int
|
|
913
|
+
while (input.read(data).also { count = it } != -1) {
|
|
914
|
+
output.write(data, 0, count)
|
|
915
|
+
}
|
|
916
|
+
val result = Bundle().apply {
|
|
917
|
+
putString("uri", Uri.fromFile(file).toString())
|
|
918
|
+
putInt("status", response.code)
|
|
919
|
+
putBundle("headers", translateHeaders(response.headers))
|
|
920
|
+
options.md5.takeIf { it }?.let { putString("md5", md5(file)) }
|
|
921
|
+
}
|
|
922
|
+
response.close()
|
|
923
|
+
promise.resolve(result)
|
|
924
|
+
null
|
|
925
|
+
} catch (e: Exception) {
|
|
926
|
+
if (call.isCanceled()) {
|
|
927
|
+
promise.resolve(null)
|
|
928
|
+
return@withContext null
|
|
929
|
+
}
|
|
930
|
+
e.message?.let { Log.e(TAG, it) }
|
|
931
|
+
promise.reject(TAG, e.message, e)
|
|
932
|
+
null
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private open class TaskHandler(val call: Call)
|
|
937
|
+
private class DownloadTaskHandler(val fileUri: Uri, call: Call) : TaskHandler(call)
|
|
938
|
+
|
|
939
|
+
// https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
|
|
940
|
+
private class ProgressResponseBody constructor(private val responseBody: ResponseBody?, private val progressListener: ProgressListener) : ResponseBody() {
|
|
941
|
+
private var bufferedSource: BufferedSource? = null
|
|
942
|
+
|
|
943
|
+
override fun contentType(): MediaType? = responseBody?.contentType()
|
|
944
|
+
|
|
945
|
+
override fun contentLength(): Long = responseBody?.contentLength() ?: -1
|
|
946
|
+
|
|
947
|
+
override fun source(): BufferedSource =
|
|
948
|
+
bufferedSource ?: source(responseBody!!.source()).buffer()
|
|
949
|
+
|
|
950
|
+
private fun source(source: Source): Source {
|
|
951
|
+
return object : ForwardingSource(source) {
|
|
952
|
+
var totalBytesRead = 0L
|
|
953
|
+
|
|
954
|
+
@Throws(IOException::class)
|
|
955
|
+
override fun read(sink: Buffer, byteCount: Long): Long {
|
|
956
|
+
val bytesRead = super.read(sink, byteCount)
|
|
957
|
+
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
|
958
|
+
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
|
959
|
+
progressListener.update(
|
|
960
|
+
totalBytesRead,
|
|
961
|
+
responseBody?.contentLength()
|
|
962
|
+
?: -1,
|
|
963
|
+
bytesRead == -1L
|
|
964
|
+
)
|
|
965
|
+
return bytesRead
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
internal fun interface ProgressListener {
|
|
972
|
+
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
@get:Synchronized
|
|
976
|
+
private val okHttpClient: OkHttpClient?
|
|
977
|
+
get() {
|
|
978
|
+
if (client == null) {
|
|
979
|
+
val builder = OkHttpClient.Builder()
|
|
980
|
+
.connectTimeout(60, TimeUnit.SECONDS)
|
|
981
|
+
.readTimeout(60, TimeUnit.SECONDS)
|
|
982
|
+
.writeTimeout(60, TimeUnit.SECONDS)
|
|
983
|
+
val cookieHandler: CookieHandler = appContext.legacyModule()
|
|
984
|
+
?: throw CookieHandlerNotFoundException()
|
|
985
|
+
|
|
986
|
+
builder.cookieJar(JavaNetCookieJar(cookieHandler))
|
|
987
|
+
client = builder.build()
|
|
988
|
+
}
|
|
989
|
+
return client
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@Throws(IOException::class)
|
|
993
|
+
private fun md5(file: File): String {
|
|
994
|
+
val inputStream: InputStream = FileInputStream(file)
|
|
995
|
+
return inputStream.use {
|
|
996
|
+
val md5bytes = DigestUtils.md5(it)
|
|
997
|
+
String(Hex.encodeHex(md5bytes))
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Concatenated copy of org.apache.commons.io@1.4.0#FileUtils#forceDelete
|
|
1003
|
+
* Newer version of commons-io uses File#toPath() under the hood that unsupported below Android SDK 26
|
|
1004
|
+
* See docs for reference https://commons.apache.org/proper/commons-io/javadocs/api-1.4/index.html
|
|
1005
|
+
*/
|
|
1006
|
+
@Throws(IOException::class)
|
|
1007
|
+
private fun forceDelete(file: File) {
|
|
1008
|
+
if (file.isDirectory) {
|
|
1009
|
+
val files = file.listFiles() ?: throw IOException("Failed to list contents of $file")
|
|
1010
|
+
var exception: IOException? = null
|
|
1011
|
+
for (f in files) {
|
|
1012
|
+
try {
|
|
1013
|
+
forceDelete(f)
|
|
1014
|
+
} catch (ioe: IOException) {
|
|
1015
|
+
exception = ioe
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (null != exception) {
|
|
1019
|
+
throw exception
|
|
1020
|
+
}
|
|
1021
|
+
if (!file.delete()) {
|
|
1022
|
+
throw IOException("Unable to delete directory $file.")
|
|
1023
|
+
}
|
|
1024
|
+
} else if (!file.delete()) {
|
|
1025
|
+
throw IOException("Unable to delete file: $file")
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private fun getFileSize(file: File): Long {
|
|
1030
|
+
if (!file.isDirectory) {
|
|
1031
|
+
return file.length()
|
|
1032
|
+
}
|
|
1033
|
+
val content = file.listFiles() ?: return 0
|
|
1034
|
+
return content.map { getFileSize(it) }.reduceOrNull { total, itemSize -> total + itemSize }
|
|
1035
|
+
?: 0
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
@Throws(IOException::class)
|
|
1039
|
+
private fun getInputStream(uri: Uri) = when {
|
|
1040
|
+
uri.scheme == "file" -> FileInputStream(uri.toFile())
|
|
1041
|
+
uri.scheme == "asset" -> openAssetInputStream(uri)
|
|
1042
|
+
uri.isSAFUri -> context.contentResolver.openInputStream(uri)!!
|
|
1043
|
+
else -> throw IOException("Unsupported scheme for location '$uri'.")
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
@Throws(IOException::class)
|
|
1047
|
+
private fun getOutputStream(uri: Uri) = when {
|
|
1048
|
+
uri.scheme == "file" -> FileOutputStream(uri.toFile())
|
|
1049
|
+
uri.isSAFUri -> context.contentResolver.openOutputStream(uri)!!
|
|
1050
|
+
else -> throw IOException("Unsupported scheme for location '$uri'.")
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private fun getNearestSAFFile(uri: Uri): DocumentFile? {
|
|
1054
|
+
val file = DocumentFile.fromSingleUri(context, uri)
|
|
1055
|
+
return if (file != null && file.isFile) {
|
|
1056
|
+
file
|
|
1057
|
+
} else DocumentFile.fromTreeUri(context, uri)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// extension functions of Uri class
|
|
1061
|
+
private fun Uri.toFile() = if (this.path != null) {
|
|
1062
|
+
File(this.path!!)
|
|
1063
|
+
} else {
|
|
1064
|
+
throw IOException("Invalid Uri: $this")
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private val Uri.isSAFUri: Boolean
|
|
1068
|
+
get() = scheme == "content" && host?.startsWith("com.android.externalstorage") ?: false
|
|
1069
|
+
|
|
1070
|
+
private fun parseFileUri(uriStr: String) = uriStr.substring(uriStr.indexOf(':') + 3)
|
|
1071
|
+
|
|
1072
|
+
@Throws(IOException::class)
|
|
1073
|
+
private fun getInputStreamBytes(inputStream: InputStream): ByteArray {
|
|
1074
|
+
val bytesResult: ByteArray
|
|
1075
|
+
val byteBuffer = ByteArrayOutputStream()
|
|
1076
|
+
val bufferSize = 1024
|
|
1077
|
+
val buffer = ByteArray(bufferSize)
|
|
1078
|
+
try {
|
|
1079
|
+
var len: Int
|
|
1080
|
+
while (inputStream.read(buffer).also { len = it } != -1) {
|
|
1081
|
+
byteBuffer.write(buffer, 0, len)
|
|
1082
|
+
}
|
|
1083
|
+
bytesResult = byteBuffer.toByteArray()
|
|
1084
|
+
} finally {
|
|
1085
|
+
try {
|
|
1086
|
+
byteBuffer.close()
|
|
1087
|
+
} catch (ignored: IOException) {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return bytesResult
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Copied out of React Native's `NetworkingModule.java`
|
|
1094
|
+
private fun translateHeaders(headers: Headers): Bundle {
|
|
1095
|
+
val responseHeaders = Bundle()
|
|
1096
|
+
for (i in 0 until headers.size) {
|
|
1097
|
+
val headerName = headers.name(i)
|
|
1098
|
+
// multiple values for the same header
|
|
1099
|
+
if (responseHeaders.containsKey(headerName)) {
|
|
1100
|
+
responseHeaders.putString(
|
|
1101
|
+
headerName,
|
|
1102
|
+
responseHeaders.getString(headerName) + ", " + headers.value(i)
|
|
1103
|
+
)
|
|
1104
|
+
} else {
|
|
1105
|
+
responseHeaders.putString(headerName, headers.value(i))
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return responseHeaders
|
|
1109
|
+
}
|
|
1110
|
+
}
|