-
Notifications
You must be signed in to change notification settings - Fork 22
feat: support concurrent chunk uploads #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
349ba8c
6d88893
13c8f55
3ef20d6
103bf09
88545f5
3229d20
770aa80
9878e2e
08eea66
3b0b501
9629bc8
5a875bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ class ClientBrowser extends ClientBase with ClientMixin { | |
| 'x-sdk-name': 'Dart', | ||
| 'x-sdk-platform': 'server', | ||
| 'x-sdk-language': 'dart', | ||
| 'x-sdk-version': '24.1.0', | ||
| 'x-sdk-version': '24.2.0', | ||
| 'X-Appwrite-Response-Format': '1.9.5', | ||
| }; | ||
|
|
||
|
|
@@ -196,6 +196,7 @@ class ClientBrowser extends ClientBase with ClientMixin { | |
| } | ||
|
|
||
| var offset = 0; | ||
| String? uploadId; | ||
| if (idParamName.isNotEmpty) { | ||
| //make a request to check if a file already exists | ||
| try { | ||
|
|
@@ -206,33 +207,108 @@ class ClientBrowser extends ClientBase with ClientMixin { | |
| ); | ||
| final int chunksUploaded = res.data['chunksUploaded'] as int; | ||
| offset = chunksUploaded * chunkSize; | ||
| uploadId = res.data['\$id'] ?? params[idParamName]?.toString(); | ||
| } on AppwriteException catch (_) {} | ||
| } | ||
|
|
||
| while (offset < size) { | ||
| if (offset >= size) { | ||
| return res; | ||
| } | ||
|
|
||
| final totalChunks = (size / chunkSize).ceil(); | ||
|
|
||
| Future<Response> uploadChunk( | ||
| int index, int start, int end, String? id) async { | ||
| List<int> chunk = []; | ||
| final end = min(offset + chunkSize, size); | ||
| chunk = file.bytes!.getRange(offset, end).toList(); | ||
| params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, | ||
| chunk = file.bytes!.getRange(start, end).toList(); | ||
|
|
||
| final chunkParams = Map<String, dynamic>.from(params); | ||
| chunkParams[paramName] = http.MultipartFile.fromBytes(paramName, chunk, | ||
| filename: file.filename); | ||
| headers['content-range'] = | ||
| 'bytes $offset-${min<int>((offset + chunkSize - 1), size - 1)}/$size'; | ||
| res = await call(HttpMethod.post, | ||
| path: path, headers: headers, params: params); | ||
| offset += chunkSize; | ||
| if (offset < size) { | ||
| headers['x-appwrite-id'] = res.data['\$id']; | ||
| final chunkHeaders = Map<String, String>.from(headers); | ||
| if (id != null && id.isNotEmpty) { | ||
| chunkHeaders['x-appwrite-id'] = id; | ||
| } | ||
| final progress = UploadProgress( | ||
| $id: res.data['\$id'] ?? '', | ||
| progress: min(offset, size) / size * 100, | ||
| sizeUploaded: min(offset, size), | ||
| chunksTotal: res.data['chunksTotal'] ?? 0, | ||
| chunksUploaded: res.data['chunksUploaded'] ?? 0, | ||
| chunkHeaders['content-range'] = 'bytes $start-${end - 1}/$size'; | ||
|
|
||
| return call( | ||
| HttpMethod.post, | ||
| path: path, | ||
| headers: chunkHeaders, | ||
| params: chunkParams, | ||
| ); | ||
| onProgress?.call(progress); | ||
| } | ||
| return res; | ||
|
|
||
| final firstStart = offset; | ||
| final firstEnd = min(firstStart + chunkSize, size); | ||
| final firstIndex = firstStart ~/ chunkSize; | ||
| res = await uploadChunk(firstIndex, firstStart, firstEnd, uploadId); | ||
| uploadId = res.data['\$id'] ?? uploadId; | ||
|
|
||
| var completedChunks = firstIndex + 1; | ||
| var uploadedBytes = firstEnd; | ||
| var lastResponse = res; | ||
| Response? finalResponse; | ||
|
|
||
| bool isUploadComplete(Response response) { | ||
| final chunksUploaded = response.data['chunksUploaded']; | ||
| final chunksTotal = response.data['chunksTotal'] ?? totalChunks; | ||
| return chunksUploaded is num && | ||
| chunksTotal is num && | ||
| chunksUploaded >= chunksTotal; | ||
| } | ||
|
|
||
| final progress = UploadProgress( | ||
| $id: uploadId ?? '', | ||
| progress: min(uploadedBytes, size) / size * 100, | ||
| sizeUploaded: min(uploadedBytes, size), | ||
| chunksTotal: totalChunks, | ||
| chunksUploaded: completedChunks, | ||
| ); | ||
| onProgress?.call(progress); | ||
|
|
||
| final chunks = <Map<String, int>>[]; | ||
| for (var start = firstEnd; start < size; start += chunkSize) { | ||
| final end = min(start + chunkSize, size); | ||
| chunks.add({ | ||
| 'index': start ~/ chunkSize, | ||
| 'start': start, | ||
| 'end': end, | ||
| }); | ||
| } | ||
|
|
||
| var nextChunk = 0; | ||
| Future<void> uploadNext() async { | ||
| while (nextChunk < chunks.length) { | ||
| final chunk = chunks[nextChunk++]; | ||
| final chunkResponse = await uploadChunk( | ||
| chunk['index']!, | ||
| chunk['start']!, | ||
| chunk['end']!, | ||
| uploadId, | ||
| ); | ||
| completedChunks++; | ||
| uploadedBytes += chunk['end']! - chunk['start']!; | ||
| lastResponse = chunkResponse; | ||
| if (isUploadComplete(chunkResponse)) { | ||
| finalResponse = chunkResponse; | ||
| } | ||
|
|
||
| final progress = UploadProgress( | ||
| $id: uploadId ?? '', | ||
| progress: min(uploadedBytes, size) / size * 100, | ||
| sizeUploaded: min(uploadedBytes, size), | ||
| chunksTotal: totalChunks, | ||
| chunksUploaded: completedChunks, | ||
| ); | ||
| onProgress?.call(progress); | ||
| } | ||
| } | ||
|
|
||
| final concurrency = min(8, chunks.length); | ||
| await Future.wait(List.generate(concurrency, (_) => uploadNext())); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| return finalResponse ?? lastResponse; | ||
| } | ||
|
|
||
| @override | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.