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
11 changes: 9 additions & 2 deletions app/lib/backend/http/api/conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,16 @@ Future<List<ServerConversation>> sendStorageToBackend(File file, String sdCardDa
}
}

Future<SyncLocalFilesResponse> syncLocalFiles(List<File> files) async {
Future<SyncLocalFilesResponse> syncLocalFiles(
List<File> files, {
void Function(int sentBytes, int totalBytes, double? speedKBps)? onUploadProgress,
}) async {
try {
var response = await makeMultipartApiCall(url: '${Env.apiBaseUrl}v1/sync-local-files', files: files);
var response = await makeMultipartApiCall(
url: '${Env.apiBaseUrl}v1/sync-local-files',
files: files,
onUploadProgress: onUploadProgress,
);

if (response.statusCode == 200) {
Logger.debug('syncLocalFile Response body: ${jsonDecode(response.body)}');
Expand Down
58 changes: 56 additions & 2 deletions app/lib/backend/http/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,52 @@ Future<http.MultipartRequest> _buildMultipartRequest({
required Map<String, String> fields,
required String fileFieldName,
required String method,
void Function(int sentBytes, int totalBytes, double? speedKBps)? onUploadProgress,
Duration progressUpdateInterval = const Duration(milliseconds: 200),
}) async {
var request = http.MultipartRequest(method, Uri.parse(url));
request.headers.addAll(headers);
request.fields.addAll(fields);

final fileLengths = <String, int>{};
var totalFileBytes = 0;
for (var file in files) {
var stream = http.ByteStream(file.openRead());
var length = await file.length();
final length = await file.length();
fileLengths[file.path] = length;
totalFileBytes += length;
}

var uploadedBytes = 0;
final speedStopwatch = Stopwatch()..start();
DateTime? lastProgressEmission;

for (var file in files) {
var length = fileLengths[file.path] ?? await file.length();
var rawStream = file.openRead();
var stream = http.ByteStream(rawStream.transform(
StreamTransformer<List<int>, List<int>>.fromHandlers(
handleData: (chunk, sink) {
uploadedBytes += chunk.length;

if (onUploadProgress != null) {
final now = DateTime.now();
final shouldEmit = lastProgressEmission == null ||
now.difference(lastProgressEmission!) >= progressUpdateInterval ||
uploadedBytes >= totalFileBytes;

if (shouldEmit) {
lastProgressEmission = now;
final elapsedMs = speedStopwatch.elapsedMilliseconds;
final speedKBps = elapsedMs > 0 ? (uploadedBytes / 1024) / (elapsedMs / 1000) : null;
onUploadProgress(uploadedBytes, totalFileBytes, speedKBps);
}
}

sink.add(chunk);
},
),
));

var multipartFile = http.MultipartFile(fileFieldName, stream, length, filename: basename(file.path));
request.files.add(multipartFile);
}
Expand All @@ -184,6 +222,8 @@ Future<http.Response> makeMultipartApiCall({
Map<String, String> fields = const {},
String fileFieldName = 'files',
String method = 'POST',
void Function(int sentBytes, int totalBytes, double? speedKBps)? onUploadProgress,
Duration progressUpdateInterval = const Duration(milliseconds: 200),
}) async {
try {
final bool requireAuthCheck = _isRequiredAuthCheck(url);
Expand All @@ -196,6 +236,8 @@ Future<http.Response> makeMultipartApiCall({
fields: fields,
fileFieldName: fileFieldName,
method: method,
onUploadProgress: onUploadProgress,
progressUpdateInterval: progressUpdateInterval,
);

var streamedResponse = await HttpPoolManager.instance.sendStreaming(request);
Expand All @@ -213,6 +255,8 @@ Future<http.Response> makeMultipartApiCall({
fields: fields,
fileFieldName: fileFieldName,
method: method,
onUploadProgress: onUploadProgress,
progressUpdateInterval: progressUpdateInterval,
);
streamedResponse = await HttpPoolManager.instance.sendStreaming(request);
response = await http.Response.fromStream(streamedResponse);
Expand All @@ -235,6 +279,16 @@ Future<http.Response> makeMultipartApiCall({
}
}

if (onUploadProgress != null) {
var totalFileBytes = 0;
for (var file in files) {
totalFileBytes += await file.length();
}
if (totalFileBytes > 0) {
onUploadProgress(totalFileBytes, totalFileBytes, null);
}
}
Comment on lines 279 to +290
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 totalFileBytes re-computed via async I/O after response is received

After a successful response, the code calculates totalFileBytes a second time by iterating over files and calling await file.length() for each file. This is the same calculation already performed inside _buildMultipartRequest. For large batches this adds unnecessary async I/O after the upload is already complete.

The 100%-complete callback could instead reuse the total calculated earlier, e.g. by returning it from _buildMultipartRequest or computing it once before the request is built.

// Current (after-response block):
if (onUploadProgress != null) {
  var totalFileBytes = 0;
  for (var file in files) {
    totalFileBytes += await file.length();   // redundant I/O
  }
  if (totalFileBytes > 0) {
    onUploadProgress(totalFileBytes, totalFileBytes, null);
  }
}


return response;
} catch (e, stackTrace) {
Logger.debug('Multipart HTTP request failed: $e, $stackTrace');
Expand Down
10 changes: 10 additions & 0 deletions app/lib/backend/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ class SharedPreferencesUtil {

String get deviceName => getString('deviceName');

bool getDeviceAutoSyncEnabled(String deviceId) {
if (deviceId.isEmpty) return true;
return getBool('deviceAutoSyncEnabled:$deviceId', defaultValue: true);
}

Future<bool> setDeviceAutoSyncEnabled(String deviceId, bool enabled) async {
if (deviceId.isEmpty) return false;
return await saveBool('deviceAutoSyncEnabled:$deviceId', enabled);
}

bool get deviceIsV2 => getBool('deviceIsV2');

set deviceIsV2(bool value) => saveBool('deviceIsV2', value);
Expand Down
Loading
Loading