✨ New protocol to upload file
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
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