New protocol to upload file

This commit is contained in:
2025-09-21 18:46:48 +08:00
parent d737232dcf
commit 9bdd08d8dd
2 changed files with 236 additions and 22 deletions

View File

@@ -1,15 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:native_exif/native_exif.dart'; import 'package:native_exif/native_exif.dart';
import 'package:tus_client_dart/tus_client_dart.dart'; import 'package:path_provider/path_provider.dart';
Future<XFile?> cropImage( Future<XFile?> cropImage(
BuildContext context, { BuildContext context, {
@@ -44,6 +44,7 @@ Completer<SnCloudFile?> putMediaToCloud({
required UniversalFile fileData, required UniversalFile fileData,
required String atk, required String atk,
required String baseUrl, required String baseUrl,
String? poolId,
String? filename, String? filename,
String? mimetype, String? mimetype,
Function(double progress, Duration estimate)? onProgress, Function(double progress, Duration estimate)? onProgress,
@@ -85,6 +86,7 @@ Completer<SnCloudFile?> putMediaToCloud({
fileData, fileData,
atk, atk,
baseUrl, baseUrl,
poolId,
filename, filename,
mimetype, mimetype,
onProgress, onProgress,
@@ -98,6 +100,7 @@ Completer<SnCloudFile?> putMediaToCloud({
fileData, fileData,
atk, atk,
baseUrl, baseUrl,
poolId,
filename, filename,
mimetype, mimetype,
onProgress, onProgress,
@@ -114,6 +117,7 @@ Completer<SnCloudFile?> putMediaToCloud({
fileData, fileData,
atk, atk,
baseUrl, baseUrl,
poolId,
filename, filename,
mimetype, mimetype,
onProgress, onProgress,
@@ -127,6 +131,7 @@ Completer<SnCloudFile?> _processUpload(
UniversalFile fileData, UniversalFile fileData,
String atk, String atk,
String baseUrl, String baseUrl,
String? poolId,
String? filename, String? filename,
String? mimetype, String? mimetype,
Function(double progress, Duration estimate)? onProgress, Function(double progress, Duration estimate)? onProgress,
@@ -168,26 +173,80 @@ Completer<SnCloudFile?> _processUpload(
return completer; return completer;
} }
final Map<String, String> metadata = { // Create Dio instance
'filename': actualFilename, final dio = Dio(
'content-type': actualMimetype, BaseOptions(
}; baseUrl: baseUrl,
headers: {
'Authorization': 'AtField $atk',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
final client = TusClient(file); final uploader = FileUploader(dio);
client
.upload( // Get File object
uri: Uri.parse('$baseUrl/drive/tus'), File fileObj;
headers: {'Authorization': 'AtField $atk'}, if (file.path.isNotEmpty) {
metadata: metadata, fileObj = File(file.path);
onComplete: (lastResponse) { // Call progress start
final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!); onProgress?.call(0.0, Duration.zero);
completer.complete(SnCloudFile.fromJson(resp)); uploader
}, .uploadFile(
onProgress: (double progress, Duration estimate) { file: fileObj,
onProgress?.call(progress, estimate); fileName: actualFilename,
}, contentType: actualMimetype,
) poolId: poolId,
.catchError(completer.completeError); )
.then((result) {
// Call progress end
onProgress?.call(1.0, Duration.zero);
completer.complete(result);
})
.catchError((e) {
completer.completeError(e);
throw e;
});
} else {
// Write to temp file
getTemporaryDirectory()
.then((tempDir) {
final tempFile = File('${tempDir.path}/temp_upload_$actualFilename');
tempFile
.writeAsBytes(byteData!)
.then((_) {
fileObj = tempFile;
// Call progress start
onProgress?.call(0.0, Duration.zero);
uploader
.uploadFile(
file: fileObj,
fileName: actualFilename,
contentType: actualMimetype,
poolId: poolId,
)
.then((result) {
// Call progress end
onProgress?.call(1.0, Duration.zero);
completer.complete(result);
})
.catchError((e) {
completer.completeError(e);
throw e;
});
})
.catchError((e) {
completer.completeError(e);
throw e;
});
})
.catchError((e) {
completer.completeError(e);
throw e;
});
}
return completer; return completer;
} }

View 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);
});