♻️ Refactored file uploading

This commit is contained in:
2025-11-08 20:04:54 +08:00
parent 5ebefae961
commit bfcbed035c
8 changed files with 148 additions and 94 deletions

View File

@@ -1,7 +1,5 @@
PODS:
- Alamofire (5.10.2)
- app_links (6.4.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- croppy (0.0.1):
@@ -52,18 +50,18 @@ PODS:
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.3):
- firebase_analytics (12.0.4):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- Flutter
- firebase_core (4.2.0):
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- Flutter
- firebase_crashlytics (5.0.3):
- firebase_crashlytics (5.0.4):
- Firebase/Crashlytics (= 12.4.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.3):
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_core
- Flutter
@@ -265,6 +263,8 @@ PODS:
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- receive_sharing_intent (1.8.1):
- Flutter
- record_ios (1.1.0):
@@ -323,7 +323,6 @@ PODS:
DEPENDENCIES:
- Alamofire
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@@ -358,6 +357,7 @@ DEPENDENCIES:
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
@@ -404,8 +404,6 @@ SPEC REPOS:
- WebRTC-SDK
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
croppy:
@@ -470,6 +468,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios:
:path: ".symlinks/plugins/protocol_handler_ios/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
record_ios:
@@ -497,7 +497,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@@ -506,10 +505,10 @@ SPEC CHECKSUMS:
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
@@ -553,6 +552,7 @@ SPEC CHECKSUMS:
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c

View File

@@ -2,6 +2,7 @@ import "dart:async";
import "dart:math" as math;
import "package:easy_localization/easy_localization.dart";
import "package:file_picker/file_picker.dart";
import "package:image_picker/image_picker.dart";
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
import "package:flutter_hooks/flutter_hooks.dart";
@@ -181,16 +182,13 @@ class ChatRoomScreen extends HookConsumerWidget {
}, [scrollController]);
Future<void> pickPhotoMedia() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
final ImagePicker picker = ImagePicker();
final List<XFile> results = await picker.pickMultiImage();
if (results.isEmpty) return;
attachments.value = [
...attachments.value,
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
...results.map(
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
),
];
}

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:convert/convert.dart';
import 'package:cross_file/cross_file.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
@@ -21,9 +22,51 @@ class FileUploader {
return digest.toString();
}
/// Calculates the MD5 hash from a stream.
Future<String> _calculateFileHashFromStream(Stream<List<int>> stream) async {
final accumulator = AccumulatorSink<Digest>();
final converter = md5.startChunkedConversion(accumulator);
await for (final chunk in stream) {
converter.add(chunk);
}
converter.close();
final digest = accumulator.events.single;
return digest.toString();
}
/// Reads the next chunk from a stream subscription.
Future<Uint8List> _readNextChunk(
StreamSubscription<List<int>> subscription,
int size,
) async {
final completer = Completer<Uint8List>();
final buffer = <int>[];
int remaining = size;
void onData(List<int> data) {
buffer.addAll(data);
remaining -= data.length;
if (remaining <= 0) {
subscription.pause();
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
}
}
void onDone() {
if (!completer.isCompleted) {
completer.complete(Uint8List.fromList(buffer));
}
}
subscription.onData(onData);
subscription.onDone(onDone);
return completer.future;
}
/// Creates an upload task for the given file.
Future<Map<String, dynamic>> createUploadTask({
required Uint8List bytes,
required dynamic fileData,
required String fileName,
required String contentType,
String? poolId,
@@ -32,8 +75,17 @@ class FileUploader {
String? expiredAt,
int? chunkSize,
}) async {
final hash = _calculateFileHash(bytes);
final fileSize = bytes.length;
String hash;
int fileSize;
if (fileData is XFile) {
fileSize = await fileData.length();
hash = await _calculateFileHashFromStream(fileData.openRead());
} else if (fileData is Uint8List) {
hash = _calculateFileHash(fileData);
fileSize = fileData.length;
} else {
throw ArgumentError('Invalid fileData type');
}
final response = await _client.post(
'/drive/files/upload/create',
@@ -81,7 +133,7 @@ class FileUploader {
/// Uploads a file in chunks using the multi-part API.
Future<SnCloudFile> uploadFile({
required Uint8List bytes,
required dynamic fileData,
required String fileName,
required String contentType,
String? poolId,
@@ -92,7 +144,7 @@ class FileUploader {
}) async {
// Step 1: Create upload task
final createResponse = await createUploadTask(
bytes: bytes,
fileData: fileData,
fileName: fileName,
contentType: contentType,
poolId: poolId,
@@ -112,22 +164,31 @@ class FileUploader {
final chunksCount = createResponse['chunks_count'] as int;
// Step 2: Upload chunks
final chunks = <Uint8List>[];
for (int i = 0; i < bytes.length; i += chunkSize) {
final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize;
chunks.add(Uint8List.fromList(bytes.sublist(i, end)));
}
if (fileData is XFile) {
// Use stream for XFile
final subscription = fileData.openRead().listen(null);
subscription.pause();
for (int i = 0; i < chunksCount; i++) {
subscription.resume();
final chunkData = await _readNextChunk(subscription, chunkSize);
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunkData);
}
subscription.cancel();
} else if (fileData is Uint8List) {
// Use old way for Uint8List
final chunks = <Uint8List>[];
for (int i = 0; i < fileData.length; i += chunkSize) {
final end =
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
}
// Ensure we have the correct number of chunks
if (chunks.length != chunksCount) {
throw Exception(
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
);
}
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
}
} else {
throw ArgumentError('Invalid fileData type');
}
// Step 3: Complete upload
@@ -216,23 +277,15 @@ class FileUploader {
final data = fileData.data;
if (data is XFile) {
// Read bytes from XFile
data
.readAsBytes()
.then((readBytes) {
_performUpload(
bytes: readBytes,
fileName: fileData.displayName ?? data.name,
contentType: actualMimetype,
client: client,
poolId: poolId,
onProgress: onProgress,
completer: completer,
);
})
.catchError((e) {
completer.completeError(e);
});
_performUpload(
fileData: data,
fileName: fileData.displayName ?? data.name,
contentType: actualMimetype,
client: client,
poolId: poolId,
onProgress: onProgress,
completer: completer,
);
return completer;
} else if (data is List<int> || data is Uint8List) {
bytes = data is List<int> ? Uint8List.fromList(data) : data;
@@ -252,7 +305,7 @@ class FileUploader {
if (bytes != null) {
_performUpload(
bytes: bytes,
fileData: bytes,
fileName: actualFilename,
contentType: actualMimetype,
client: client,
@@ -267,7 +320,7 @@ class FileUploader {
// Helper method to perform the actual upload
static void _performUpload({
required Uint8List bytes,
required dynamic fileData,
required String fileName,
required String contentType,
required Dio client,
@@ -281,7 +334,7 @@ class FileUploader {
onProgress?.call(0.0, Duration.zero);
uploader
.uploadFile(
bytes: bytes,
fileData: fileData,
fileName: fileName,
contentType: contentType,
poolId: poolId,

View File

@@ -112,23 +112,28 @@ class CloudFilePicker extends HookConsumerWidget {
void pickImage() async {
showLoadingModal(context);
final result = await FilePicker.platform.pickFiles(
allowMultiple: allowMultiple,
type: FileType.image,
);
if (result == null || result.files.isEmpty) {
final ImagePicker picker = ImagePicker();
List<XFile> results;
if (allowMultiple) {
results = await picker.pickMultiImage();
} else {
final XFile? result = await picker.pickImage(
source: ImageSource.gallery,
);
results = result != null ? [result] : [];
}
if (results.isEmpty) {
if (context.mounted) hideLoadingModal(context);
return;
}
final newFiles =
result.files.map((e) {
final xfile =
e.bytes != null
? XFile.fromData(e.bytes!, name: e.name)
: XFile(e.path!);
return UniversalFile(data: xfile, type: UniversalFileType.image);
}).toList();
results
.map(
(xfile) =>
UniversalFile(data: xfile, type: UniversalFileType.image),
)
.toList();
if (!allowMultiple) {
files.value = newFiles;

View File

@@ -402,16 +402,13 @@ class ComposeLogic {
}
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
final ImagePicker picker = ImagePicker();
final List<XFile> results = await picker.pickMultiImage();
if (results.isEmpty) return;
state.attachments.value = [
...state.attachments.value,
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
...results.map(
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
),
];
}

View File

@@ -19,19 +19,19 @@ PODS:
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.3):
- firebase_analytics (12.0.4):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- FlutterMacOS
- firebase_core (4.2.0):
- firebase_core (4.2.1):
- Firebase/CoreOnly (~> 12.4.0)
- FlutterMacOS
- firebase_crashlytics (5.0.3):
- firebase_crashlytics (5.0.4):
- Firebase/CoreOnly (~> 12.4.0)
- Firebase/Crashlytics (~> 12.4.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (16.0.3):
- firebase_messaging (16.0.4):
- Firebase/CoreOnly (~> 12.4.0)
- Firebase/Messaging (~> 12.4.0)
- firebase_core
@@ -416,10 +416,10 @@ SPEC CHECKSUMS:
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: d876586269c1d8d2b3dcac085bc2d97c62abc9df
firebase_core: d81d1a44df95699ce074ae63d8cb43e9df21e142
firebase_crashlytics: 723622cc39a9fa7320585424f5864c5699893ce1
firebase_messaging: 31f412ae5a54e02d1c46d467969f7ad92c4b81ec
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018

View File

@@ -282,7 +282,7 @@ packages:
source: hosted
version: "4.1.0"
convert:
dependency: transitive
dependency: "direct main"
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
@@ -1185,10 +1185,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
url: "https://pub.dev"
source: hosted
version: "16.3.0"
version: "17.0.0"
google_fonts:
dependency: "direct main"
description:

View File

@@ -38,7 +38,7 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_hooks: ^0.21.3+1
hooks_riverpod: ^2.6.1
go_router: ^16.3.0
go_router: ^17.0.0
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
@@ -165,6 +165,7 @@ dependencies:
dio_smart_retry: ^7.0.1
flutter_expandable_fab: ^2.5.2
event_bus: ^2.0.1
convert: ^3.1.2
dev_dependencies:
flutter_test: