✨ New protocol to upload file
This commit is contained in:
155
lib/services/file_uploader.dart
Normal file
155
lib/services/file_uploader.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
class FileUploader {
|
||||
final Dio _dio;
|
||||
|
||||
FileUploader(this._dio);
|
||||
|
||||
/// Calculates the MD5 hash of a file.
|
||||
Future<String> _calculateFileHash(File file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
final digest = md5.convert(bytes);
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Creates an upload task for the given file.
|
||||
Future<Map<String, dynamic>> createUploadTask({
|
||||
required File file,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? chunkSize,
|
||||
}) async {
|
||||
final hash = await _calculateFileHash(file);
|
||||
final fileSize = await file.length();
|
||||
|
||||
final response = await _dio.post(
|
||||
'/drive/files/upload/create',
|
||||
data: {
|
||||
'hash': hash,
|
||||
'file_name': fileName,
|
||||
'file_size': fileSize,
|
||||
'content_type': contentType,
|
||||
'pool_id': poolId,
|
||||
'bundle_id': bundleId,
|
||||
'encrypt_password': encryptPassword,
|
||||
'expired_at': expiredAt,
|
||||
'chunk_size': chunkSize,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/// Uploads a single chunk of the file.
|
||||
Future<void> uploadChunk({
|
||||
required String taskId,
|
||||
required int chunkIndex,
|
||||
required Uint8List chunkData,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'chunk': MultipartFile.fromBytes(
|
||||
chunkData,
|
||||
filename: 'chunk_$chunkIndex',
|
||||
),
|
||||
});
|
||||
|
||||
await _dio.post(
|
||||
'/drive/files/upload/chunk/$taskId/$chunkIndex',
|
||||
data: formData,
|
||||
);
|
||||
}
|
||||
|
||||
/// Completes the upload and returns the CloudFile object.
|
||||
Future<SnCloudFile> completeUpload(String taskId) async {
|
||||
final response = await _dio.post('/drive/files/upload/complete/$taskId');
|
||||
|
||||
return SnCloudFile.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Uploads a file in chunks using the multi-part API.
|
||||
Future<SnCloudFile> uploadFile({
|
||||
required File file,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? customChunkSize,
|
||||
}) async {
|
||||
// Step 1: Create upload task
|
||||
final createResponse = await createUploadTask(
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
bundleId: bundleId,
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
chunkSize: customChunkSize,
|
||||
);
|
||||
|
||||
if (createResponse['file_exists'] == true) {
|
||||
// File already exists, return the existing file
|
||||
return SnCloudFile.fromJson(createResponse['file']);
|
||||
}
|
||||
|
||||
final taskId = createResponse['task_id'] as String;
|
||||
final chunkSize = createResponse['chunk_size'] as int;
|
||||
final chunksCount = createResponse['chunks_count'] as int;
|
||||
|
||||
// Step 2: Upload chunks
|
||||
final stream = file.openRead();
|
||||
final chunks = <Uint8List>[];
|
||||
int bytesRead = 0;
|
||||
final buffer = BytesBuilder();
|
||||
|
||||
await for (final chunk in stream) {
|
||||
buffer.add(chunk);
|
||||
bytesRead += chunk.length;
|
||||
|
||||
if (bytesRead >= chunkSize) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
bytesRead = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining bytes as last chunk
|
||||
if (buffer.length > 0) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
}
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
||||
// Step 3: Complete upload
|
||||
return await completeUpload(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// Riverpod provider for the FileUploader service
|
||||
final fileUploaderProvider = Provider<FileUploader>((ref) {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
return FileUploader(dio);
|
||||
});
|
Reference in New Issue
Block a user