From 57669c13fb9f1676dac163ce5b5dbb44e2a386af Mon Sep 17 00:00:00 2001 From: Florian Gyger Date: Tue, 9 Apr 2024 11:40:23 +0200 Subject: [PATCH 1/3] feat: allow reading chunks from files --- README.md | 5 ++++ .../java/com/alpha0010/fs/FileAccessModule.kt | 20 ++++++++++++++ android/src/oldarch/FileAccessSpec.kt | 1 + ios/FileAccess.mm | 14 ++++++++++ ios/FileAccess.swift | 27 +++++++++++++++++++ src/NativeFileAccess.ts | 1 + src/index.ts | 7 +++++ 7 files changed, 75 insertions(+) diff --git a/README.md b/README.md index a864d30..cf8d6c6 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,11 @@ type ManagedFetchResult = { - Read the content of a file. - Default encoding of returned string is utf8. +`FileSystem.readFileChunk(path: string, offset: number, length: number, encoding?: 'utf8' | 'base64'): Promise` + +- Read a chunk of the content of a file, starting from byte at `offset`, reading for `length` bytes. + - Default encoding of returned string is utf8. + ``` FileSystem.stat(path: string): Promise diff --git a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt index 248a938..c8bcb98 100644 --- a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt +++ b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt @@ -400,6 +400,26 @@ class FileAccessModule internal constructor(context: ReactApplicationContext) : } } + @ReactMethod + override fun readFileChunk(path: String, offset: Double, length: Double, encoding: String, promise: Promise) { + ioScope.launch { + try { + val inputStream = openForReading(path); + inputStream.skip(offset.toLong()) + val data = ByteArray(length.toInt()) + inputStream.read(data); + + if (encoding == "base64") { + promise.resolve(Base64.encodeToString(data, Base64.NO_WRAP)) + } else { + promise.resolve(data.decodeToString()) + } + } catch (e: Throwable) { + promise.reject(e) + } + } + } + @ReactMethod override fun stat(path: String, promise: Promise) { ioScope.launch { diff --git a/android/src/oldarch/FileAccessSpec.kt b/android/src/oldarch/FileAccessSpec.kt index 736324b..aef083e 100644 --- a/android/src/oldarch/FileAccessSpec.kt +++ b/android/src/oldarch/FileAccessSpec.kt @@ -30,6 +30,7 @@ abstract class FileAccessSpec internal constructor(context: ReactApplicationCont abstract fun mkdir(path: String, promise: Promise) abstract fun mv(source: String, target: String, promise: Promise) abstract fun readFile(path: String, encoding: String, promise: Promise) + abstract fun readFileChunk(path: String, offset: Double, length: Double, encoding: String, promise: Promise) abstract fun stat(path: String, promise: Promise) abstract fun statDir(path: String, promise: Promise) abstract fun unlink(path: String, promise: Promise) diff --git a/ios/FileAccess.mm b/ios/FileAccess.mm index 240d863..4447099 100644 --- a/ios/FileAccess.mm +++ b/ios/FileAccess.mm @@ -22,6 +22,7 @@ - (void)ls:(NSString * _Nonnull)path withResolver:(RCTPromiseResolveBlock _Nonnu - (void)mkdir:(NSString * _Nonnull)path withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; - (void)mv:(NSString * _Nonnull)source withTarget:(NSString * _Nonnull)target withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; - (void)readFile:(NSString * _Nonnull)path withEncoding:(NSString * _Nonnull)encoding withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; +- (void)readFileChunk:(NSString * _Nonnull)path withOffset:(NSNumber * _Nonnull)offset withLength:(NSNumber * _Nonnull)length withEncoding:(NSString * _Nonnull)encoding withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; - (void)stat:(NSString * _Nonnull)path withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; - (void)statDir:(NSString * _Nonnull)path withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; - (void)unlink:(NSString * _Nonnull)path withResolver:(RCTPromiseResolveBlock _Nonnull)resolve withRejecter:(RCTPromiseRejectBlock _Nonnull)reject; @@ -170,6 +171,19 @@ - (instancetype)init [impl readFile:path withEncoding:encoding withResolver:resolve withRejecter:reject]; } +RCT_EXPORT_METHOD(readFileChunk:(NSString *)path + offset:(double)offset + length:(double)length + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSNumber *offsetNumber = @(offset); + NSNumber *lengthNumber = @(length); + [impl readFileChunk:path withOffset:offsetNumber withLength:lengthNumber withEncoding:encoding withResolver:resolve withRejecter:reject]; +} + + RCT_EXPORT_METHOD(stat:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) diff --git a/ios/FileAccess.swift b/ios/FileAccess.swift index 7264573..eb4593d 100644 --- a/ios/FileAccess.swift +++ b/ios/FileAccess.swift @@ -272,6 +272,33 @@ public class FileAccess : NSObject { } } + @objc(readFileChunk:withOffset:withLength:withEncoding:withResolver:withRejecter:) + public func readFileChunk(path: String, offset: NSNumber, length: NSNumber, encoding: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + DispatchQueue.global().async { + do { + let fileHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path.path())) + defer { + fileHandle.closeFile() + } + + fileHandle.seek(toFileOffset: UInt64(truncating: offset)) + let binaryData = fileHandle.readData(ofLength: Int(truncating: length)) + + if encoding == "base64" { + resolve(binaryData.base64EncodedString()) + } else { + if let content = String(data: binaryData, encoding: .utf8) { + resolve(content) + } else { + reject("ERR", "Failed to decode content from file '\(path)' with specified encoding.", nil) + } + } + } catch { + reject("ERR", "Failed to read '\(path)'. \(error.localizedDescription)", error) + } + } + } + @objc(stat:withResolver:withRejecter:) public func stat(path: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { DispatchQueue.global().async { diff --git a/src/NativeFileAccess.ts b/src/NativeFileAccess.ts index b3a7baa..22ead08 100644 --- a/src/NativeFileAccess.ts +++ b/src/NativeFileAccess.ts @@ -71,6 +71,7 @@ export interface Spec extends TurboModule { mkdir(path: string): Promise; mv(source: string, target: string): Promise; readFile(path: string, encoding: string): Promise; + readFileChunk(path: string, offset: number, length: number, encoding: string): Promise; stat(path: string): Promise; statDir(path: string): Promise; unlink(path: string): Promise; diff --git a/src/index.ts b/src/index.ts index 1b38d45..acf9bc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -275,6 +275,13 @@ export const FileSystem = { return FileAccessNative.readFile(path, encoding); }, + /** + * Read a chunk of the content of a file. + */ + readFileChunk(path: string, offset: number, length: number, encoding: Encoding = 'utf8') { + return FileAccessNative.readFileChunk(path, offset, length, encoding); + }, + /** * Read file metadata. */ From acc265caf365de8a760bd6f0a5d3929dddc7a1d7 Mon Sep 17 00:00:00 2001 From: Florian Gyger Date: Fri, 12 Apr 2024 22:47:12 +0200 Subject: [PATCH 2/3] fix: auto-close file after reading --- .../java/com/alpha0010/fs/FileAccessModule.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt index c8bcb98..db3577b 100644 --- a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt +++ b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt @@ -404,16 +404,20 @@ class FileAccessModule internal constructor(context: ReactApplicationContext) : override fun readFileChunk(path: String, offset: Double, length: Double, encoding: String, promise: Promise) { ioScope.launch { try { - val inputStream = openForReading(path); - inputStream.skip(offset.toLong()) - val data = ByteArray(length.toInt()) - inputStream.read(data); + val data = openForReading(path).use { inputStream -> + inputStream.skip(offset.toLong()) + val data = ByteArray(length.toInt()) + inputStream.read(data) + data + } - if (encoding == "base64") { - promise.resolve(Base64.encodeToString(data, Base64.NO_WRAP)) + val result = if (encoding == "base64") { + Base64.encodeToString(data, Base64.NO_WRAP) } else { - promise.resolve(data.decodeToString()) + data.decodeToString() } + + promise.resolve(result) } catch (e: Throwable) { promise.reject(e) } From 2796a48603d460a0cc6ee6f499f19d2014fe6f7a Mon Sep 17 00:00:00 2001 From: flogy Date: Mon, 15 Apr 2024 09:14:20 +0200 Subject: [PATCH 3/3] chore: fixed linting issues --- src/NativeFileAccess.ts | 7 ++++++- src/index.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/NativeFileAccess.ts b/src/NativeFileAccess.ts index 22ead08..b638c4b 100644 --- a/src/NativeFileAccess.ts +++ b/src/NativeFileAccess.ts @@ -71,7 +71,12 @@ export interface Spec extends TurboModule { mkdir(path: string): Promise; mv(source: string, target: string): Promise; readFile(path: string, encoding: string): Promise; - readFileChunk(path: string, offset: number, length: number, encoding: string): Promise; + readFileChunk( + path: string, + offset: number, + length: number, + encoding: string + ): Promise; stat(path: string): Promise; statDir(path: string): Promise; unlink(path: string): Promise; diff --git a/src/index.ts b/src/index.ts index acf9bc8..2931ee7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -278,7 +278,12 @@ export const FileSystem = { /** * Read a chunk of the content of a file. */ - readFileChunk(path: string, offset: number, length: number, encoding: Encoding = 'utf8') { + readFileChunk( + path: string, + offset: number, + length: number, + encoding: Encoding = 'utf8' + ) { return FileAccessNative.readFileChunk(path, offset, length, encoding); },