@capacitor/filesystem 7.0.2-nightly-20250526T150552.0 → 7.1.0-dev.2

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.
Files changed (39) hide show
  1. package/CapacitorFilesystem.podspec +4 -3
  2. package/LICENSE +17 -19
  3. package/Package.swift +10 -4
  4. package/README.md +149 -78
  5. package/android/build.gradle +12 -22
  6. package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemErrors.kt +101 -0
  7. package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemMethodOptions.kt +129 -0
  8. package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemMethodResults.kt +65 -0
  9. package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemPlugin.kt +412 -0
  10. package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/LegacyFilesystemImplementation.kt +169 -0
  11. package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/PluginResultExtensions.kt +25 -0
  12. package/dist/docs.json +227 -145
  13. package/dist/esm/definitions.d.ts +102 -64
  14. package/dist/esm/definitions.js +25 -3
  15. package/dist/esm/definitions.js.map +1 -1
  16. package/dist/esm/index.js +3 -1
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/web.d.ts +3 -1
  19. package/dist/esm/web.js +10 -10
  20. package/dist/esm/web.js.map +1 -1
  21. package/dist/plugin.cjs.js +40 -14
  22. package/dist/plugin.cjs.js.map +1 -1
  23. package/dist/plugin.js +41 -16
  24. package/dist/plugin.js.map +1 -1
  25. package/ios/Sources/FilesystemPlugin/CAPPluginCall+Accelerators.swift +73 -0
  26. package/ios/Sources/FilesystemPlugin/FilesystemConstants.swift +61 -0
  27. package/ios/Sources/FilesystemPlugin/FilesystemError.swift +57 -0
  28. package/ios/Sources/FilesystemPlugin/FilesystemLocationResolver.swift +39 -0
  29. package/ios/Sources/FilesystemPlugin/FilesystemOperation.swift +24 -0
  30. package/ios/Sources/FilesystemPlugin/FilesystemOperationExecutor.swift +116 -0
  31. package/ios/Sources/FilesystemPlugin/FilesystemPlugin.swift +103 -264
  32. package/ios/Sources/FilesystemPlugin/IONFileStructures+Converters.swift +60 -0
  33. package/ios/Sources/FilesystemPlugin/{Filesystem.swift → LegacyFilesystemImplementation.swift} +18 -179
  34. package/package.json +28 -24
  35. package/android/src/main/java/com/capacitorjs/plugins/filesystem/Filesystem.java +0 -414
  36. package/android/src/main/java/com/capacitorjs/plugins/filesystem/FilesystemPlugin.java +0 -551
  37. package/android/src/main/java/com/capacitorjs/plugins/filesystem/exceptions/CopyFailedException.java +0 -16
  38. package/android/src/main/java/com/capacitorjs/plugins/filesystem/exceptions/DirectoryExistsException.java +0 -16
  39. package/android/src/main/java/com/capacitorjs/plugins/filesystem/exceptions/DirectoryNotFoundException.java +0 -16
@@ -0,0 +1,129 @@
1
+ package com.capacitorjs.plugins.filesystem
2
+
3
+ import com.getcapacitor.PluginCall
4
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEEncoding
5
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEFolderType
6
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEReadInChunksOptions
7
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEReadOptions
8
+ import io.ionic.libs.ionfilesystemlib.model.IONFILESaveMode
9
+ import io.ionic.libs.ionfilesystemlib.model.IONFILESaveOptions
10
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEUri
11
+
12
+ internal const val INPUT_APPEND = "append"
13
+ private const val INPUT_PATH = "path"
14
+ private const val INPUT_DIRECTORY = "directory"
15
+ private const val INPUT_ENCODING = "encoding"
16
+ private const val INPUT_CHUNK_SIZE = "chunkSize"
17
+ private const val INPUT_DATA = "data"
18
+ private const val INPUT_RECURSIVE = "recursive"
19
+ private const val INPUT_FROM = "from"
20
+ private const val INPUT_FROM_DIRECTORY = "directory"
21
+ private const val INPUT_TO = "to"
22
+ private const val INPUT_TO_DIRECTORY = "toDirectory"
23
+
24
+ internal data class ReadFileOptions(
25
+ val uri: IONFILEUri.Unresolved,
26
+ val options: IONFILEReadOptions
27
+ )
28
+
29
+ internal data class ReadFileInChunksOptions(
30
+ val uri: IONFILEUri.Unresolved,
31
+ val options: IONFILEReadInChunksOptions
32
+ )
33
+
34
+ internal data class WriteFileOptions(
35
+ val uri: IONFILEUri.Unresolved,
36
+ val options: IONFILESaveOptions
37
+ )
38
+
39
+ internal data class SingleUriWithRecursiveOptions(
40
+ val uri: IONFILEUri.Unresolved,
41
+ val recursive: Boolean
42
+ )
43
+
44
+ internal data class DoubleUri(
45
+ val fromUri: IONFILEUri.Unresolved,
46
+ val toUri: IONFILEUri.Unresolved,
47
+ )
48
+
49
+ /**
50
+ * @return [ReadFileOptions] from JSON inside [PluginCall], or null if input is invalid
51
+ */
52
+ internal fun PluginCall.getReadFileOptions(): ReadFileOptions? {
53
+ val uri = getSingleIONFILEUri() ?: return null
54
+ val encoding = IONFILEEncoding.fromEncodingName(getString(INPUT_ENCODING))
55
+ return ReadFileOptions(uri = uri, options = IONFILEReadOptions(encoding))
56
+ }
57
+
58
+ /**
59
+ * @return [ReadFileInChunksOptions] from JSON inside [PluginCall], or null if input is invalid
60
+ */
61
+ internal fun PluginCall.getReadFileInChunksOptions(): ReadFileInChunksOptions? {
62
+ val uri = getSingleIONFILEUri() ?: return null
63
+ val encoding = IONFILEEncoding.fromEncodingName(getString(INPUT_ENCODING))
64
+ val chunkSize = getInt(INPUT_CHUNK_SIZE)?.takeIf { it > 0 } ?: return null
65
+ return ReadFileInChunksOptions(
66
+ uri = uri,
67
+ options = IONFILEReadInChunksOptions(encoding, chunkSize)
68
+ )
69
+ }
70
+
71
+ /**
72
+ * @return [ReadFileOptions] from JSON inside [PluginCall], or null if input is invalid
73
+ */
74
+ internal fun PluginCall.getWriteFileOptions(): WriteFileOptions? {
75
+ val uri = getSingleIONFILEUri() ?: return null
76
+ val data = getString(INPUT_DATA) ?: return null
77
+ val recursive = getBoolean(INPUT_RECURSIVE) ?: false
78
+ val append = getBoolean(INPUT_APPEND) ?: false
79
+ val saveMode = if (append) IONFILESaveMode.APPEND else IONFILESaveMode.WRITE
80
+ val encoding = IONFILEEncoding.fromEncodingName(getString(INPUT_ENCODING))
81
+ return WriteFileOptions(
82
+ uri = uri,
83
+ options = IONFILESaveOptions(
84
+ data = data,
85
+ encoding = encoding,
86
+ mode = saveMode,
87
+ createFileRecursive = recursive
88
+ )
89
+ )
90
+ }
91
+
92
+ /**
93
+ * @return [SingleUriWithRecursiveOptions] from JSON inside [PluginCall], or null if input is invalid
94
+ */
95
+ internal fun PluginCall.getSingleUriWithRecursiveOptions(): SingleUriWithRecursiveOptions? {
96
+ val uri = getSingleIONFILEUri() ?: return null
97
+ val recursive = getBoolean(INPUT_RECURSIVE) ?: false
98
+ return SingleUriWithRecursiveOptions(uri = uri, recursive = recursive)
99
+ }
100
+
101
+ /**
102
+ * @return two uris in form of [DoubleUri] from JSON inside [PluginCall], or null if input is invalid
103
+ */
104
+ internal fun PluginCall.getDoubleIONFILEUri(): DoubleUri? {
105
+ val fromPath = getString(INPUT_FROM) ?: return null
106
+ val fromFolder = IONFILEFolderType.fromStringAlias(getString(INPUT_FROM_DIRECTORY))
107
+ val toPath = getString(INPUT_TO) ?: return null
108
+ val toFolder = getString(INPUT_TO_DIRECTORY)?.let { toDirectory ->
109
+ IONFILEFolderType.fromStringAlias(toDirectory)
110
+ } ?: fromFolder
111
+ return DoubleUri(
112
+ fromUri = IONFILEUri.Unresolved(fromFolder, fromPath),
113
+ toUri = IONFILEUri.Unresolved(toFolder, toPath),
114
+ )
115
+ }
116
+
117
+ /**
118
+ * return a single [IONFILEUri.Unresolved] from JSON inside [PluginCall], or null if input is invalid
119
+ */
120
+ internal fun PluginCall.getSingleIONFILEUri(): IONFILEUri.Unresolved? {
121
+ val path = getString(INPUT_PATH) ?: return null
122
+ val directoryAlias = getString(INPUT_DIRECTORY)
123
+ return unresolvedUri(path, directoryAlias)
124
+ }
125
+
126
+ private fun unresolvedUri(path: String, directoryAlias: String?) = IONFILEUri.Unresolved(
127
+ parentFolder = IONFILEFolderType.fromStringAlias(directoryAlias),
128
+ uriPath = path
129
+ )
@@ -0,0 +1,65 @@
1
+ package com.capacitorjs.plugins.filesystem
2
+
3
+ import android.net.Uri
4
+ import com.getcapacitor.JSArray
5
+ import com.getcapacitor.JSObject
6
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEFileType
7
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEMetadataResult
8
+ import io.ionic.libs.ionfilesystemlib.model.IONFILESaveMode
9
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEUri
10
+
11
+ private val OUTPUT_DATA = "data"
12
+ private val OUTPUT_NAME = "name"
13
+ private val OUTPUT_TYPE = "type"
14
+ private val OUTPUT_SIZE = "size"
15
+ private val OUTPUT_MODIFIED_TIME = "mtime"
16
+ private val OUTPUT_CREATED_TIME = "ctime"
17
+ private val OUTPUT_URI = "uri"
18
+ private val OUTPUT_FILES = "files"
19
+
20
+ /**
21
+ * @return a result [JSObject] for reading a file
22
+ */
23
+ fun createReadResultObject(readData: String): JSObject =
24
+ JSObject().also { it.putOpt(OUTPUT_DATA, readData) }
25
+
26
+
27
+ /**
28
+ * @return a result [JSObject] for writing/append a file
29
+ */
30
+ fun createWriteResultObject(uri: Uri, mode: IONFILESaveMode): JSObject? =
31
+ if (mode == IONFILESaveMode.APPEND) {
32
+ null
33
+ } else {
34
+ createUriResultObject(uri)
35
+ }
36
+
37
+ /**
38
+ * @return a result [JSObject] for the list of a directories contents
39
+ */
40
+ fun createReadDirResultObject(list: List<IONFILEMetadataResult>): JSObject = JSObject().also {
41
+ it.put(OUTPUT_FILES, JSArray(list.map { child -> child.toResultObject() }))
42
+ }
43
+
44
+ /**
45
+ * @return a result [JSObject] for stat, from the [IONFILEMetadataResult] object
46
+ */
47
+ fun IONFILEMetadataResult.toResultObject(): JSObject = JSObject().apply {
48
+ put(OUTPUT_NAME, name)
49
+ put(OUTPUT_TYPE, if (type is IONFILEFileType.Directory) "directory" else "file")
50
+ put(OUTPUT_SIZE, size)
51
+ put(OUTPUT_MODIFIED_TIME, lastModifiedTimestamp)
52
+ put(OUTPUT_CREATED_TIME, createdTimestamp)
53
+ put(OUTPUT_URI, uri)
54
+ }
55
+
56
+ /**
57
+ * @return a result [JSObject] based on a resolved uri [IONFILEUri.Resolved]
58
+ */
59
+ fun IONFILEUri.Resolved.toResultObject(): JSObject = createUriResultObject(this.uri)
60
+
61
+ /**
62
+ * @return a result [JSObject] for an Android [Uri]
63
+ */
64
+ fun createUriResultObject(uri: Uri): JSObject =
65
+ JSObject().also { it.put(OUTPUT_URI, uri.toString()) }
@@ -0,0 +1,412 @@
1
+ package com.capacitorjs.plugins.filesystem
2
+
3
+ import android.Manifest
4
+ import android.media.MediaScannerConnection
5
+ import android.os.Build
6
+ import android.os.Environment
7
+ import android.util.Log
8
+ import com.getcapacitor.JSObject
9
+ import com.getcapacitor.Logger
10
+ import com.getcapacitor.PermissionState
11
+ import com.getcapacitor.Plugin
12
+ import com.getcapacitor.PluginCall
13
+ import com.getcapacitor.PluginMethod
14
+ import com.getcapacitor.annotation.CapacitorPlugin
15
+ import com.getcapacitor.annotation.Permission
16
+ import com.getcapacitor.annotation.PermissionCallback
17
+ import com.getcapacitor.plugin.util.HttpRequestHandler.ProgressEmitter
18
+ import io.ionic.libs.ionfilesystemlib.IONFILEController
19
+ import io.ionic.libs.ionfilesystemlib.model.IONFILECreateOptions
20
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEDeleteOptions
21
+ import io.ionic.libs.ionfilesystemlib.model.IONFILEUri
22
+ import kotlinx.coroutines.CoroutineScope
23
+ import kotlinx.coroutines.Dispatchers
24
+ import kotlinx.coroutines.cancel
25
+ import kotlinx.coroutines.flow.catch
26
+ import kotlinx.coroutines.flow.launchIn
27
+ import kotlinx.coroutines.flow.onCompletion
28
+ import kotlinx.coroutines.flow.onEach
29
+ import kotlinx.coroutines.launch
30
+ import org.json.JSONException
31
+
32
+ private const val PUBLIC_STORAGE = "publicStorage"
33
+ private const val PUBLIC_STORAGE_ABOVE_ANDROID_10 = "publicStorageAboveAPI29"
34
+ private const val PERMISSION_GRANTED = "granted"
35
+
36
+ @CapacitorPlugin(
37
+ name = "Filesystem",
38
+ permissions = [
39
+ Permission(
40
+ strings = [Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE],
41
+ alias = PUBLIC_STORAGE
42
+ ),
43
+ /*
44
+ For SDK versions 30-32 (Android 11 and Android 12)
45
+ Could be that certain files may require read permission, such as local file path to photos/videos in gallery
46
+ */
47
+ Permission(
48
+ strings = [Manifest.permission.READ_EXTERNAL_STORAGE],
49
+ alias = PUBLIC_STORAGE_ABOVE_ANDROID_10
50
+ )
51
+ ]
52
+ )
53
+ class FilesystemPlugin : Plugin() {
54
+
55
+ private var legacyImplementation: LegacyFilesystemImplementation? = null
56
+
57
+ private val coroutineScope: CoroutineScope by lazy { CoroutineScope(Dispatchers.Main) }
58
+ private val controller: IONFILEController by lazy { IONFILEController(context.applicationContext) }
59
+
60
+ override fun load() {
61
+ super.load()
62
+ legacyImplementation = LegacyFilesystemImplementation(context)
63
+ }
64
+
65
+ override fun handleOnDestroy() {
66
+ super.handleOnDestroy()
67
+ coroutineScope.cancel()
68
+ }
69
+
70
+ @PluginMethod
71
+ fun readFile(call: PluginCall) {
72
+ val input: ReadFileOptions = call.getReadFileOptions() ?: run {
73
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
74
+ return
75
+ }
76
+ runWithPermission(input.uri, call) { uri ->
77
+ controller.readFile(uri, input.options)
78
+ .onSuccess { call.sendSuccess(result = createReadResultObject(it)) }
79
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
80
+ }
81
+ }
82
+
83
+ @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
84
+ fun readFileInChunks(call: PluginCall) {
85
+ val input: ReadFileInChunksOptions = call.getReadFileInChunksOptions() ?: run {
86
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
87
+ return
88
+ }
89
+ runWithPermission(input.uri, call) { uri ->
90
+ controller.readFileInChunks(uri, input.options)
91
+ .onEach { chunk ->
92
+ call.sendSuccess(result = createReadResultObject(chunk), keepCallback = true)
93
+ }
94
+ .onCompletion { error ->
95
+ if (error == null) {
96
+ call.sendSuccess(result = createReadResultObject(""))
97
+ }
98
+ }
99
+ .catch {
100
+ call.sendError(it.toFilesystemError(call.methodName))
101
+ }
102
+ .launchIn(coroutineScope)
103
+ }
104
+ }
105
+
106
+ @PluginMethod
107
+ fun writeFile(call: PluginCall) {
108
+ val input: WriteFileOptions = call.getWriteFileOptions() ?: run {
109
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
110
+ return
111
+ }
112
+ runWithPermission(input.uri, call) { uri ->
113
+ controller.saveFile(uri, input.options)
114
+ .onSuccess { uriSaved ->
115
+ // update mediaStore index only if file was written to external storage
116
+ if (uri.inExternalStorage) {
117
+ uriSaved.path?.let {
118
+ MediaScannerConnection.scanFile(context, arrayOf(it), null, null)
119
+ }
120
+ }
121
+ call.sendSuccess(result = createWriteResultObject(uriSaved, input.options.mode))
122
+ }
123
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
124
+ }
125
+ }
126
+
127
+ @PluginMethod
128
+ fun appendFile(call: PluginCall) {
129
+ try {
130
+ call.data.putOpt(INPUT_APPEND, true)
131
+ } catch (ex: JSONException) {
132
+ Log.e(logTag, "Tried to set `append` in `PluginCall`, but got exception", ex)
133
+ call.sendError(
134
+ FilesystemErrors.operationFailed(call.methodName, ex.localizedMessage ?: "")
135
+ )
136
+ return
137
+ }
138
+ writeFile(call)
139
+ }
140
+
141
+ @PluginMethod
142
+ fun deleteFile(call: PluginCall) {
143
+ val input = call.getSingleIONFILEUri() ?: run {
144
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
145
+ return
146
+ }
147
+ runWithPermission(input, call) { uri ->
148
+ controller.delete(uri, IONFILEDeleteOptions(recursive = false))
149
+ .onSuccess { call.sendSuccess() }
150
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
151
+ }
152
+ }
153
+
154
+ @PluginMethod
155
+ fun mkdir(call: PluginCall) {
156
+ val input = call.getSingleUriWithRecursiveOptions() ?: run {
157
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
158
+ return
159
+ }
160
+ runWithPermission(input.uri, call) { uri ->
161
+ controller.createDirectory(uri, IONFILECreateOptions(input.recursive))
162
+ .onSuccess { call.sendSuccess() }
163
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
164
+ }
165
+ }
166
+
167
+ @PluginMethod
168
+ fun rmdir(call: PluginCall) {
169
+ val input = call.getSingleUriWithRecursiveOptions() ?: run {
170
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
171
+ return
172
+ }
173
+ runWithPermission(input.uri, call) { uri ->
174
+ controller.delete(uri, IONFILEDeleteOptions(input.recursive))
175
+ .onSuccess { call.sendSuccess() }
176
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
177
+ }
178
+ }
179
+
180
+ @PluginMethod
181
+ fun readdir(call: PluginCall) {
182
+ val input = call.getSingleIONFILEUri() ?: run {
183
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
184
+ return
185
+ }
186
+ runWithPermission(input, call) { uri ->
187
+ controller.listDirectory(uri)
188
+ .onSuccess { call.sendSuccess(result = createReadDirResultObject(it)) }
189
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
190
+ }
191
+ }
192
+
193
+ @PluginMethod
194
+ fun getUri(call: PluginCall) {
195
+ val input = call.getSingleIONFILEUri() ?: run {
196
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
197
+ return
198
+ }
199
+ coroutineScope.launch {
200
+ controller.getFileUri(input)
201
+ .onSuccess { resolvedUri -> call.sendSuccess(result = resolvedUri.toResultObject()) }
202
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
203
+ }
204
+ }
205
+
206
+ @PluginMethod
207
+ fun stat(call: PluginCall) {
208
+ val input = call.getSingleIONFILEUri() ?: run {
209
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
210
+ return
211
+ }
212
+ runWithPermission(input, call) { uri ->
213
+ controller.getMetadata(uri)
214
+ .onSuccess { metadata -> call.sendSuccess(result = metadata.toResultObject()) }
215
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
216
+ }
217
+ }
218
+
219
+ @PluginMethod
220
+ fun rename(call: PluginCall) {
221
+ val input = call.getDoubleIONFILEUri() ?: run {
222
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
223
+ return
224
+ }
225
+ runWithPermission(input.fromUri, input.toUri, call) { source, destination ->
226
+ controller.move(source, destination)
227
+ .onSuccess { call.sendSuccess() }
228
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
229
+ }
230
+ }
231
+
232
+ @PluginMethod
233
+ fun copy(call: PluginCall) {
234
+ val input = call.getDoubleIONFILEUri() ?: run {
235
+ call.sendError(FilesystemErrors.invalidInputMethod(call.methodName))
236
+ return
237
+ }
238
+ runWithPermission(input.fromUri, input.toUri, call) { source, destination ->
239
+ controller.copy(source, destination)
240
+ .onSuccess { call.sendSuccess(createUriResultObject(it)) }
241
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
242
+ }
243
+ }
244
+
245
+ @PluginMethod
246
+ @Deprecated("Use @capacitor/file-transfer plugin instead")
247
+ fun downloadFile(call: PluginCall) {
248
+ try {
249
+ val directory = call.getString("directory", Environment.DIRECTORY_DOWNLOADS)
250
+
251
+ if (legacyImplementation?.isPublicDirectory(directory) == true &&
252
+ !isStoragePermissionGranted(false)
253
+ ) {
254
+ requestAllPermissions(call, "permissionCallback")
255
+ return
256
+ }
257
+
258
+ val emitter = ProgressEmitter { bytes: Int?, contentLength: Int? ->
259
+ val ret = JSObject()
260
+ ret.put("url", call.getString("url"))
261
+ ret.put("bytes", bytes)
262
+ ret.put("contentLength", contentLength)
263
+ notifyListeners("progress", ret)
264
+ }
265
+
266
+ legacyImplementation?.downloadFile(
267
+ call,
268
+ bridge,
269
+ emitter,
270
+ object : LegacyFilesystemImplementation.FilesystemDownloadCallback {
271
+ override fun onSuccess(result: JSObject) {
272
+ // update mediaStore index only if file was written to external storage
273
+ if (legacyImplementation?.isPublicDirectory(directory) == true) {
274
+ MediaScannerConnection.scanFile(
275
+ context,
276
+ arrayOf(result.getString("path")),
277
+ null,
278
+ null
279
+ )
280
+ }
281
+ call.resolve(result)
282
+ }
283
+
284
+ override fun onError(error: Exception) {
285
+ call.reject("Error downloading file: " + error.localizedMessage, error)
286
+ }
287
+ }
288
+ )
289
+ } catch (ex: Exception) {
290
+ call.reject("Error downloading file: " + ex.localizedMessage, ex)
291
+ }
292
+ }
293
+
294
+ @PluginMethod
295
+ override fun checkPermissions(call: PluginCall) {
296
+ if (isStoragePermissionGranted(false)) {
297
+ call.sendSuccess(JSObject().also { it.put(PUBLIC_STORAGE, PERMISSION_GRANTED) })
298
+ } else {
299
+ super.checkPermissions(call)
300
+ }
301
+ }
302
+
303
+ @PluginMethod
304
+ override fun requestPermissions(call: PluginCall) {
305
+ if (isStoragePermissionGranted(false)) {
306
+ call.sendSuccess(JSObject().also { it.put(PUBLIC_STORAGE, PERMISSION_GRANTED) })
307
+ } else {
308
+ super.requestPermissions(call)
309
+ }
310
+ }
311
+
312
+ @PermissionCallback
313
+ private fun permissionCallback(call: PluginCall) {
314
+ if (!isStoragePermissionGranted(true)) {
315
+ Logger.debug(logTag, "User denied storage permission")
316
+ call.sendError(FilesystemErrors.filePermissionsDenied)
317
+ return
318
+ }
319
+
320
+ when (call.methodName) {
321
+ // appendFile and writeFile have the same implementation, hence the same method is called;
322
+ // the only difference being that we add a boolean for append in the PluginCall,
323
+ // which is done before this method is called.
324
+ "appendFile", "writeFile" -> writeFile(call)
325
+ "deleteFile" -> deleteFile(call)
326
+ "mkdir" -> mkdir(call)
327
+ "rmdir" -> rmdir(call)
328
+ "rename" -> rename(call)
329
+ "copy" -> copy(call)
330
+ "readFile" -> readFile(call)
331
+ "readFileInChunks" -> readFileInChunks(call)
332
+ "readdir" -> readdir(call)
333
+ "getUri" -> getUri(call)
334
+ "stat" -> stat(call)
335
+ "downloadFile" -> downloadFile(call)
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Runs a suspend code if the app has permission to access the uri
341
+ *
342
+ * Will ask for permission if it has not been granted.
343
+ *
344
+ * May return an error if the uri is not resolvable.
345
+ *
346
+ * @param uri the uri pointing to the file / directory
347
+ * @param call the capacitor plugin call
348
+ * @param onPermissionGranted the callback to run the suspending code
349
+ */
350
+ private fun runWithPermission(
351
+ uri: IONFILEUri.Unresolved,
352
+ call: PluginCall,
353
+ onPermissionGranted: suspend (resolvedUri: IONFILEUri.Resolved) -> Unit
354
+ ) {
355
+ coroutineScope.launch {
356
+ controller.getFileUri(uri)
357
+ .onSuccess { resolvedUri ->
358
+ // certain files like a photo/video in gallery may require read permission on Android 11 and 12.
359
+ if (
360
+ resolvedUri.inExternalStorage
361
+ && !isStoragePermissionGranted(shouldRequestAboveAndroid10 = uri.parentFolder == null)
362
+ ) {
363
+ requestAllPermissions(call, this@FilesystemPlugin::permissionCallback.name)
364
+ } else {
365
+ onPermissionGranted(resolvedUri)
366
+ }
367
+ }
368
+ .onFailure { call.sendError(it.toFilesystemError(call.methodName)) }
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Runs a suspend code if the app has permission to access both to and from uris
374
+ *
375
+ * Will ask for permission if it has not been granted.
376
+ *
377
+ * May return an error if the uri is not resolvable.
378
+ *
379
+ * @param fromUri the source uri pointing to the file / directory
380
+ * @param toUri the destination uri pointing to the file / directory
381
+ * @param call the capacitor plugin call
382
+ * @param onPermissionGranted the callback to run the suspending code
383
+ */
384
+ private fun runWithPermission(
385
+ fromUri: IONFILEUri.Unresolved,
386
+ toUri: IONFILEUri.Unresolved,
387
+ call: PluginCall,
388
+ onPermissionGranted: suspend (resolvedSourceUri: IONFILEUri.Resolved, resolvedDestinationUri: IONFILEUri.Resolved) -> Unit
389
+ ) {
390
+ runWithPermission(fromUri, call) { resolvedSourceUri ->
391
+ runWithPermission(toUri, call) { resolvedDestinationUri ->
392
+ onPermissionGranted(resolvedSourceUri, resolvedDestinationUri)
393
+ }
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Checks the the given permission is granted or not
399
+ * @param shouldRequestAboveAndroid10 whether or not should check for read permission above android 10
400
+ * May vary with the kind of file path that is provided.
401
+ * @return Returns true if the app is running on Android 13 (API 33) or newer, or if the permission is already granted
402
+ * or false if it is denied.
403
+ */
404
+ private fun isStoragePermissionGranted(shouldRequestAboveAndroid10: Boolean): Boolean = when {
405
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> true
406
+
407
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
408
+ !shouldRequestAboveAndroid10 || getPermissionState(PUBLIC_STORAGE_ABOVE_ANDROID_10) == PermissionState.GRANTED
409
+
410
+ else -> getPermissionState(PUBLIC_STORAGE) == PermissionState.GRANTED
411
+ }
412
+ }