react-native-buffered-blob 1.0.0
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/android/AGENTS.md +74 -0
- package/android/build.gradle +34 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/bufferedblob/BufferedBlobModule.kt +274 -0
- package/android/src/main/java/com/bufferedblob/BufferedBlobPackage.kt +32 -0
- package/android/src/main/java/com/bufferedblob/HandleRegistry.kt +84 -0
- package/android/src/main/java/com/bufferedblob/StreamingBridge.kt +211 -0
- package/cpp/AGENTS.md +71 -0
- package/cpp/AndroidPlatformBridge.cpp +437 -0
- package/cpp/AndroidPlatformBridge.h +79 -0
- package/cpp/BufferedBlobStreamingHostObject.cpp +344 -0
- package/cpp/BufferedBlobStreamingHostObject.h +118 -0
- package/cpp/CMakeLists.txt +49 -0
- package/cpp/jni_onload.cpp +32 -0
- package/ios/AGENTS.md +76 -0
- package/ios/BufferedBlobModule.h +44 -0
- package/ios/BufferedBlobModule.m +433 -0
- package/ios/BufferedBlobModule.mm +192 -0
- package/ios/BufferedBlobStreamingBridge.h +21 -0
- package/ios/BufferedBlobStreamingBridge.mm +442 -0
- package/ios/HandleRegistry.h +29 -0
- package/ios/HandleRegistry.m +67 -0
- package/ios/HandleTypes.h +83 -0
- package/ios/HandleTypes.m +333 -0
- package/lib/module/AGENTS.md +70 -0
- package/lib/module/NativeBufferedBlob.js +5 -0
- package/lib/module/NativeBufferedBlob.js.map +1 -0
- package/lib/module/api/AGENTS.md +62 -0
- package/lib/module/api/download.js +40 -0
- package/lib/module/api/download.js.map +1 -0
- package/lib/module/api/fileOps.js +70 -0
- package/lib/module/api/fileOps.js.map +1 -0
- package/lib/module/api/hash.js +13 -0
- package/lib/module/api/hash.js.map +1 -0
- package/lib/module/api/readFile.js +23 -0
- package/lib/module/api/readFile.js.map +1 -0
- package/lib/module/api/writeFile.js +18 -0
- package/lib/module/api/writeFile.js.map +1 -0
- package/lib/module/errors.js +45 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +25 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/module.js +19 -0
- package/lib/module/module.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/paths.js +32 -0
- package/lib/module/paths.js.map +1 -0
- package/lib/module/types.js +15 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/wrappers.js +107 -0
- package/lib/module/wrappers.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeBufferedBlob.d.ts +37 -0
- package/lib/typescript/src/NativeBufferedBlob.d.ts.map +1 -0
- package/lib/typescript/src/api/download.d.ts +13 -0
- package/lib/typescript/src/api/download.d.ts.map +1 -0
- package/lib/typescript/src/api/fileOps.d.ts +9 -0
- package/lib/typescript/src/api/fileOps.d.ts.map +1 -0
- package/lib/typescript/src/api/hash.d.ts +3 -0
- package/lib/typescript/src/api/hash.d.ts.map +1 -0
- package/lib/typescript/src/api/readFile.d.ts +3 -0
- package/lib/typescript/src/api/readFile.d.ts.map +1 -0
- package/lib/typescript/src/api/writeFile.d.ts +3 -0
- package/lib/typescript/src/api/writeFile.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +25 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/module.d.ts +23 -0
- package/lib/typescript/src/module.d.ts.map +1 -0
- package/lib/typescript/src/paths.d.ts +11 -0
- package/lib/typescript/src/paths.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/wrappers.d.ts +14 -0
- package/lib/typescript/src/wrappers.d.ts.map +1 -0
- package/package.json +114 -0
- package/react-native-buffered-blob.podspec +37 -0
- package/react-native.config.js +10 -0
- package/src/AGENTS.md +70 -0
- package/src/NativeBufferedBlob.ts +54 -0
- package/src/api/AGENTS.md +62 -0
- package/src/api/download.ts +46 -0
- package/src/api/fileOps.ts +83 -0
- package/src/api/hash.ts +14 -0
- package/src/api/readFile.ts +37 -0
- package/src/api/writeFile.ts +24 -0
- package/src/errors.ts +50 -0
- package/src/index.ts +28 -0
- package/src/module.ts +48 -0
- package/src/paths.ts +35 -0
- package/src/types.ts +42 -0
- package/src/wrappers.ts +123 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#import "BufferedBlobStreamingBridge.h"
|
|
2
|
+
#import "BufferedBlobStreamingHostObject.h"
|
|
3
|
+
#import "HandleRegistry.h"
|
|
4
|
+
#import "HandleTypes.h"
|
|
5
|
+
#import <Foundation/Foundation.h>
|
|
6
|
+
#import <React/RCTBridge+Private.h>
|
|
7
|
+
|
|
8
|
+
@interface DownloadSessionDelegate : NSObject <NSURLSessionDataDelegate>
|
|
9
|
+
@property (nonatomic, copy) NSString *destPath;
|
|
10
|
+
@property (nonatomic, strong) NSOutputStream *outputStream;
|
|
11
|
+
@property (nonatomic, assign) int64_t totalBytes;
|
|
12
|
+
@property (nonatomic, assign) int64_t downloadedBytes;
|
|
13
|
+
@property (nonatomic, assign) BOOL isFinished;
|
|
14
|
+
@property (nonatomic, weak) DownloaderHandleIOS *handle;
|
|
15
|
+
@property (nonatomic, copy) void (^onProgress)(double, double, double);
|
|
16
|
+
@property (nonatomic, copy) void (^onSuccess)(void);
|
|
17
|
+
@property (nonatomic, copy) void (^onError)(NSString *);
|
|
18
|
+
@property (nonatomic, strong) NSLock *stateLock;
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
@implementation DownloadSessionDelegate
|
|
22
|
+
|
|
23
|
+
- (void)finishWithError:(NSString *)msg session:(NSURLSession *)session {
|
|
24
|
+
[self.stateLock lock];
|
|
25
|
+
if (self.isFinished) {
|
|
26
|
+
[self.stateLock unlock];
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
self.isFinished = YES;
|
|
30
|
+
void (^errorBlock)(NSString *) = self.onError;
|
|
31
|
+
self.onProgress = nil;
|
|
32
|
+
self.onSuccess = nil;
|
|
33
|
+
self.onError = nil;
|
|
34
|
+
[self.stateLock unlock];
|
|
35
|
+
|
|
36
|
+
if (errorBlock) errorBlock(msg);
|
|
37
|
+
[session invalidateAndCancel];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
- (void)finishWithSuccess:(NSURLSession *)session {
|
|
41
|
+
[self.stateLock lock];
|
|
42
|
+
if (self.isFinished) {
|
|
43
|
+
[self.stateLock unlock];
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
self.isFinished = YES;
|
|
47
|
+
void (^successBlock)(void) = self.onSuccess;
|
|
48
|
+
self.onProgress = nil;
|
|
49
|
+
self.onSuccess = nil;
|
|
50
|
+
self.onError = nil;
|
|
51
|
+
[self.stateLock unlock];
|
|
52
|
+
|
|
53
|
+
if (successBlock) successBlock();
|
|
54
|
+
[session finishTasksAndInvalidate];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
- (void)URLSession:(NSURLSession *)session
|
|
58
|
+
dataTask:(NSURLSessionDataTask *)dataTask
|
|
59
|
+
didReceiveResponse:(NSURLResponse *)response
|
|
60
|
+
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
|
|
61
|
+
[self.stateLock lock];
|
|
62
|
+
if (self.isFinished) {
|
|
63
|
+
[self.stateLock unlock];
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
[self.stateLock unlock];
|
|
67
|
+
|
|
68
|
+
DownloaderHandleIOS *strongHandle = self.handle;
|
|
69
|
+
if (!strongHandle || strongHandle.isCancelled) {
|
|
70
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
71
|
+
[self finishWithError:@"[DOWNLOAD_CANCELLED] Download was cancelled" session:session];
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
76
|
+
if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
|
|
77
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
78
|
+
NSString *errorMsg = [NSString stringWithFormat:@"[DOWNLOAD_FAILED] HTTP %ld",
|
|
79
|
+
(long)httpResponse.statusCode];
|
|
80
|
+
[self finishWithError:errorMsg session:session];
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
self.totalBytes = response.expectedContentLength;
|
|
85
|
+
self.downloadedBytes = 0;
|
|
86
|
+
|
|
87
|
+
self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.destPath append:NO];
|
|
88
|
+
[self.outputStream open];
|
|
89
|
+
|
|
90
|
+
if (self.outputStream.streamStatus == NSStreamStatusError) {
|
|
91
|
+
completionHandler(NSURLSessionResponseCancel);
|
|
92
|
+
[self finishWithError:@"[IO_ERROR] Failed to open output stream" session:session];
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
completionHandler(NSURLSessionResponseAllow);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)URLSession:(NSURLSession *)session
|
|
100
|
+
dataTask:(NSURLSessionDataTask *)dataTask
|
|
101
|
+
didReceiveData:(NSData *)data {
|
|
102
|
+
[self.stateLock lock];
|
|
103
|
+
if (self.isFinished) {
|
|
104
|
+
[self.stateLock unlock];
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
[self.stateLock unlock];
|
|
108
|
+
|
|
109
|
+
DownloaderHandleIOS *strongHandle = self.handle;
|
|
110
|
+
if (!strongHandle || strongHandle.isCancelled) {
|
|
111
|
+
[self finishWithError:@"[DOWNLOAD_CANCELLED] Download was cancelled" session:session];
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!self.outputStream || self.outputStream.streamStatus != NSStreamStatusOpen) {
|
|
116
|
+
[self finishWithError:@"[IO_ERROR] Output stream not open" session:session];
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const uint8_t *bytes = (const uint8_t *)data.bytes;
|
|
121
|
+
NSUInteger length = data.length;
|
|
122
|
+
NSUInteger totalWritten = 0;
|
|
123
|
+
|
|
124
|
+
while (totalWritten < length) {
|
|
125
|
+
NSInteger written = [self.outputStream write:(bytes + totalWritten)
|
|
126
|
+
maxLength:(length - totalWritten)];
|
|
127
|
+
if (written < 0) {
|
|
128
|
+
[self finishWithError:@"[IO_ERROR] Failed to write to file" session:session];
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
totalWritten += written;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
self.downloadedBytes += length;
|
|
135
|
+
|
|
136
|
+
if (self.onProgress && self.totalBytes > 0) {
|
|
137
|
+
double progress = (double)self.downloadedBytes / (double)self.totalBytes;
|
|
138
|
+
self.onProgress((double)self.downloadedBytes, (double)self.totalBytes, progress);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
- (void)URLSession:(NSURLSession *)session
|
|
143
|
+
task:(NSURLSessionTask *)task
|
|
144
|
+
didCompleteWithError:(NSError *)error {
|
|
145
|
+
if (self.outputStream) {
|
|
146
|
+
[self.outputStream close];
|
|
147
|
+
self.outputStream = nil;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
[self.stateLock lock];
|
|
151
|
+
if (self.isFinished) {
|
|
152
|
+
[self.stateLock unlock];
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
[self.stateLock unlock];
|
|
156
|
+
|
|
157
|
+
DownloaderHandleIOS *strongHandle = self.handle;
|
|
158
|
+
if (!strongHandle || strongHandle.isCancelled) {
|
|
159
|
+
[self finishWithError:@"[DOWNLOAD_CANCELLED] Download was cancelled" session:session];
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (error) {
|
|
164
|
+
NSString *errorMsg = [NSString stringWithFormat:@"[DOWNLOAD_FAILED] %@",
|
|
165
|
+
error.localizedDescription];
|
|
166
|
+
[self finishWithError:errorMsg session:session];
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
[self finishWithSuccess:session];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@end
|
|
174
|
+
|
|
175
|
+
namespace {
|
|
176
|
+
|
|
177
|
+
using namespace bufferedblob;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* iOS implementation of PlatformBridge.
|
|
181
|
+
* Calls into HandleRegistry and handle types via ObjC interop.
|
|
182
|
+
*/
|
|
183
|
+
class IOSPlatformBridge : public PlatformBridge {
|
|
184
|
+
public:
|
|
185
|
+
IOSPlatformBridge() {}
|
|
186
|
+
|
|
187
|
+
void readNextChunk(
|
|
188
|
+
int handleId,
|
|
189
|
+
std::function<void(std::vector<uint8_t>)> onSuccess,
|
|
190
|
+
std::function<void()> onEOF,
|
|
191
|
+
std::function<void(std::string)> onError) override {
|
|
192
|
+
|
|
193
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
194
|
+
ReaderHandleIOS *reader = (ReaderHandleIOS *)[registry objectForId:handleId];
|
|
195
|
+
|
|
196
|
+
if (!reader) {
|
|
197
|
+
onError("[READER_CLOSED] Reader handle not found");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Dispatch to the reader's serial queue to serialize all access to this handle
|
|
202
|
+
dispatch_async(reader.queue, ^{
|
|
203
|
+
@autoreleasepool {
|
|
204
|
+
if (reader.isClosed) {
|
|
205
|
+
onError("[READER_CLOSED] Reader is closed");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (reader.isEOF) {
|
|
210
|
+
onEOF();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
NSInteger bufferSize = reader.bufferSize;
|
|
215
|
+
uint8_t *buffer = (uint8_t *)malloc(bufferSize);
|
|
216
|
+
if (!buffer) {
|
|
217
|
+
onError("[IO_ERROR] Failed to allocate read buffer");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
NSInteger bytesRead = [reader.inputStream read:buffer maxLength:bufferSize];
|
|
222
|
+
|
|
223
|
+
if (bytesRead < 0) {
|
|
224
|
+
free(buffer);
|
|
225
|
+
onError("[IO_ERROR] Read error");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (bytesRead == 0) {
|
|
230
|
+
free(buffer);
|
|
231
|
+
reader.isEOF = YES;
|
|
232
|
+
onEOF();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
reader.bytesRead = reader.bytesRead + bytesRead;
|
|
237
|
+
|
|
238
|
+
std::vector<uint8_t> data(buffer, buffer + bytesRead);
|
|
239
|
+
free(buffer);
|
|
240
|
+
onSuccess(std::move(data));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void write(
|
|
246
|
+
int handleId,
|
|
247
|
+
std::vector<uint8_t> data,
|
|
248
|
+
std::function<void(int)> onSuccess,
|
|
249
|
+
std::function<void(std::string)> onError) override {
|
|
250
|
+
|
|
251
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
252
|
+
WriterHandleIOS *writer = (WriterHandleIOS *)[registry objectForId:handleId];
|
|
253
|
+
|
|
254
|
+
if (!writer) {
|
|
255
|
+
onError("[WRITER_CLOSED] Writer handle not found");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Move data into block-captured variable (single copy from JSI, no second copy)
|
|
260
|
+
__block auto ownedData = std::move(data);
|
|
261
|
+
|
|
262
|
+
// Dispatch to the writer's serial queue to serialize all access to this handle
|
|
263
|
+
dispatch_async(writer.queue, ^{
|
|
264
|
+
@autoreleasepool {
|
|
265
|
+
if (writer.isClosed) {
|
|
266
|
+
onError("[WRITER_CLOSED] Writer is closed");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
NSInteger totalWritten = 0;
|
|
271
|
+
const uint8_t *ptr = ownedData.data();
|
|
272
|
+
NSInteger remaining = static_cast<NSInteger>(ownedData.size());
|
|
273
|
+
|
|
274
|
+
while (remaining > 0) {
|
|
275
|
+
NSInteger written = [writer.outputStream write:ptr maxLength:remaining];
|
|
276
|
+
if (written < 0) {
|
|
277
|
+
onError("[IO_ERROR] Write error");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (written == 0) {
|
|
281
|
+
onError("[IO_ERROR] Write returned 0 bytes");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
totalWritten += written;
|
|
285
|
+
ptr += written;
|
|
286
|
+
remaining -= written;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
writer.bytesWritten = writer.bytesWritten + totalWritten;
|
|
290
|
+
onSuccess(static_cast<int>(totalWritten));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
void flush(
|
|
296
|
+
int handleId,
|
|
297
|
+
std::function<void()> onSuccess,
|
|
298
|
+
std::function<void(std::string)> onError) override {
|
|
299
|
+
|
|
300
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
301
|
+
WriterHandleIOS *writer = (WriterHandleIOS *)[registry objectForId:handleId];
|
|
302
|
+
|
|
303
|
+
if (!writer) {
|
|
304
|
+
onError("[WRITER_CLOSED] Writer handle not found");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Dispatch to the writer's serial queue to serialize all access to this handle
|
|
309
|
+
dispatch_async(writer.queue, ^{
|
|
310
|
+
@autoreleasepool {
|
|
311
|
+
if (writer.isClosed) {
|
|
312
|
+
onError("[WRITER_CLOSED] Writer is closed");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// NSOutputStream doesn't have an explicit flush, data is written immediately
|
|
317
|
+
onSuccess();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
void close(int handleId) override {
|
|
323
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
324
|
+
[registry removeObjectForId:handleId];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
void startDownload(
|
|
328
|
+
int handleId,
|
|
329
|
+
std::function<void(double, double, double)> onProgress,
|
|
330
|
+
std::function<void()> onSuccess,
|
|
331
|
+
std::function<void(std::string)> onError) override {
|
|
332
|
+
|
|
333
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
334
|
+
DownloaderHandleIOS *handle = (DownloaderHandleIOS *)[registry objectForId:handleId];
|
|
335
|
+
|
|
336
|
+
if (!handle) {
|
|
337
|
+
onError("[DOWNLOAD_FAILED] Download handle not found");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (handle.isCancelled) {
|
|
342
|
+
onError("[DOWNLOAD_CANCELLED] Download was cancelled");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
NSURL *url = [NSURL URLWithString:handle.url];
|
|
347
|
+
if (!url) {
|
|
348
|
+
onError("[DOWNLOAD_FAILED] Invalid URL");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
353
|
+
for (NSString *key in handle.headers) {
|
|
354
|
+
[request setValue:handle.headers[key] forHTTPHeaderField:key];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
NSString *destPath = handle.destPath;
|
|
358
|
+
NSString *parentDir = [destPath stringByDeletingLastPathComponent];
|
|
359
|
+
[[NSFileManager defaultManager] createDirectoryAtPath:parentDir
|
|
360
|
+
withIntermediateDirectories:YES
|
|
361
|
+
attributes:nil
|
|
362
|
+
error:nil];
|
|
363
|
+
|
|
364
|
+
DownloadSessionDelegate *delegate = [[DownloadSessionDelegate alloc] init];
|
|
365
|
+
delegate.stateLock = [NSLock new];
|
|
366
|
+
delegate.destPath = destPath;
|
|
367
|
+
delegate.handle = handle;
|
|
368
|
+
delegate.onProgress = ^(double downloaded, double total, double progress) {
|
|
369
|
+
onProgress(downloaded, total, progress);
|
|
370
|
+
};
|
|
371
|
+
delegate.onSuccess = ^{
|
|
372
|
+
onSuccess();
|
|
373
|
+
};
|
|
374
|
+
delegate.onError = ^(NSString *errorMsg) {
|
|
375
|
+
onError(std::string([errorMsg UTF8String]));
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
379
|
+
config.timeoutIntervalForRequest = 30.0;
|
|
380
|
+
config.timeoutIntervalForResource = 600.0;
|
|
381
|
+
NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
|
|
382
|
+
delegateQueue.maxConcurrentOperationCount = 1;
|
|
383
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
|
|
384
|
+
delegate:delegate
|
|
385
|
+
delegateQueue:delegateQueue];
|
|
386
|
+
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
|
|
387
|
+
|
|
388
|
+
// Store session and task on the handle so cancelDownload can properly invalidate
|
|
389
|
+
[handle storeSession:session task:task];
|
|
390
|
+
|
|
391
|
+
// Re-check: cancel may have been called between the first check and storeSession
|
|
392
|
+
if (handle.isCancelled) {
|
|
393
|
+
[task cancel];
|
|
394
|
+
[session invalidateAndCancel];
|
|
395
|
+
onError("[DOWNLOAD_CANCELLED] Download was cancelled");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
[task resume];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
void cancelDownload(int handleId) override {
|
|
403
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
404
|
+
DownloaderHandleIOS *handle = (DownloaderHandleIOS *)[registry objectForId:handleId];
|
|
405
|
+
if (handle) {
|
|
406
|
+
// cancel() now properly invalidates the NSURLSession and task
|
|
407
|
+
[handle cancel];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
ReaderInfo getReaderInfo(int handleId) override {
|
|
412
|
+
ReaderInfo info{0, 0, false};
|
|
413
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
414
|
+
ReaderHandleIOS *reader = (ReaderHandleIOS *)[registry objectForId:handleId];
|
|
415
|
+
if (reader) {
|
|
416
|
+
info.fileSize = static_cast<double>(reader.fileSize);
|
|
417
|
+
info.bytesRead = static_cast<double>(reader.bytesRead);
|
|
418
|
+
info.isEOF = reader.isEOF;
|
|
419
|
+
}
|
|
420
|
+
return info;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
WriterInfo getWriterInfo(int handleId) override {
|
|
424
|
+
WriterInfo info{0};
|
|
425
|
+
HandleRegistry *registry = [HandleRegistry shared];
|
|
426
|
+
WriterHandleIOS *writer = (WriterHandleIOS *)[registry objectForId:handleId];
|
|
427
|
+
if (writer) {
|
|
428
|
+
info.bytesWritten = static_cast<double>(writer.bytesWritten);
|
|
429
|
+
}
|
|
430
|
+
return info;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
} // anonymous namespace
|
|
435
|
+
|
|
436
|
+
void installBufferedBlobStreaming(
|
|
437
|
+
facebook::jsi::Runtime& runtime,
|
|
438
|
+
std::shared_ptr<facebook::react::CallInvoker> callInvoker) {
|
|
439
|
+
auto bridge = std::make_shared<IOSPlatformBridge>();
|
|
440
|
+
bufferedblob::BufferedBlobStreamingHostObject::install(
|
|
441
|
+
runtime, std::move(callInvoker), std::move(bridge));
|
|
442
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
|
|
5
|
+
@protocol HandleCloseable <NSObject>
|
|
6
|
+
- (void)closeHandle;
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Thread-safe singleton registry that maps integer IDs to handle objects.
|
|
11
|
+
* Used to pass opaque handles between JS and native.
|
|
12
|
+
*/
|
|
13
|
+
@interface HandleRegistry : NSObject
|
|
14
|
+
|
|
15
|
+
+ (instancetype)shared;
|
|
16
|
+
|
|
17
|
+
/** Register an object and return its unique ID. */
|
|
18
|
+
- (NSInteger)registerObject:(id)obj;
|
|
19
|
+
|
|
20
|
+
/** Look up an object by ID. Returns nil if not found. */
|
|
21
|
+
- (id)objectForId:(NSInteger)handleId;
|
|
22
|
+
|
|
23
|
+
/** Remove and close (if HandleCloseable) the object for the given ID. */
|
|
24
|
+
- (void)removeObjectForId:(NSInteger)handleId;
|
|
25
|
+
|
|
26
|
+
/** Remove and close all registered objects. */
|
|
27
|
+
- (void)clear;
|
|
28
|
+
|
|
29
|
+
@end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#import "HandleRegistry.h"
|
|
2
|
+
|
|
3
|
+
@implementation HandleRegistry {
|
|
4
|
+
NSInteger _nextId;
|
|
5
|
+
NSMutableDictionary<NSNumber *, id> *_handles;
|
|
6
|
+
NSLock *_lock;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
+ (instancetype)shared {
|
|
10
|
+
static HandleRegistry *instance;
|
|
11
|
+
static dispatch_once_t onceToken;
|
|
12
|
+
dispatch_once(&onceToken, ^{
|
|
13
|
+
instance = [[HandleRegistry alloc] initPrivate];
|
|
14
|
+
});
|
|
15
|
+
return instance;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
- (instancetype)initPrivate {
|
|
19
|
+
self = [super init];
|
|
20
|
+
if (self) {
|
|
21
|
+
_nextId = 1;
|
|
22
|
+
_handles = [NSMutableDictionary new];
|
|
23
|
+
_lock = [NSLock new];
|
|
24
|
+
}
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (NSInteger)registerObject:(id)obj {
|
|
29
|
+
[_lock lock];
|
|
30
|
+
NSInteger handleId = _nextId++;
|
|
31
|
+
_handles[@(handleId)] = obj;
|
|
32
|
+
[_lock unlock];
|
|
33
|
+
return handleId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (id)objectForId:(NSInteger)handleId {
|
|
37
|
+
[_lock lock];
|
|
38
|
+
id obj = _handles[@(handleId)];
|
|
39
|
+
[_lock unlock];
|
|
40
|
+
return obj;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (void)removeObjectForId:(NSInteger)handleId {
|
|
44
|
+
[_lock lock];
|
|
45
|
+
id obj = _handles[@(handleId)];
|
|
46
|
+
[_handles removeObjectForKey:@(handleId)];
|
|
47
|
+
[_lock unlock];
|
|
48
|
+
|
|
49
|
+
if ([obj conformsToProtocol:@protocol(HandleCloseable)]) {
|
|
50
|
+
[(id<HandleCloseable>)obj closeHandle];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
- (void)clear {
|
|
55
|
+
[_lock lock];
|
|
56
|
+
NSDictionary<NSNumber *, id> *snapshot = [_handles copy];
|
|
57
|
+
[_handles removeAllObjects];
|
|
58
|
+
[_lock unlock];
|
|
59
|
+
|
|
60
|
+
for (id obj in snapshot.allValues) {
|
|
61
|
+
if ([obj conformsToProtocol:@protocol(HandleCloseable)]) {
|
|
62
|
+
[(id<HandleCloseable>)obj closeHandle];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
#import "HandleRegistry.h"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reader handle: wraps an NSInputStream for buffered file reading.
|
|
8
|
+
* All I/O is dispatched to a dedicated serial queue for thread safety.
|
|
9
|
+
*/
|
|
10
|
+
@interface ReaderHandleIOS : NSObject <HandleCloseable>
|
|
11
|
+
|
|
12
|
+
@property (nonatomic, strong, readonly) NSInputStream *inputStream;
|
|
13
|
+
@property (nonatomic, assign, readonly) NSInteger bufferSize;
|
|
14
|
+
@property (nonatomic, assign, readonly) int64_t fileSize;
|
|
15
|
+
|
|
16
|
+
/** Serial queue for dispatching I/O operations from the C++ bridge. */
|
|
17
|
+
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
|
|
18
|
+
|
|
19
|
+
/** Bytes read so far. Thread-safe via internal lock. */
|
|
20
|
+
@property (atomic, assign) int64_t bytesRead;
|
|
21
|
+
/** Whether the stream has reached end-of-file. Thread-safe via internal lock. */
|
|
22
|
+
@property (atomic, assign) BOOL isEOF;
|
|
23
|
+
/** Whether the handle has been closed. Thread-safe via internal lock. */
|
|
24
|
+
@property (atomic, assign) BOOL isClosed;
|
|
25
|
+
|
|
26
|
+
- (nullable instancetype)initWithPath:(NSString *)path
|
|
27
|
+
bufferSize:(NSInteger)bufferSize
|
|
28
|
+
error:(NSError **)error;
|
|
29
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
30
|
+
|
|
31
|
+
@end
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Writer handle: wraps an NSOutputStream for buffered file writing.
|
|
35
|
+
* All I/O is dispatched to a dedicated serial queue for thread safety.
|
|
36
|
+
*/
|
|
37
|
+
@interface WriterHandleIOS : NSObject <HandleCloseable>
|
|
38
|
+
|
|
39
|
+
@property (nonatomic, strong, readonly) NSOutputStream *outputStream;
|
|
40
|
+
|
|
41
|
+
/** Serial queue for dispatching I/O operations from the C++ bridge. */
|
|
42
|
+
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
|
|
43
|
+
|
|
44
|
+
/** Bytes written so far. Thread-safe via atomic. */
|
|
45
|
+
@property (atomic, assign) int64_t bytesWritten;
|
|
46
|
+
/** Whether the handle has been closed. Thread-safe via atomic. */
|
|
47
|
+
@property (atomic, assign) BOOL isClosed;
|
|
48
|
+
|
|
49
|
+
- (nullable instancetype)initWithPath:(NSString *)path
|
|
50
|
+
append:(BOOL)append
|
|
51
|
+
error:(NSError **)error;
|
|
52
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
53
|
+
|
|
54
|
+
@end
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Downloader handle: manages a URLSession download to a file.
|
|
58
|
+
* Supports cancellation via the cancel method.
|
|
59
|
+
*/
|
|
60
|
+
@interface DownloaderHandleIOS : NSObject <HandleCloseable>
|
|
61
|
+
|
|
62
|
+
@property (nonatomic, copy, readonly) NSString *url;
|
|
63
|
+
@property (nonatomic, copy, readonly) NSString *destPath;
|
|
64
|
+
@property (nonatomic, copy, readonly) NSDictionary<NSString *, NSString *> *headers;
|
|
65
|
+
|
|
66
|
+
/** Serial queue exposed for future use if needed. */
|
|
67
|
+
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
|
|
68
|
+
|
|
69
|
+
/** Whether the download has been cancelled. Thread-safe via atomic. */
|
|
70
|
+
@property (atomic, assign) BOOL isCancelled;
|
|
71
|
+
|
|
72
|
+
- (instancetype)initWithURL:(NSString *)url
|
|
73
|
+
destPath:(NSString *)destPath
|
|
74
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers;
|
|
75
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
76
|
+
|
|
77
|
+
/** Store the session and task so cancel can properly invalidate them. */
|
|
78
|
+
- (void)storeSession:(NSURLSession *)session task:(NSURLSessionTask *)task;
|
|
79
|
+
|
|
80
|
+
/** Cancel the download, invalidating the session. */
|
|
81
|
+
- (void)cancel;
|
|
82
|
+
|
|
83
|
+
@end
|