@capacitor/filesystem 7.0.2-nightly-20250526T150552.0 → 7.1.0-dev.1
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/CapacitorFilesystem.podspec +4 -3
- package/Package.swift +10 -4
- package/README.md +149 -78
- package/android/build.gradle +12 -22
- package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemErrors.kt +101 -0
- package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemMethodOptions.kt +129 -0
- package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemMethodResults.kt +65 -0
- package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/FilesystemPlugin.kt +412 -0
- package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/LegacyFilesystemImplementation.kt +169 -0
- package/android/src/main/kotlin/com/capacitorjs/plugins/filesystem/PluginResultExtensions.kt +25 -0
- package/dist/docs.json +227 -145
- package/dist/esm/definitions.d.ts +102 -64
- package/dist/esm/definitions.js +25 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +3 -1
- package/dist/esm/web.js +10 -10
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +40 -14
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +41 -16
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/FilesystemPlugin/CAPPluginCall+Accelerators.swift +73 -0
- package/ios/Sources/FilesystemPlugin/FilesystemConstants.swift +61 -0
- package/ios/Sources/FilesystemPlugin/FilesystemError.swift +57 -0
- package/ios/Sources/FilesystemPlugin/FilesystemLocationResolver.swift +39 -0
- package/ios/Sources/FilesystemPlugin/FilesystemOperation.swift +24 -0
- package/ios/Sources/FilesystemPlugin/FilesystemOperationExecutor.swift +116 -0
- package/ios/Sources/FilesystemPlugin/FilesystemPlugin.swift +103 -264
- package/ios/Sources/FilesystemPlugin/IONFileStructures+Converters.swift +60 -0
- package/ios/Sources/FilesystemPlugin/{Filesystem.swift → LegacyFilesystemImplementation.swift} +18 -179
- package/package.json +28 -24
- package/LICENSE +0 -23
- package/android/src/main/java/com/capacitorjs/plugins/filesystem/Filesystem.java +0 -414
- package/android/src/main/java/com/capacitorjs/plugins/filesystem/FilesystemPlugin.java +0 -551
- package/android/src/main/java/com/capacitorjs/plugins/filesystem/exceptions/CopyFailedException.java +0 -16
- package/android/src/main/java/com/capacitorjs/plugins/filesystem/exceptions/DirectoryExistsException.java +0 -16
- 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
|
+
}
|