diff --git a/README.md b/README.md index 84ed4e08..6cfe247b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Add this to your package's `pubspec.yaml` file: ```yml dependencies: - appwrite: ^24.1.1 + appwrite: ^24.2.0 ``` You can install packages from the command line: diff --git a/docs/examples/avatars/get-screenshot.md b/docs/examples/avatars/get-screenshot.md index 6695d05a..6097941e 100644 --- a/docs/examples/avatars/get-screenshot.md +++ b/docs/examples/avatars/get-screenshot.md @@ -22,7 +22,7 @@ Uint8List bytes = await avatars.getScreenshot( userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15', // optional fullpage: true, // optional locale: 'en-US', // optional - timezone: enums.Timezone.americaNewYork, // optional + timezone: enums.Timezone.africaAbidjan, // optional latitude: 37.7749, // optional longitude: -122.4194, // optional accuracy: 100, // optional @@ -53,7 +53,7 @@ FutureBuilder( userAgent:'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15' , // optional fullpage:true , // optional locale:'en-US' , // optional - timezone: enums.Timezone.americaNewYork, // optional + timezone: enums.Timezone.africaAbidjan, // optional latitude:37.7749 , // optional longitude:-122.4194 , // optional accuracy:100 , // optional diff --git a/lib/src/client_browser.dart b/lib/src/client_browser.dart index 98c71f28..2c562071 100644 --- a/lib/src/client_browser.dart +++ b/lib/src/client_browser.dart @@ -40,7 +40,7 @@ class ClientBrowser extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '24.1.1', + 'x-sdk-version': '24.2.0', 'X-Appwrite-Response-Format': '1.9.5', }; @@ -207,6 +207,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 { @@ -217,40 +218,109 @@ 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 uploadChunk( + int index, int start, int end, String? id) async { List chunk = []; - final end = min(offset + chunkSize, size); - chunk = file.bytes!.getRange(offset, end).toList(); - params[paramName] = http.MultipartFile.fromBytes( + chunk = file.bytes!.getRange(start, end).toList(); + + final chunkParams = Map.from(params); + chunkParams[paramName] = http.MultipartFile.fromBytes( paramName, chunk, filename: file.filename, ); - headers['content-range'] = - 'bytes $offset-${min((offset + chunkSize - 1), size - 1)}/$size'; - res = await call( + final chunkHeaders = Map.from(headers); + if (id != null && id.isNotEmpty) { + chunkHeaders['x-appwrite-id'] = id; + } + chunkHeaders['content-range'] = 'bytes $start-${end - 1}/$size'; + + return call( HttpMethod.post, path: path, - headers: headers, - params: params, + headers: chunkHeaders, + params: chunkParams, ); - offset += chunkSize; - if (offset < size) { - headers['x-appwrite-id'] = res.data['\$id']; + } + + 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; + + 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 = >[]; + 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 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']!; + if (isUploadComplete(chunkResponse)) { + lastResponse = chunkResponse; + } + + final progress = UploadProgress( + $id: uploadId ?? '', + progress: min(uploadedBytes, size) / size * 100, + sizeUploaded: min(uploadedBytes, size), + chunksTotal: totalChunks, + chunksUploaded: completedChunks, + ); + onProgress?.call(progress); } - 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, - ); - onProgress?.call(progress); } - return res; + + final concurrency = min(8, chunks.length); + await Future.wait(List.generate(concurrency, (_) => uploadNext())); + + return lastResponse; } @override diff --git a/lib/src/client_io.dart b/lib/src/client_io.dart index ff6501c8..95436bfd 100644 --- a/lib/src/client_io.dart +++ b/lib/src/client_io.dart @@ -58,7 +58,7 @@ class ClientIO extends ClientBase with ClientMixin { 'x-sdk-name': 'Flutter', 'x-sdk-platform': 'client', 'x-sdk-language': 'flutter', - 'x-sdk-version': '24.1.1', + 'x-sdk-version': '24.2.0', 'X-Appwrite-Response-Format': '1.9.5', }; @@ -328,6 +328,7 @@ class ClientIO extends ClientBase with ClientMixin { } var offset = 0; + String? uploadId; if (idParamName.isNotEmpty) { //make a request to check if a file already exists try { @@ -338,52 +339,119 @@ class ClientIO 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 (_) {} } - RandomAccessFile? raf; - // read chunk and upload each chunk - if (iofile != null) { - raf = await iofile.open(mode: FileMode.read); + if (offset >= size) { + return res; } - while (offset < size) { + final totalChunks = (size / chunkSize).ceil(); + + Future uploadChunk( + int index, int start, int end, String? id) async { List chunk = []; if (file.bytes != null) { - final end = min(offset + chunkSize, size); - chunk = file.bytes!.getRange(offset, end).toList(); + chunk = file.bytes!.getRange(start, end).toList(); } else { - raf!.setPositionSync(offset); - chunk = raf.readSync(chunkSize); + final raf = await iofile!.open(mode: FileMode.read); + try { + await raf.setPosition(start); + chunk = await raf.read(end - start); + } finally { + await raf.close(); + } } - params[paramName] = http.MultipartFile.fromBytes( + + final chunkParams = Map.from(params); + chunkParams[paramName] = http.MultipartFile.fromBytes( paramName, chunk, filename: file.filename, ); - headers['content-range'] = - 'bytes $offset-${min((offset + chunkSize - 1), size - 1)}/$size'; - res = await call( + final chunkHeaders = Map.from(headers); + if (id != null && id.isNotEmpty) { + chunkHeaders['x-appwrite-id'] = id; + } + chunkHeaders['content-range'] = 'bytes $start-${end - 1}/$size'; + + return call( HttpMethod.post, path: path, - headers: headers, - params: params, + headers: chunkHeaders, + params: chunkParams, ); - offset += chunkSize; - if (offset < size) { - headers['x-appwrite-id'] = res.data['\$id']; + } + + 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; + + 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 = >[]; + 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 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']!; + if (isUploadComplete(chunkResponse)) { + lastResponse = chunkResponse; + } + + final progress = UploadProgress( + $id: uploadId ?? '', + progress: min(uploadedBytes, size) / size * 100, + sizeUploaded: min(uploadedBytes, size), + chunksTotal: totalChunks, + chunksUploaded: completedChunks, + ); + onProgress?.call(progress); } - 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, - ); - onProgress?.call(progress); } - raf?.close(); - return res; + + final concurrency = min(8, chunks.length); + await Future.wait(List.generate(concurrency, (_) => uploadNext())); + + return lastResponse; } bool get _customSchemeAllowed => Platform.isWindows || Platform.isLinux; diff --git a/pubspec.yaml b/pubspec.yaml index 052a6a98..776d6e45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: appwrite -version: 24.1.1 +version: 24.2.0 description: Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API homepage: https://appwrite.io repository: https://github.com/appwrite/sdk-for-flutter