expo-file-system 18.2.0-canary-20250709-136b77f → 18.2.0-canary-20250722-599a28f

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 (74) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/android/build.gradle +3 -2
  3. package/android/src/main/java/expo/modules/filesystem/next/FileSystemDirectory.kt +50 -13
  4. package/android/src/main/java/expo/modules/filesystem/next/FileSystemFile.kt +30 -47
  5. package/android/src/main/java/expo/modules/filesystem/next/FileSystemFileHandle.kt +1 -1
  6. package/android/src/main/java/expo/modules/filesystem/next/FileSystemNextModule.kt +19 -7
  7. package/android/src/main/java/expo/modules/filesystem/next/FileSystemNextRecords.kt +10 -0
  8. package/android/src/main/java/expo/modules/filesystem/next/FileSystemPath.kt +81 -28
  9. package/android/src/main/java/expo/modules/filesystem/next/unifiedfile/JavaFile.kt +34 -0
  10. package/android/src/main/java/expo/modules/filesystem/next/unifiedfile/SAFDocumentFile.kt +38 -0
  11. package/android/src/main/java/expo/modules/filesystem/next/unifiedfile/UnifiedFileInterface.kt +15 -0
  12. package/build/next/ExpoFileSystem.types.d.ts +34 -0
  13. package/build/next/ExpoFileSystem.types.d.ts.map +1 -1
  14. package/build/next/FileSystem.d.ts +2 -0
  15. package/build/next/FileSystem.d.ts.map +1 -1
  16. package/build/next/index.d.ts +1 -1
  17. package/build/next/index.d.ts.map +1 -1
  18. package/build/next/pathUtilities/index.d.ts.map +1 -1
  19. package/build/next/pathUtilities/url.d.ts +3 -4
  20. package/build/next/pathUtilities/url.d.ts.map +1 -1
  21. package/expo-module.config.json +1 -1
  22. package/ios/Next/FileSystemDirectory.swift +24 -0
  23. package/ios/Next/FileSystemFile.swift +0 -27
  24. package/ios/Next/FileSystemNextModule.swift +4 -0
  25. package/ios/Next/FileSystemNextRecords.swift +9 -0
  26. package/ios/Next/FileSystemPath.swift +27 -0
  27. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/{18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f-sources.jar → 18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f-sources.jar} +0 -0
  28. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f-sources.jar.md5 +1 -0
  29. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f-sources.jar.sha1 +1 -0
  30. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f-sources.jar.sha256 +1 -0
  31. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f-sources.jar.sha512 +1 -0
  32. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.aar +0 -0
  33. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.aar.md5 +1 -0
  34. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.aar.sha1 +1 -0
  35. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.aar.sha256 +1 -0
  36. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.aar.sha512 +1 -0
  37. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/{18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.module → 18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.module} +36 -22
  38. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.module.md5 +1 -0
  39. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.module.sha1 +1 -0
  40. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.module.sha256 +1 -0
  41. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.module.sha512 +1 -0
  42. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/{18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.pom → 18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.pom} +7 -1
  43. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.pom.md5 +1 -0
  44. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.pom.sha1 +1 -0
  45. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.pom.sha256 +1 -0
  46. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250722-599a28f/expo.modules.filesystem-18.2.0-canary-20250722-599a28f.pom.sha512 +1 -0
  47. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/maven-metadata.xml +4 -4
  48. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/maven-metadata.xml.md5 +1 -1
  49. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/maven-metadata.xml.sha1 +1 -1
  50. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/maven-metadata.xml.sha256 +1 -1
  51. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/maven-metadata.xml.sha512 +1 -1
  52. package/package.json +4 -4
  53. package/src/next/ExpoFileSystem.types.ts +38 -0
  54. package/src/next/FileSystem.ts +11 -2
  55. package/src/next/index.ts +1 -0
  56. package/src/next/pathUtilities/index.ts +29 -21
  57. package/src/next/pathUtilities/url.ts +17 -32
  58. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f-sources.jar.md5 +0 -1
  59. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f-sources.jar.sha1 +0 -1
  60. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f-sources.jar.sha256 +0 -1
  61. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f-sources.jar.sha512 +0 -1
  62. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.aar +0 -0
  63. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.aar.md5 +0 -1
  64. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.aar.sha1 +0 -1
  65. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.aar.sha256 +0 -1
  66. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.aar.sha512 +0 -1
  67. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.module.md5 +0 -1
  68. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.module.sha1 +0 -1
  69. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.module.sha256 +0 -1
  70. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.module.sha512 +0 -1
  71. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.pom.md5 +0 -1
  72. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.pom.sha1 +0 -1
  73. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.pom.sha256 +0 -1
  74. package/local-maven-repo/host/exp/exponent/expo.modules.filesystem/18.2.0-canary-20250709-136b77f/expo.modules.filesystem-18.2.0-canary-20250709-136b77f.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -6,9 +6,11 @@
6
6
 
7
7
  ### 🎉 New features
8
8
 
9
+ - Add directory info function ([#37910](https://github.com/expo/expo/pull/37910) by [@Wenszel](https://github.com/Wenszel))
9
10
  - Add total and available sizes, directory sizes. ([#37594](https://github.com/expo/expo/pull/37594) by [@aleqsio](https://github.com/aleqsio))
10
11
  - Add info method, modificationTime and creationTime properties to file-system/next. ([#37505](https://github.com/expo/expo/pull/37505) by [@Wenszel](https://github.com/Wenszel))
11
12
  - Add support for custom headers in downloadFileAsync ([#36108](https://github.com/expo/expo/pull/36108) by [@leonhh](https://github.com/leonhh))
13
+ - [next] Add limited support for SAF Uris. ([#38075](https://github.com/expo/expo/pull/38075) by [@aleqsio](https://github.com/aleqsio))
12
14
 
13
15
  ### 🐛 Bug fixes
14
16
 
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '18.2.0-canary-20250709-136b77f'
7
+ version = '18.2.0-canary-20250722-599a28f'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.filesystem"
11
11
  defaultConfig {
12
12
  versionCode 30
13
- versionName "18.2.0-canary-20250709-136b77f"
13
+ versionName "18.2.0-canary-20250722-599a28f"
14
14
  }
15
15
  }
16
16
 
@@ -21,4 +21,5 @@ dependencies {
21
21
  api 'com.squareup.okhttp3:okhttp-urlconnection:4.9.2'
22
22
  api 'com.squareup.okio:okio:2.9.0'
23
23
  api "androidx.legacy:legacy-support-v4:1.0.0"
24
+ api "androidx.documentfile:documentfile:1.1.0"
24
25
  }
@@ -1,23 +1,23 @@
1
1
  package expo.modules.filesystem.next
2
2
 
3
3
  import android.net.Uri
4
+ import expo.modules.filesystem.slashifyFilePath
4
5
  import expo.modules.interfaces.filesystem.Permission
5
- import java.io.File
6
6
 
7
- class FileSystemDirectory(file: File) : FileSystemPath(file) {
7
+ class FileSystemDirectory(uri: Uri) : FileSystemPath(uri) {
8
8
  fun validatePath() {
9
9
  // Kept empty for now, but can be used to validate if the path is a valid directory path.
10
10
  }
11
11
 
12
12
  override fun validateType() {
13
- if (file.exists() && !file.isDirectory) {
13
+ if (file.exists() && !file.isDirectory()) {
14
14
  throw InvalidTypeFolderException()
15
15
  }
16
16
  }
17
17
 
18
18
  val exists: Boolean get() {
19
19
  return if (checkPermission(Permission.READ)) {
20
- file.isDirectory
20
+ file.isDirectory()
21
21
  } else {
22
22
  false
23
23
  }
@@ -26,7 +26,34 @@ class FileSystemDirectory(file: File) : FileSystemPath(file) {
26
26
  val size: Long get() {
27
27
  validatePermission(Permission.READ)
28
28
  validateType()
29
- return file.walkTopDown().filter { it.isFile }.map { it.length() }.sum()
29
+ return javaFile.walkTopDown().filter { it.isFile }.map { it.length() }.sum()
30
+ }
31
+
32
+ fun info(): DirectoryInfo {
33
+ validateType()
34
+ validatePermission(Permission.READ)
35
+ if (!file.exists()) {
36
+ val directoryInfo = DirectoryInfo(
37
+ exists = false,
38
+ uri = slashifyFilePath(javaFile.toURI().toString())
39
+ )
40
+ return directoryInfo
41
+ }
42
+
43
+ when {
44
+ javaFile.toURI().scheme == "file" -> {
45
+ val directoryInfo = DirectoryInfo(
46
+ exists = true,
47
+ uri = slashifyFilePath(javaFile.toURI().toString()),
48
+ files = javaFile.listFiles()?.map { i -> i.name },
49
+ modificationTime = modificationTime,
50
+ creationTime = creationTime,
51
+ size = size
52
+ )
53
+ return directoryInfo
54
+ }
55
+ else -> throw UnableToGetInfoException("file schema ${javaFile.toURI().scheme} is not supported")
56
+ }
30
57
  }
31
58
 
32
59
  fun create(options: CreateOptions = CreateOptions()) {
@@ -34,33 +61,43 @@ class FileSystemDirectory(file: File) : FileSystemPath(file) {
34
61
  validatePermission(Permission.WRITE)
35
62
  validateCanCreate(options)
36
63
  if (options.overwrite && file.exists()) {
37
- file.delete()
64
+ javaFile.delete()
38
65
  }
39
66
  val created = if (options.intermediates) {
40
- file.mkdirs()
67
+ javaFile.mkdirs()
41
68
  } else {
42
- file.mkdir()
69
+ javaFile.mkdir()
43
70
  }
44
71
  if (!created) {
45
72
  throw UnableToCreateException("directory already exists or could not be created")
46
73
  }
47
74
  }
48
75
 
76
+ fun createFile(mimeType: String?, fileName: String): FileSystemFile {
77
+ val newFile = file.createFile(mimeType ?: "text/plain", fileName) ?: throw UnableToCreateException("file could not be created")
78
+ return FileSystemFile(newFile.uri)
79
+ }
80
+
81
+ fun createDirectory(fileName: String): FileSystemDirectory {
82
+ val newDirectory = file.createDirectory(fileName) ?: throw UnableToCreateException("directory could not be created")
83
+ return FileSystemDirectory(newDirectory.uri)
84
+ }
85
+
49
86
  // this function is internal and will be removed in the future (when returning arrays of shared objects is supported)
50
87
  fun listAsRecords(): List<Map<String, Any>> {
51
88
  validateType()
52
89
  validatePermission(Permission.READ)
53
- return file.listFiles()?.map {
54
- val uriString = Uri.fromFile(it).toString()
90
+ return file.listFilesAsUnified().map {
91
+ val uriString = it.uri.toString()
55
92
  mapOf(
56
- "isDirectory" to it.isDirectory,
93
+ "isDirectory" to it.isDirectory(),
57
94
  "uri" to if (uriString.endsWith("/")) uriString else "$uriString/"
58
95
  )
59
- } ?: emptyList()
96
+ }
60
97
  }
61
98
 
62
99
  fun asString(): String {
63
- val uriString = Uri.fromFile(file).toString()
100
+ val uriString = file.uri.toString()
64
101
  return if (uriString.endsWith("/")) uriString else "$uriString/"
65
102
  }
66
103
  }
@@ -1,7 +1,6 @@
1
1
  package expo.modules.filesystem.next
2
2
 
3
3
  import android.net.Uri
4
- import android.os.Build
5
4
  import android.util.Base64
6
5
  import android.webkit.MimeTypeMap
7
6
  import expo.modules.filesystem.InfoOptions
@@ -9,16 +8,11 @@ import expo.modules.filesystem.slashifyFilePath
9
8
  import expo.modules.interfaces.filesystem.Permission
10
9
  import expo.modules.kotlin.apifeatures.EitherType
11
10
  import expo.modules.kotlin.typedarray.TypedArray
12
- import java.io.File
13
11
  import java.io.FileOutputStream
14
- import java.nio.file.attribute.BasicFileAttributes
15
12
  import java.security.MessageDigest
16
- import kotlin.io.path.Path
17
- import kotlin.io.path.readAttributes
18
- import kotlin.time.Duration.Companion.milliseconds
19
13
 
20
14
  @OptIn(EitherType::class)
21
- class FileSystemFile(file: File) : FileSystemPath(file) {
15
+ class FileSystemFile(uri: Uri) : FileSystemPath(uri) {
22
16
  // Kept empty for now, but can be used to validate if the uri is a valid file uri. // TODO: Move to the constructor once also moved on iOS
23
17
  fun validatePath() {
24
18
  }
@@ -27,14 +21,14 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
27
21
  // After calling this function, we can use the `isDirectory` and `isFile` functions safely as they will match the shared class used.
28
22
  override fun validateType() {
29
23
  validatePermission(Permission.READ)
30
- if (file.exists() && file.isDirectory) {
24
+ if (file.exists() && file.isDirectory()) {
31
25
  throw InvalidTypeFileException()
32
26
  }
33
27
  }
34
28
 
35
29
  val exists: Boolean get() {
36
30
  return if (checkPermission(Permission.READ)) {
37
- file.isFile
31
+ file.isFile()
38
32
  } else {
39
33
  false
40
34
  }
@@ -44,15 +38,19 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
44
38
  validateType()
45
39
  validatePermission(Permission.WRITE)
46
40
  validateCanCreate(options)
47
- if (options.overwrite && file.exists()) {
48
- file.delete()
49
- }
50
- if (options.intermediates) {
51
- file.parentFile?.mkdirs()
52
- }
53
- val created = file.createNewFile()
54
- if (!created) {
55
- throw UnableToCreateException("file already exists or could not be created")
41
+ if (uri.isContentUri) {
42
+ throw UnableToCreateException("create function does not work with SAF Uris, use `createDirectory` and `createFile` instead")
43
+ } else {
44
+ if (options.overwrite && exists) {
45
+ javaFile.delete()
46
+ }
47
+ if (options.intermediates) {
48
+ javaFile.parentFile?.mkdirs()
49
+ }
50
+ val created = javaFile.createNewFile()
51
+ if (!created) {
52
+ throw UnableToCreateException("file already exists or could not be created")
53
+ }
56
54
  }
57
55
  }
58
56
 
@@ -62,7 +60,7 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
62
60
  if (!exists) {
63
61
  create()
64
62
  }
65
- FileOutputStream(file).use {
63
+ FileOutputStream(javaFile).use {
66
64
  it.write(content.toByteArray())
67
65
  }
68
66
  }
@@ -73,85 +71,70 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
73
71
  if (!exists) {
74
72
  create()
75
73
  }
76
- FileOutputStream(file).use {
74
+ FileOutputStream(javaFile).use {
77
75
  it.channel.write(content.toDirectBuffer())
78
76
  }
79
77
  }
80
78
 
81
79
  fun asString(): String {
82
- val uriString = Uri.fromFile(file).toString()
80
+ val uriString = file.uri.toString()
83
81
  return if (uriString.endsWith("/")) uriString.dropLast(1) else uriString
84
82
  }
85
83
 
86
84
  fun text(): String {
87
85
  validateType()
88
86
  validatePermission(Permission.READ)
89
- return file.readText()
87
+ return javaFile.readText()
90
88
  }
91
89
 
92
90
  fun base64(): String {
93
91
  validateType()
94
92
  validatePermission(Permission.READ)
95
- return Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)
93
+ return Base64.encodeToString(javaFile.readBytes(), Base64.NO_WRAP)
96
94
  }
97
95
 
98
96
  fun bytes(): ByteArray {
99
97
  validateType()
100
98
  validatePermission(Permission.READ)
101
- return file.readBytes()
99
+ return javaFile.readBytes()
102
100
  }
103
101
 
104
102
  @OptIn(ExperimentalStdlibApi::class)
105
103
  val md5: String get() {
106
104
  validatePermission(Permission.READ)
107
105
  val md = MessageDigest.getInstance("MD5")
108
- val digest = md.digest(file.readBytes())
106
+ val digest = md.digest(javaFile.readBytes())
109
107
  return digest.toHexString()
110
108
  }
111
109
 
112
110
  val size: Long? get() {
113
- return if (file.exists()) {
114
- file.length()
111
+ return if (javaFile.exists()) {
112
+ javaFile.length()
115
113
  } else {
116
114
  null
117
115
  }
118
116
  }
119
117
 
120
118
  val type: String? get() {
121
- return MimeTypeMap.getFileExtensionFromUrl(file.path)
119
+ return MimeTypeMap.getFileExtensionFromUrl(javaFile.path)
122
120
  ?.run { MimeTypeMap.getSingleton().getMimeTypeFromExtension(lowercase()) }
123
121
  }
124
122
 
125
- val modificationTime: Long get() {
126
- validateType()
127
- return file.lastModified()
128
- }
129
-
130
- val creationTime: Long? get() {
131
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
132
- validateType()
133
- val attributes = Path(file.path).readAttributes<BasicFileAttributes>()
134
- return attributes.creationTime().toMillis().milliseconds.inWholeMilliseconds
135
- } else {
136
- return null
137
- }
138
- }
139
-
140
123
  fun info(options: InfoOptions?): FileInfo {
141
124
  validateType()
142
125
  validatePermission(Permission.READ)
143
126
  if (!file.exists()) {
144
127
  val fileInfo = FileInfo(
145
128
  exists = false,
146
- uri = slashifyFilePath(file.toURI().toString())
129
+ uri = slashifyFilePath(javaFile.toURI().toString())
147
130
  )
148
131
  return fileInfo
149
132
  }
150
133
  when {
151
- file.toURI().scheme == "file" -> {
134
+ javaFile.toURI().scheme == "file" -> {
152
135
  val fileInfo = FileInfo(
153
136
  exists = true,
154
- uri = slashifyFilePath(file.toURI().toString()),
137
+ uri = slashifyFilePath(javaFile.toURI().toString()),
155
138
  size = size,
156
139
  modificationTime = modificationTime,
157
140
  creationTime = creationTime
@@ -161,7 +144,7 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
161
144
  }
162
145
  return fileInfo
163
146
  }
164
- else -> throw UnableToGetInfoException("file schema ${file.toURI().scheme} is not supported")
147
+ else -> throw UnableToGetInfoException("file schema ${javaFile.toURI().scheme} is not supported")
165
148
  }
166
149
  }
167
150
  }
@@ -4,7 +4,7 @@ import expo.modules.kotlin.sharedobjects.SharedRef
4
4
  import java.io.RandomAccessFile
5
5
  import java.nio.ByteBuffer
6
6
  import java.nio.channels.FileChannel
7
- class FileSystemFileHandle(file: FileSystemFile) : SharedRef<FileChannel>(RandomAccessFile(file.file, "rw").channel), AutoCloseable {
7
+ class FileSystemFileHandle(file: FileSystemFile) : SharedRef<FileChannel>(RandomAccessFile(file.javaFile, "rw").channel), AutoCloseable {
8
8
  private val fileChannel: FileChannel = ref
9
9
 
10
10
  private fun ensureIsOpen() {
@@ -63,9 +63,9 @@ class FileSystemNextModule : Module() {
63
63
  val fileName = URLUtil.guessFileName(url.toString(), contentDisposition, contentType)
64
64
 
65
65
  val destination = if (to is FileSystemDirectory) {
66
- File(to.file, fileName)
66
+ File(to.javaFile, fileName)
67
67
  } else {
68
- to.file
68
+ to.javaFile
69
69
  }
70
70
 
71
71
  if (destination.exists()) {
@@ -78,7 +78,7 @@ class FileSystemNextModule : Module() {
78
78
  input.copyTo(output)
79
79
  }
80
80
  }
81
- return@Coroutine destination.path
81
+ return@Coroutine destination.toURI()
82
82
  }
83
83
 
84
84
  Function("info") { url: URI ->
@@ -93,8 +93,8 @@ class FileSystemNextModule : Module() {
93
93
  }
94
94
 
95
95
  Class(FileSystemFile::class) {
96
- Constructor { uri: URI ->
97
- FileSystemFile(File(uri.path))
96
+ Constructor { uri: Uri ->
97
+ FileSystemFile(uri)
98
98
  }
99
99
 
100
100
  Function("delete") { file: FileSystemFile ->
@@ -210,8 +210,12 @@ class FileSystemNextModule : Module() {
210
210
  }
211
211
 
212
212
  Class(FileSystemDirectory::class) {
213
- Constructor { uri: URI ->
214
- FileSystemDirectory(File(uri.path))
213
+ Constructor { uri: Uri ->
214
+ FileSystemDirectory(uri)
215
+ }
216
+
217
+ Function("info") { directory: FileSystemDirectory ->
218
+ directory.info()
215
219
  }
216
220
 
217
221
  Function("delete") { directory: FileSystemDirectory ->
@@ -222,6 +226,14 @@ class FileSystemNextModule : Module() {
222
226
  directory.create(options ?: CreateOptions())
223
227
  }
224
228
 
229
+ Function("createDirectory") { file: FileSystemDirectory, name: String ->
230
+ return@Function file.createDirectory(name)
231
+ }
232
+
233
+ Function("createFile") { file: FileSystemDirectory, name: String, mimeType: String? ->
234
+ return@Function file.createFile(mimeType, name)
235
+ }
236
+
225
237
  Property("exists") { directory: FileSystemDirectory ->
226
238
  directory.exists
227
239
  }
@@ -28,3 +28,13 @@ data class PathInfo(
28
28
  @Field var exists: Boolean,
29
29
  @Field var isDirectory: Boolean?
30
30
  ) : Record
31
+
32
+ data class DirectoryInfo(
33
+ @Field var exists: Boolean,
34
+ @Field var uri: String?,
35
+ @Field var files: List<String>? = null,
36
+ @Field var md5: String? = null,
37
+ @Field var size: Long? = null,
38
+ @Field var modificationTime: Long? = null,
39
+ @Field var creationTime: Long? = null
40
+ ) : Record
@@ -1,35 +1,69 @@
1
1
  package expo.modules.filesystem.next
2
2
 
3
+ import android.net.Uri
3
4
  import android.os.Build
5
+ import androidx.core.net.toUri
6
+ import expo.modules.filesystem.next.unifiedfile.JavaFile
7
+ import expo.modules.filesystem.next.unifiedfile.SAFDocumentFile
8
+ import expo.modules.filesystem.next.unifiedfile.UnifiedFileInterface
4
9
  import expo.modules.interfaces.filesystem.Permission
5
10
  import expo.modules.kotlin.sharedobjects.SharedObject
6
11
  import java.io.File
12
+ import java.nio.file.attribute.BasicFileAttributes
7
13
  import java.util.EnumSet
14
+ import kotlin.io.path.Path
8
15
  import kotlin.io.path.moveTo
16
+ import kotlin.io.path.readAttributes
17
+ import kotlin.time.Duration.Companion.milliseconds
9
18
 
10
- // We use the `File` class to represent a file or a directory in the file system.
11
- // The Path class might be better, but `java.nio.file.Path` class is not available in API 23.
12
- // The URL, URI classes seem like a less suitable choice.
13
- // https://stackoverflow.com/questions/27845223/whats-the-difference-between-a-resource-uri-url-path-and-file-in-java
14
- abstract class FileSystemPath(public var file: File) : SharedObject() {
15
- fun delete(fileOrDirectory: File = file) {
16
- if (!fileOrDirectory.exists()) {
17
- throw UnableToDeleteException("path '${fileOrDirectory.path}' does not exist")
19
+ val Uri.isContentUri get(): Boolean {
20
+ return scheme == "content"
21
+ }
22
+
23
+ abstract class FileSystemPath(var uri: Uri) : SharedObject() {
24
+ private var fileAdapter: UnifiedFileInterface? = null
25
+ val file: UnifiedFileInterface get() {
26
+ val currentAdapter = fileAdapter
27
+ if (currentAdapter?.uri == uri) {
28
+ return currentAdapter
29
+ }
30
+ val newAdapter = if (uri.isContentUri) {
31
+ SAFDocumentFile(appContext?.reactContext ?: throw Exception("No context"), uri)
32
+ } else {
33
+ JavaFile(uri)
34
+ }
35
+ fileAdapter = newAdapter
36
+ return newAdapter
37
+ }
38
+ val javaFile: File get() =
39
+ if (uri.isContentUri) {
40
+ throw Exception("This method cannot be used with content URIs: $uri")
41
+ } else {
42
+ (file as File)
18
43
  }
19
- if (fileOrDirectory.isDirectory) {
20
- fileOrDirectory.listFiles()?.forEach { child ->
21
- if (child.isDirectory) {
44
+
45
+ fun delete() {
46
+ if (!file.exists()) {
47
+ throw UnableToDeleteException("uri '${file.uri}' does not exist")
48
+ }
49
+ if (file.isDirectory()) {
50
+ file.listFilesAsUnified().forEach { child ->
51
+ if (child.isDirectory()) {
22
52
  // Recursively delete subdirectories
23
- delete(child)
53
+ if (uri.isContentUri) {
54
+ SAFDocumentFile(appContext?.reactContext ?: throw Exception("No context"), child.uri).delete()
55
+ } else {
56
+ JavaFile(child.uri).delete()
57
+ }
24
58
  } else {
25
59
  if (!child.delete()) {
26
- throw UnableToDeleteException("failed to delete '${child.path}'")
60
+ throw UnableToDeleteException("failed to delete '${child.uri}'")
27
61
  }
28
62
  }
29
63
  }
30
64
  }
31
- if (!fileOrDirectory.delete()) {
32
- throw UnableToDeleteException("failed to delete '${fileOrDirectory.path}'")
65
+ if (!file.delete()) {
66
+ throw UnableToDeleteException("failed to delete '${file.uri}'")
33
67
  }
34
68
  }
35
69
 
@@ -41,26 +75,26 @@ abstract class FileSystemPath(public var file: File) : SharedObject() {
41
75
  if (!destination.exists) {
42
76
  throw DestinationDoesNotExistException()
43
77
  }
44
- return File(destination.file, file.name)
78
+ return File(destination.javaFile, javaFile.name)
45
79
  }
46
80
  // this if FileSystemDirectory
47
81
  // we match unix behavior https://askubuntu.com/a/763915
48
82
  if (destination.exists) {
49
- return File(destination.file, file.name)
83
+ return File(destination.javaFile, javaFile.name)
50
84
  }
51
- if (destination.file.parentFile?.exists() != true) {
85
+ if (destination.javaFile.parentFile?.exists() != true) {
52
86
  throw DestinationDoesNotExistException()
53
87
  }
54
- return destination.file
88
+ return destination.javaFile
55
89
  }
56
90
  // destination is FileSystemFile
57
91
  if (this !is FileSystemFile) {
58
92
  throw CopyOrMoveDirectoryToFileException()
59
93
  }
60
- if (destination.file.parentFile?.exists() != true) {
94
+ if (destination.javaFile.parentFile?.exists() != true) {
61
95
  throw DestinationDoesNotExistException()
62
96
  }
63
- return destination.file
97
+ return destination.javaFile
64
98
  }
65
99
 
66
100
  fun validatePermission(permission: Permission) {
@@ -70,7 +104,11 @@ abstract class FileSystemPath(public var file: File) : SharedObject() {
70
104
  }
71
105
 
72
106
  fun checkPermission(permission: Permission): Boolean {
73
- val permissions = appContext?.filePermission?.getPathPermissions(appContext?.reactContext, file.path) ?: EnumSet.noneOf(Permission::class.java)
107
+ if (uri.isContentUri) {
108
+ // TODO: Consider adding a check for content URIs (not in legacy FS)
109
+ return true
110
+ }
111
+ val permissions = appContext?.filePermission?.getPathPermissions(appContext?.reactContext, javaFile.path) ?: EnumSet.noneOf(Permission::class.java)
74
112
  return permissions.contains(permission)
75
113
  }
76
114
 
@@ -86,7 +124,7 @@ abstract class FileSystemPath(public var file: File) : SharedObject() {
86
124
  validatePermission(Permission.READ)
87
125
  to.validatePermission(Permission.WRITE)
88
126
 
89
- file.copyRecursively(getMoveOrCopyPath(to))
127
+ javaFile.copyRecursively(getMoveOrCopyPath(to))
90
128
  }
91
129
 
92
130
  fun move(to: FileSystemPath) {
@@ -97,12 +135,27 @@ abstract class FileSystemPath(public var file: File) : SharedObject() {
97
135
 
98
136
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
99
137
  val destination = getMoveOrCopyPath(to)
100
- file.toPath().moveTo(destination.toPath())
101
- file = destination
138
+ javaFile.toPath().moveTo(destination.toPath())
139
+ uri = destination.toUri()
140
+ } else {
141
+ javaFile.copyTo(getMoveOrCopyPath(to))
142
+ javaFile.delete()
143
+ uri = getMoveOrCopyPath(to).toUri()
144
+ }
145
+ }
146
+
147
+ val modificationTime: Long get() {
148
+ validateType()
149
+ return javaFile.lastModified()
150
+ }
151
+
152
+ val creationTime: Long? get() {
153
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
154
+ validateType()
155
+ val attributes = Path(javaFile.path).readAttributes<BasicFileAttributes>()
156
+ return attributes.creationTime().toMillis().milliseconds.inWholeMilliseconds
102
157
  } else {
103
- file.copyTo(getMoveOrCopyPath(to))
104
- file.delete()
105
- file = getMoveOrCopyPath(to)
158
+ return null
106
159
  }
107
160
  }
108
161
  }
@@ -0,0 +1,34 @@
1
+ package expo.modules.filesystem.next.unifiedfile
2
+
3
+ import android.net.Uri
4
+ import androidx.core.net.toUri
5
+ import java.io.File
6
+ import java.net.URI
7
+
8
+ class JavaFile(override val uri: Uri) : UnifiedFileInterface, File(URI.create(uri.toString())) {
9
+ override fun isDirectory(): Boolean {
10
+ return super<File>.isDirectory()
11
+ }
12
+
13
+ override fun isFile(): Boolean {
14
+ return super<File>.isFile()
15
+ }
16
+
17
+ override val parentFile: UnifiedFileInterface?
18
+ get() = super<File>.parentFile?.toUri()?.let { JavaFile(it) }
19
+
20
+ override fun createFile(mimeType: String, displayName: String): UnifiedFileInterface? {
21
+ val childFile = File(super<File>.parentFile, displayName)
22
+ childFile.createNewFile()
23
+ return JavaFile(childFile.toUri())
24
+ }
25
+
26
+ override fun createDirectory(displayName: String): UnifiedFileInterface? {
27
+ val childFile = File(super<File>.parentFile, displayName)
28
+ childFile.mkdir()
29
+ return JavaFile(childFile.toUri())
30
+ }
31
+
32
+ override fun listFilesAsUnified(): List<UnifiedFileInterface> =
33
+ super<File>.listFiles()?.map { JavaFile(it.toUri()) } ?: emptyList()
34
+ }
@@ -0,0 +1,38 @@
1
+ package expo.modules.filesystem.next.unifiedfile
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import androidx.documentfile.provider.DocumentFile
6
+
7
+ class SAFDocumentFile(private val context: Context, override val uri: Uri) : UnifiedFileInterface {
8
+ private val treeDocumentFile: DocumentFile? = DocumentFile.fromTreeUri(context, uri)
9
+ private val singleDocumentFile: DocumentFile? = DocumentFile.fromSingleUri(context, uri)
10
+
11
+ override fun exists(): Boolean = singleDocumentFile?.exists() == true
12
+
13
+ override fun isDirectory(): Boolean {
14
+ return singleDocumentFile?.isDirectory == true
15
+ }
16
+
17
+ override fun isFile(): Boolean {
18
+ return singleDocumentFile?.isFile == true
19
+ }
20
+
21
+ override val parentFile: UnifiedFileInterface?
22
+ get() = treeDocumentFile?.parentFile?.uri?.let { SAFDocumentFile(context, it) }
23
+
24
+ override fun createFile(mimeType: String, displayName: String): UnifiedFileInterface? {
25
+ val documentFile = treeDocumentFile?.createFile(mimeType, displayName)
26
+ return documentFile?.uri?.let { SAFDocumentFile(context, it) }
27
+ }
28
+
29
+ override fun createDirectory(displayName: String): UnifiedFileInterface? {
30
+ val documentFile = treeDocumentFile?.createDirectory(displayName)
31
+ return documentFile?.uri?.let { SAFDocumentFile(context, it) }
32
+ }
33
+
34
+ override fun delete(): Boolean = singleDocumentFile?.delete() ?: false
35
+
36
+ override fun listFilesAsUnified(): List<UnifiedFileInterface> =
37
+ treeDocumentFile?.listFiles()?.map { SAFDocumentFile(context, it.uri) } ?: emptyList()
38
+ }