Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/avatars/get-screenshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
114 changes: 92 additions & 22 deletions lib/src/client_browser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down Expand Up @@ -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 {
Expand All @@ -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<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(
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(
final chunkHeaders = Map<String, String>.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'];
}
Comment on lines +231 to +254

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused index parameter in uploadChunk

The index parameter is accepted by uploadChunk in both client_browser.dart and client_io.dart but is never referenced in either function body. All positioning is done via start/end, so the parameter appears to be dead code. Remove it or document its intended use to avoid confusion.


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 = <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']!;
if (isUploadComplete(chunkResponse)) {
lastResponse = chunkResponse;
}
Comment on lines +305 to +307

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 lastResponse falls back to the first-chunk response if the server never signals completion

lastResponse is only updated inside uploadNext when isUploadComplete returns true. If every concurrent chunk response has null or a non-num chunksUploaded field (e.g., a transient server regression), lastResponse stays as the first chunk's Response—which has stale metadata such as chunksUploaded: 1. The caller has no way to distinguish this from a genuinely incomplete upload. The old sequential loop always returned the last chunk's response regardless of field values.


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
Expand Down
126 changes: 97 additions & 29 deletions lib/src/client_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down Expand Up @@ -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 {
Expand All @@ -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<Response> uploadChunk(
int index, int start, int end, String? id) async {
List<int> 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<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(
final chunkHeaders = Map<String, String>.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 = <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']!;
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;
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down