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..db3577b 100644 --- a/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt +++ b/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt @@ -400,6 +400,30 @@ class FileAccessModule internal constructor(context: ReactApplicationContext) : } } + @ReactMethod + override fun readFileChunk(path: String, offset: Double, length: Double, encoding: String, promise: Promise) { + ioScope.launch { + try { + val data = openForReading(path).use { inputStream -> + inputStream.skip(offset.toLong()) + val data = ByteArray(length.toInt()) + inputStream.read(data) + data + } + + val result = if (encoding == "base64") { + Base64.encodeToString(data, Base64.NO_WRAP) + } else { + data.decodeToString() + } + + promise.resolve(result) + } 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..b638c4b 100644 --- a/src/NativeFileAccess.ts +++ b/src/NativeFileAccess.ts @@ -71,6 +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; stat(path: string): Promise; statDir(path: string): Promise; unlink(path: string): Promise; diff --git a/src/index.ts b/src/index.ts index 1b38d45..2931ee7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -275,6 +275,18 @@ 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. */