expo-file-system 18.0.2 → 18.0.4

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/CHANGELOG.md CHANGED
@@ -10,6 +10,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 18.0.4 — 2024-11-19
14
+
15
+ ### 🎉 New features
16
+
17
+ - [next] Added `.bytes()` and writing a `Uint8Array`. ([#33020](https://github.com/expo/expo/pull/33020) by [@aleqsio](https://github.com/aleqsio))
18
+
19
+ ## 18.0.3 — 2024-11-13
20
+
21
+ ### 🎉 New features
22
+
23
+ - [next] Add file handles. ([#31738](https://github.com/expo/expo/pull/31738) by [@aleqsio](https://github.com/aleqsio))
24
+
13
25
  ## 18.0.2 — 2024-11-11
14
26
 
15
27
  _This version does not introduce any user-facing changes._
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '18.0.2'
4
+ version = '18.0.4'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -14,7 +14,7 @@ android {
14
14
  namespace "expo.modules.filesystem"
15
15
  defaultConfig {
16
16
  versionCode 30
17
- versionName "18.0.2"
17
+ versionName "18.0.4"
18
18
  }
19
19
  }
20
20
 
@@ -53,7 +53,7 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
53
53
  create()
54
54
  }
55
55
  FileOutputStream(file).use {
56
- it.write(content.toDirectBuffer().array())
56
+ it.channel.write(content.toDirectBuffer())
57
57
  }
58
58
  }
59
59
 
@@ -74,6 +74,12 @@ class FileSystemFile(file: File) : FileSystemPath(file) {
74
74
  return Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)
75
75
  }
76
76
 
77
+ fun bytes(): ByteArray {
78
+ validateType()
79
+ validatePermission(Permission.READ)
80
+ return file.readBytes()
81
+ }
82
+
77
83
  @OptIn(ExperimentalStdlibApi::class)
78
84
  val md5: String get() {
79
85
  validatePermission(Permission.READ)
@@ -0,0 +1,68 @@
1
+ package expo.modules.filesystem.next
2
+
3
+ import expo.modules.kotlin.sharedobjects.SharedRef
4
+ import java.io.RandomAccessFile
5
+ import java.nio.ByteBuffer
6
+ import java.nio.channels.FileChannel
7
+ class FileSystemFileHandle(file: FileSystemFile) : SharedRef<FileChannel>(RandomAccessFile(file.file, "rw").channel), AutoCloseable {
8
+ private val fileChannel: FileChannel = ref
9
+
10
+ private fun ensureIsOpen() {
11
+ if (!fileChannel.isOpen) {
12
+ throw UnableToReadHandleException("file handle is closed")
13
+ }
14
+ }
15
+
16
+ override fun sharedObjectDidRelease() {
17
+ close()
18
+ }
19
+
20
+ override fun close() {
21
+ fileChannel.close()
22
+ }
23
+
24
+ fun read(length: Int): ByteArray {
25
+ ensureIsOpen()
26
+ try {
27
+ val buffer = ByteBuffer.allocate(length.coerceAtMost((fileChannel.size() - fileChannel.position()).toInt()))
28
+ fileChannel.read(buffer)
29
+ return buffer.array()
30
+ } catch (e: Exception) {
31
+ throw UnableToReadHandleException(e.message ?: "unknown error")
32
+ }
33
+ }
34
+
35
+ fun write(data: ByteArray) {
36
+ ensureIsOpen()
37
+ try {
38
+ val buffer = ByteBuffer.wrap(data)
39
+ fileChannel.write(buffer)
40
+ } catch (e: Exception) {
41
+ throw UnableToWriteHandleException(e.message ?: "unknown error")
42
+ }
43
+ }
44
+
45
+ var offset: Long?
46
+ get() {
47
+ return try {
48
+ fileChannel.position()
49
+ } catch (e: Exception) {
50
+ null
51
+ }
52
+ }
53
+ set(value) {
54
+ if (value == null) {
55
+ return
56
+ }
57
+ fileChannel.position(value)
58
+ }
59
+
60
+ val size: Long?
61
+ get() {
62
+ return try {
63
+ fileChannel.size()
64
+ } catch (e: Exception) {
65
+ null
66
+ }
67
+ }
68
+ }
@@ -29,6 +29,16 @@ internal class InvalidPermissionException(permission: Permission) :
29
29
  "Missing '${permission.name}' permission for accessing the file."
30
30
  )
31
31
 
32
+ internal class UnableToReadHandleException(reason: String) :
33
+ CodedException(
34
+ "Unable to read from a file handle: '$reason'"
35
+ )
36
+
37
+ internal class UnableToWriteHandleException(reason: String) :
38
+ CodedException(
39
+ "Unable to write to a file handle: '$reason'"
40
+ )
41
+
32
42
  internal class DestinationAlreadyExistsException :
33
43
  CodedException(
34
44
  "Destination already exists"
@@ -102,6 +102,10 @@ class FileSystemNextModule : Module() {
102
102
  file.base64()
103
103
  }
104
104
 
105
+ Function("bytes") { file: FileSystemFile ->
106
+ file.bytes()
107
+ }
108
+
105
109
  Property("exists") { file: FileSystemFile ->
106
110
  file.exists
107
111
  }
@@ -133,6 +137,33 @@ class FileSystemNextModule : Module() {
133
137
  null
134
138
  }
135
139
  }
140
+
141
+ Function("open") { file: FileSystemFile ->
142
+ FileSystemFileHandle(file)
143
+ }
144
+ }
145
+
146
+ Class(FileSystemFileHandle::class) {
147
+ Constructor { file: FileSystemFile ->
148
+ FileSystemFileHandle(file)
149
+ }
150
+ Function("readBytes") { fileHandle: FileSystemFileHandle, bytes: Int ->
151
+ fileHandle.read(bytes)
152
+ }
153
+ Function("writeBytes") { fileHandle: FileSystemFileHandle, data: ByteArray ->
154
+ fileHandle.write(data)
155
+ }
156
+ Function("close") { fileHandle: FileSystemFileHandle ->
157
+ fileHandle.close()
158
+ }
159
+ Property("offset") { fileHandle: FileSystemFileHandle ->
160
+ fileHandle.offset
161
+ }.set { fileHandle: FileSystemFileHandle, offset: Long ->
162
+ fileHandle.offset = offset
163
+ }
164
+ Property("size") { fileHandle: FileSystemFileHandle ->
165
+ fileHandle.size
166
+ }
136
167
  }
137
168
 
138
169
  Class(FileSystemDirectory::class) {
@@ -11,7 +11,7 @@ import kotlin.io.path.moveTo
11
11
  // The Path class might be better, but `java.nio.file.Path` class is not available in API 23.
12
12
  // The URL, URI classes seem like a less suitable choice.
13
13
  // https://stackoverflow.com/questions/27845223/whats-the-difference-between-a-resource-uri-url-path-and-file-in-java
14
- abstract class FileSystemPath(var file: File) : SharedObject() {
14
+ abstract class FileSystemPath(public var file: File) : SharedObject() {
15
15
  fun delete() {
16
16
  if (!file.exists()) {
17
17
  throw UnableToDeleteException("path does not exist")
@@ -85,11 +85,16 @@ export declare class File {
85
85
  * @returns The contents of the file as a base64 string.
86
86
  */
87
87
  base64(): string;
88
+ /**
89
+ * Retrieves byte content of the entire file.
90
+ * @returns The contents of the file as a Uint8Array.
91
+ */
92
+ bytes(): Uint8Array;
88
93
  /**
89
94
  * Writes content to the file.
90
95
  * @param content - The content to write into the file.
91
96
  */
92
- write(content: string): void;
97
+ write(content: string | Uint8Array): void;
93
98
  /**
94
99
  * Deletes a file.
95
100
  *
@@ -115,6 +120,11 @@ export declare class File {
115
120
  * Moves a directory. Updates the `uri` property that now points to the new location.
116
121
  */
117
122
  move(destination: Directory | File): any;
123
+ /**
124
+ * Returns a FileHandle object that can be used to read and write data to the file.
125
+ * @throws Error if the file does not exist or cannot be opened.
126
+ */
127
+ open(): FileHandle;
118
128
  /**
119
129
  * A static method that downloads a file from the network.
120
130
  * @param url - The URL of the file to download.
@@ -135,4 +145,11 @@ export declare class File {
135
145
  */
136
146
  md5: string | null;
137
147
  }
148
+ export declare class FileHandle {
149
+ close(): void;
150
+ readBytes(length: number): Uint8Array;
151
+ writeBytes(bytes: Uint8Array): void;
152
+ offset: number | null;
153
+ size: number | null;
154
+ }
138
155
  //# sourceMappingURL=ExpoFileSystem.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoFileSystem.types.d.ts","sourceRoot":"","sources":["../../src/next/ExpoFileSystem.types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,SAAS;IAC5B;;;;;;;OAOG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAElD;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,IAAI,IAAI;IAEpB;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;;;OAIG;IACH,aAAa,IAAI;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE;IAExD;;OAEG;IACH,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE;CAC7B;AAED;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,IAAI;IACvB;;;;OAIG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAElD;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,IAAI,IAAI;IAEpB;;;OAGG;IACH,IAAI,IAAI,MAAM;IAEd;;;OAGG;IACH,MAAM,IAAI,MAAM;IAEhB;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE5B;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;;;;;;;;OASG;IACH,MAAM,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnF;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpB;;OAEG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB"}
1
+ {"version":3,"file":"ExpoFileSystem.types.d.ts","sourceRoot":"","sources":["../../src/next/ExpoFileSystem.types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,SAAS;IAC5B;;;;;;;OAOG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAElD;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,IAAI,IAAI;IAEpB;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;;;OAIG;IACH,aAAa,IAAI;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE;IAExD;;OAEG;IACH,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE;CAC7B;AAED;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,IAAI;IACvB;;;;OAIG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAElD;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,IAAI,IAAI;IAEpB;;;OAGG;IACH,IAAI,IAAI,MAAM;IAEd;;;OAGG;IACH,MAAM,IAAI,MAAM;IAEhB;;;OAGG;IACH,KAAK,IAAI,UAAU;IAEnB;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAEzC;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAEd;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,IAAI;IAElC;;;OAGG;IACH,IAAI,IAAI,UAAU;IAElB;;;;;;;;;OASG;IACH,MAAM,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnF;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpB;;OAEG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,CAAC,OAAO,OAAO,UAAU;IAI7B,KAAK,IAAI,IAAI;IAKb,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU;IAKrC,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAMnC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAItB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB"}
@@ -1,3 +1,4 @@
1
+ import { ReadableStream, WritableStream } from 'web-streams-polyfill';
1
2
  import ExpoFileSystem from './ExpoFileSystem';
2
3
  import { PathUtilities } from './pathUtilities';
3
4
  export declare class Paths extends PathUtilities {
@@ -31,6 +32,8 @@ export declare class File extends ExpoFileSystem.FileSystemFile {
31
32
  * File name. Includes the extension.
32
33
  */
33
34
  get name(): string;
35
+ readableStream(): ReadableStream<Uint8Array>;
36
+ writableStream(): WritableStream<Uint8Array>;
34
37
  }
35
38
  /**
36
39
  * Represents a directory on the filesystem.
@@ -1 +1 @@
1
- {"version":3,"file":"FileSystem.d.ts","sourceRoot":"","sources":["../../src/next/FileSystem.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,qBAAa,KAAM,SAAQ,aAAa;IACtC;;OAEG;IACH,MAAM,KAAK,KAAK,cAEf;IAED;;OAEG;IACH,MAAM,KAAK,QAAQ,cAElB;IACD,MAAM,KAAK,qBAAqB,8BAO/B;CACF;AAED,qBAAa,IAAK,SAAQ,cAAc,CAAC,cAAc;IACrD;;;;;;;OAOG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAQlD,IAAI,eAAe,cAElB;IAED;;;OAGG;IACH,IAAI,SAAS,WAEZ;IAED;;OAEG;IACH,IAAI,IAAI,WAEP;CACF;AAQD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAc,CAAC,mBAAmB;IAC/D;;;;;;;OAOG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAQlD,IAAI,eAAe,cAElB;IAED;;;;OAIG;IACH,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE;IAO5B;;OAEG;IACH,IAAI,IAAI,WAEP;CACF"}
1
+ {"version":3,"file":"FileSystem.d.ts","sourceRoot":"","sources":["../../src/next/FileSystem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,cAAc,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,qBAAa,KAAM,SAAQ,aAAa;IACtC;;OAEG;IACH,MAAM,KAAK,KAAK,cAEf;IAED;;OAEG;IACH,MAAM,KAAK,QAAQ,cAElB;IACD,MAAM,KAAK,qBAAqB,8BAO/B;CACF;AAED,qBAAa,IAAK,SAAQ,cAAc,CAAC,cAAc;IACrD;;;;;;;OAOG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAQlD,IAAI,eAAe,cAElB;IAED;;;OAGG;IACH,IAAI,SAAS,WAEZ;IAED;;OAEG;IACH,IAAI,IAAI,WAEP;IAED,cAAc;IAId,cAAc;CAGf;AAQD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAc,CAAC,mBAAmB;IAC/D;;;;;;;OAOG;gBACS,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE;IAQlD,IAAI,eAAe,cAElB;IAED;;;;OAIG;IACH,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE;IAO5B;;OAEG;IACH,IAAI,IAAI,WAEP;CACF"}
@@ -0,0 +1,17 @@
1
+ import type { FileHandle } from './ExpoFileSystem.types';
2
+ export declare class FileSystemReadableStreamSource implements UnderlyingByteSource {
3
+ handle: FileHandle;
4
+ size: number;
5
+ type: "bytes";
6
+ constructor(handle: any);
7
+ cancel(): void;
8
+ pull(controller: ReadableByteStreamController): void;
9
+ }
10
+ export declare class FileSystemWritableSink implements UnderlyingSink {
11
+ handle: FileHandle;
12
+ constructor(handle: any);
13
+ abort(): void;
14
+ close(): void;
15
+ write(chunk: Uint8Array): void;
16
+ }
17
+ //# sourceMappingURL=streams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../src/next/streams.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,qBAAa,8BAA+B,YAAW,oBAAoB;IACzE,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAQ;IACpB,IAAI,UAAoB;gBAEZ,MAAM,KAAA;IAIlB,MAAM;IAIN,IAAI,CAAC,UAAU,EAAE,4BAA4B;CA4B9C;AAED,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,MAAM,EAAE,UAAU,CAAC;gBAEP,MAAM,KAAA;IAIlB,KAAK;IAIL,KAAK;IAIL,KAAK,CAAC,KAAK,EAAE,UAAU;CAGxB"}
@@ -73,8 +73,11 @@ internal final class FileSystemFile: FileSystemPath {
73
73
  try content.write(to: url, atomically: false, encoding: .utf8) // TODO: better error handling
74
74
  }
75
75
 
76
- // TODO: typedarray, blobs, others support
76
+ // TODO: blob support
77
77
  func write(_ content: TypedArray) throws {
78
+ try validateType()
79
+ try validatePermission(.write)
80
+ try Data(bytes: content.rawPointer, count: content.byteLength).write(to: url)
78
81
  }
79
82
 
80
83
  func text() throws -> String {
@@ -83,6 +86,12 @@ internal final class FileSystemFile: FileSystemPath {
83
86
  return try String(contentsOf: url)
84
87
  }
85
88
 
89
+ func bytes() throws -> Data {
90
+ try validateType()
91
+ try validatePermission(.read)
92
+ return try Data(contentsOf: url)
93
+ }
94
+
86
95
  func base64() throws -> String {
87
96
  try validatePermission(.read)
88
97
  return try Data(contentsOf: url).base64EncodedString()
@@ -0,0 +1,53 @@
1
+ import Foundation
2
+ import ExpoModulesCore
3
+
4
+ internal final class FileSystemFileHandle: SharedRef<FileHandle> {
5
+ let file: FileSystemFile
6
+ let handle: FileHandle
7
+
8
+ init(file: FileSystemFile) throws {
9
+ self.file = file
10
+ handle = try FileHandle(forUpdating: file.url)
11
+ super.init(handle)
12
+ }
13
+
14
+ func read(_ length: Int) throws -> Data {
15
+ do {
16
+ let data = try handle.read(upToCount: length)
17
+ return data ?? Data()
18
+ } catch {
19
+ throw UnableToReadHandleException(error.localizedDescription)
20
+ }
21
+ }
22
+
23
+ func write(_ bytes: Data) throws {
24
+ try handle.write(contentsOf: bytes)
25
+ }
26
+
27
+ func close() throws {
28
+ try handle.close()
29
+ }
30
+
31
+ var offset: UInt64? {
32
+ get {
33
+ try? handle.offset()
34
+ }
35
+ set(newOffset) {
36
+ guard let newOffset else {
37
+ return
38
+ }
39
+ handle.seek(toFileOffset: newOffset)
40
+ }
41
+ }
42
+
43
+ var size: UInt64? {
44
+ do {
45
+ let offset = try handle.offset()
46
+ let size = try handle.seekToEnd()
47
+ handle.seek(toFileOffset: offset)
48
+ return size
49
+ } catch {
50
+ return nil
51
+ }
52
+ }
53
+ }
@@ -49,6 +49,12 @@ internal class UnableToCreateFileException: GenericException<String> {
49
49
  }
50
50
  }
51
51
 
52
+ internal class UnableToReadHandleException: GenericException<String> {
53
+ override var reason: String {
54
+ "Unable to read from a file handle: \(param)"
55
+ }
56
+ }
57
+
52
58
  internal class DestinationAlreadyExistsException: Exception {
53
59
  override var reason: String {
54
60
  "Destination already exists"
@@ -72,6 +72,14 @@ public final class FileSystemNextModule: Module {
72
72
  return try file.base64()
73
73
  }
74
74
 
75
+ Function("bytes") { file in
76
+ return try file.bytes()
77
+ }
78
+
79
+ Function("open") { file in
80
+ return try FileSystemFileHandle(file: file)
81
+ }
82
+
75
83
  Function("write") { (file, content: Either<String, TypedArray>) in
76
84
  if let content: String = content.get() {
77
85
  try file.write(content)
@@ -114,6 +122,30 @@ public final class FileSystemNextModule: Module {
114
122
  }
115
123
  }
116
124
 
125
+ Class(FileSystemFileHandle.self) {
126
+ Function("readBytes") { (fileHandle, bytes: Int) in
127
+ try fileHandle.read(bytes)
128
+ }
129
+
130
+ Function("writeBytes") { (fileHandle, bytes: Data) in
131
+ try fileHandle.write(bytes)
132
+ }
133
+
134
+ Function("close") { fileHandle in
135
+ try fileHandle.close()
136
+ }
137
+
138
+ Property("offset") { fileHandle in
139
+ fileHandle.offset
140
+ }.set { (fileHandle, volume: UInt64) in
141
+ fileHandle.offset = volume
142
+ }
143
+
144
+ Property("size") { fileHandle in
145
+ fileHandle.size
146
+ }
147
+ }
148
+
117
149
  Class(FileSystemDirectory.self) {
118
150
  Constructor { (url: URL) in
119
151
  return FileSystemDirectory(url: url.standardizedFileURL)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-file-system",
3
- "version": "18.0.2",
3
+ "version": "18.0.4",
4
4
  "description": "Provides access to the local file system on the device.",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -42,5 +42,8 @@
42
42
  "expo": "*",
43
43
  "react-native": "*"
44
44
  },
45
- "gitHead": "8f11fad6f46b878de4746b49b18599fc57b5729f"
45
+ "dependencies": {
46
+ "web-streams-polyfill": "^3.3.2"
47
+ },
48
+ "gitHead": "128718d43bac2eaed764b3551469b95400f2363e"
46
49
  }
@@ -97,11 +97,17 @@ export declare class File {
97
97
  */
98
98
  base64(): string;
99
99
 
100
+ /**
101
+ * Retrieves byte content of the entire file.
102
+ * @returns The contents of the file as a Uint8Array.
103
+ */
104
+ bytes(): Uint8Array;
105
+
100
106
  /**
101
107
  * Writes content to the file.
102
108
  * @param content - The content to write into the file.
103
109
  */
104
- write(content: string): void;
110
+ write(content: string | Uint8Array): void;
105
111
 
106
112
  /**
107
113
  * Deletes a file.
@@ -133,6 +139,12 @@ export declare class File {
133
139
  */
134
140
  move(destination: Directory | File);
135
141
 
142
+ /**
143
+ * Returns a FileHandle object that can be used to read and write data to the file.
144
+ * @throws Error if the file does not exist or cannot be opened.
145
+ */
146
+ open(): FileHandle;
147
+
136
148
  /**
137
149
  * A static method that downloads a file from the network.
138
150
  * @param url - The URL of the file to download.
@@ -155,3 +167,30 @@ export declare class File {
155
167
  */
156
168
  md5: string | null;
157
169
  }
170
+
171
+ export declare class FileHandle {
172
+ /*
173
+ * Closes the file handle. This allows the file to be deleted, moved or read by a different process. Subsequent calls to `readBytes` or `writeBytes` will throw an error.
174
+ */
175
+ close(): void;
176
+ /*
177
+ * Reads the specified amount of bytes from the file at the current offset.
178
+ * @param length - The number of bytes to read.
179
+ */
180
+ readBytes(length: number): Uint8Array;
181
+ /*
182
+ * Writes the specified bytes to the file at the current offset.
183
+ * @param bytes - A Uint8Array array containing bytes to write.
184
+ */
185
+ writeBytes(bytes: Uint8Array): void;
186
+ /*
187
+ * A property that indicates the current byte offset in the file. Calling `readBytes` or `writeBytes` will read or write a specified amount of bytes starting from this offset. The offset is incremented by the number of bytes read or written.
188
+ * The offset can be set to any value within the file size. If the offset is set to a value greater than the file size, the next write operation will append data to the end of the file.
189
+ * Null if the file handle is closed.
190
+ */
191
+ offset: number | null;
192
+ /*
193
+ * A size of the file in bytes or `null` if the file handle is closed.
194
+ */
195
+ size: number | null;
196
+ }
@@ -1,5 +1,8 @@
1
+ import { ReadableStream, WritableStream } from 'web-streams-polyfill';
2
+
1
3
  import ExpoFileSystem from './ExpoFileSystem';
2
4
  import { PathUtilities } from './pathUtilities';
5
+ import { FileSystemReadableStreamSource, FileSystemWritableSink } from './streams';
3
6
 
4
7
  export class Paths extends PathUtilities {
5
8
  /**
@@ -60,6 +63,14 @@ export class File extends ExpoFileSystem.FileSystemFile {
60
63
  get name() {
61
64
  return Paths.basename(this.uri);
62
65
  }
66
+
67
+ readableStream() {
68
+ return new ReadableStream<Uint8Array>(new FileSystemReadableStreamSource(super.open()));
69
+ }
70
+
71
+ writableStream() {
72
+ return new WritableStream<Uint8Array>(new FileSystemWritableSink(super.open()));
73
+ }
63
74
  }
64
75
 
65
76
  // Cannot use `static` keyword in class declaration because of a runtime error.
@@ -0,0 +1,64 @@
1
+ import type { FileHandle } from './ExpoFileSystem.types';
2
+
3
+ export class FileSystemReadableStreamSource implements UnderlyingByteSource {
4
+ handle: FileHandle;
5
+ size: number = 1024;
6
+ type = 'bytes' as const;
7
+
8
+ constructor(handle) {
9
+ this.handle = handle;
10
+ }
11
+
12
+ cancel() {
13
+ this.handle.close();
14
+ }
15
+
16
+ pull(controller: ReadableByteStreamController) {
17
+ const theView = controller.byobRequest?.view;
18
+ if (!theView) {
19
+ const bytes = this.handle.readBytes(this.size);
20
+ if (bytes.length === 0) {
21
+ controller.close();
22
+ return;
23
+ }
24
+ controller.enqueue(bytes);
25
+ return;
26
+ }
27
+
28
+ // TODO: Optimize by adding a native method that can write into a TypedArray at a given offset.
29
+ const bytes = this.handle.readBytes(theView.byteLength - theView.byteOffset);
30
+ if (bytes.length === 0) {
31
+ controller.close();
32
+ controller.byobRequest.respond(0);
33
+ return;
34
+ }
35
+ if (theView instanceof Uint8Array) {
36
+ theView.set(bytes, theView.byteOffset);
37
+ } else {
38
+ for (let i = 0; i < bytes.length; i++) {
39
+ theView[i + theView.byteOffset] = bytes[i];
40
+ }
41
+ }
42
+ controller.byobRequest.respond(bytes.length);
43
+ }
44
+ }
45
+
46
+ export class FileSystemWritableSink implements UnderlyingSink {
47
+ handle: FileHandle;
48
+
49
+ constructor(handle) {
50
+ this.handle = handle;
51
+ }
52
+
53
+ abort() {
54
+ this.close();
55
+ }
56
+
57
+ close() {
58
+ this.handle.close();
59
+ }
60
+
61
+ write(chunk: Uint8Array) {
62
+ this.handle.writeBytes(chunk);
63
+ }
64
+ }