The tus-based upload flow (/drive/tus) has been removed upstream in favor of a new multipart upload protocol. This commit replaces all TusClient usage with the new FileUploader service that follows the official /drive/files/upload/{create,chunk,complete} endpoints. Changes include: - remove tus_client_dart dependency and related code - add putFileToPool() backed by FileUploader.uploadFile() - update uploadAttachment() to call the new putFileToPool - preserve poolId support, filename, and mimetype handling - ensure progress callbacks fire at start and completion This aligns the client with the new upload protocol while keeping the same Compose UI and settings logic introduced in earlier patches. Signed-off-by: Texas0295 <kimura@texas0295.top>
302 lines
8.2 KiB
Dart
302 lines
8.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:ui';
|
|
import 'package:croppy/croppy.dart';
|
|
import 'package:cross_file/cross_file.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:island/models/file.dart';
|
|
import 'package:island/services/file_uploader.dart';
|
|
import 'package:native_exif/native_exif.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
Future<XFile?> cropImage(
|
|
BuildContext context, {
|
|
required XFile image,
|
|
List<CropAspectRatio?>? allowedAspectRatios,
|
|
bool replacePath = false,
|
|
}) async {
|
|
final result = await showMaterialImageCropper(
|
|
context,
|
|
imageProvider:
|
|
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)),
|
|
showLoadingIndicatorOnSubmit: true,
|
|
allowedAspectRatios: allowedAspectRatios,
|
|
);
|
|
if (result == null) return null; // Cancelled operation
|
|
final croppedFile = result.uiImage;
|
|
final croppedBytes = await croppedFile.toByteData(
|
|
format: ImageByteFormat.png,
|
|
);
|
|
if (croppedBytes == null) {
|
|
return image;
|
|
}
|
|
croppedFile.dispose();
|
|
return XFile.fromData(
|
|
croppedBytes.buffer.asUint8List(),
|
|
path: !replacePath ? image.path : null,
|
|
mimeType: image.mimeType,
|
|
);
|
|
}
|
|
|
|
Completer<SnCloudFile?> putFileToPool({
|
|
required UniversalFile fileData,
|
|
required String atk,
|
|
required String baseUrl,
|
|
required String poolId,
|
|
String? filename,
|
|
String? mimetype,
|
|
Function(double progress, Duration estimate)? onProgress,
|
|
}) {
|
|
final completer = Completer<SnCloudFile?>();
|
|
final data = fileData.data;
|
|
if (data is! XFile) {
|
|
completer.completeError(
|
|
ArgumentError('Unsupported fileData type for putFileToPool'),
|
|
);
|
|
return completer;
|
|
}
|
|
|
|
final actualFilename = filename ?? data.name;
|
|
final actualMimetype = mimetype ?? data.mimeType ?? 'application/octet-stream';
|
|
|
|
final dio = Dio(BaseOptions(
|
|
baseUrl: baseUrl,
|
|
headers: {
|
|
'Authorization': 'AtField $atk',
|
|
'Accept': 'application/json',
|
|
},
|
|
));
|
|
|
|
final uploader = FileUploader(dio);
|
|
final fileObj = File(data.path);
|
|
|
|
onProgress?.call(0.0, Duration.zero);
|
|
uploader.uploadFile(
|
|
file: fileObj,
|
|
fileName: actualFilename,
|
|
contentType: actualMimetype,
|
|
poolId: poolId,
|
|
).then((result) {
|
|
onProgress?.call(1.0, Duration.zero);
|
|
completer.complete(result);
|
|
}).catchError((e) {
|
|
completer.completeError(e);
|
|
});
|
|
|
|
return completer;
|
|
}
|
|
|
|
|
|
Completer<SnCloudFile?> putMediaToCloud({
|
|
required UniversalFile fileData,
|
|
required String atk,
|
|
required String baseUrl,
|
|
String? poolId,
|
|
String? filename,
|
|
String? mimetype,
|
|
Function(double progress, Duration estimate)? onProgress,
|
|
}) {
|
|
final completer = Completer<SnCloudFile?>();
|
|
|
|
// Process the image to remove GPS EXIF data if needed
|
|
if (fileData.isOnDevice && fileData.type == UniversalFileType.image) {
|
|
final data = fileData.data;
|
|
if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
|
// Use native_exif to selectively remove GPS data
|
|
Exif.fromPath(data.path)
|
|
.then((exif) {
|
|
// Remove GPS-related attributes
|
|
final gpsAttributes = [
|
|
'GPSLatitude',
|
|
'GPSLatitudeRef',
|
|
'GPSLongitude',
|
|
'GPSLongitudeRef',
|
|
'GPSAltitude',
|
|
'GPSAltitudeRef',
|
|
'GPSTimeStamp',
|
|
'GPSProcessingMethod',
|
|
'GPSDateStamp',
|
|
];
|
|
|
|
// Create a map of attributes to clear
|
|
final clearAttributes = <String, String>{};
|
|
for (final attr in gpsAttributes) {
|
|
clearAttributes[attr] = '';
|
|
}
|
|
|
|
// Write empty values to remove GPS data
|
|
return exif.writeAttributes(clearAttributes);
|
|
})
|
|
.then((_) {
|
|
// Continue with upload after GPS data is removed
|
|
_processUpload(
|
|
fileData,
|
|
atk,
|
|
baseUrl,
|
|
poolId,
|
|
filename,
|
|
mimetype,
|
|
onProgress,
|
|
completer,
|
|
);
|
|
})
|
|
.catchError((e) {
|
|
// If there's an error, continue with the original file
|
|
debugPrint('Error removing GPS EXIF data: $e');
|
|
_processUpload(
|
|
fileData,
|
|
atk,
|
|
baseUrl,
|
|
poolId,
|
|
filename,
|
|
mimetype,
|
|
onProgress,
|
|
completer,
|
|
);
|
|
});
|
|
|
|
return completer;
|
|
}
|
|
}
|
|
|
|
// If not an image or on web, continue with normal upload
|
|
_processUpload(
|
|
fileData,
|
|
atk,
|
|
baseUrl,
|
|
poolId,
|
|
filename,
|
|
mimetype,
|
|
onProgress,
|
|
completer,
|
|
);
|
|
return completer;
|
|
}
|
|
|
|
// Helper method to process the upload after any EXIF processing
|
|
Completer<SnCloudFile?> _processUpload(
|
|
UniversalFile fileData,
|
|
String atk,
|
|
String baseUrl,
|
|
String? poolId,
|
|
String? filename,
|
|
String? mimetype,
|
|
Function(double progress, Duration estimate)? onProgress,
|
|
Completer<SnCloudFile?> completer,
|
|
) {
|
|
late XFile file;
|
|
String actualFilename = filename ?? 'randomly_file';
|
|
String actualMimetype = mimetype ?? '';
|
|
Uint8List? byteData;
|
|
|
|
// Handle the data based on what's in the UniversalFile
|
|
final data = fileData.data;
|
|
|
|
if (data is XFile) {
|
|
file = data;
|
|
actualFilename = filename ?? data.name;
|
|
actualMimetype = mimetype ?? data.mimeType ?? '';
|
|
} else if (data is List<int> || data is Uint8List) {
|
|
byteData = data is List<int> ? Uint8List.fromList(data) : data;
|
|
actualFilename = filename ?? 'uploaded_file';
|
|
actualMimetype = mimetype ?? 'application/octet-stream';
|
|
if (mimetype == null) {
|
|
completer.completeError(
|
|
ArgumentError('Mimetype is required when providing raw bytes.'),
|
|
);
|
|
return completer;
|
|
}
|
|
file = XFile.fromData(byteData!, mimeType: actualMimetype);
|
|
} else if (data is SnCloudFile) {
|
|
// If the file is already on the cloud, just return it
|
|
completer.complete(data);
|
|
return completer;
|
|
} else {
|
|
completer.completeError(
|
|
ArgumentError(
|
|
'Invalid fileData type. Expected data to be XFile, List<int>, Uint8List, or SnCloudFile.',
|
|
),
|
|
);
|
|
return completer;
|
|
}
|
|
|
|
// Create Dio instance
|
|
final dio = Dio(
|
|
BaseOptions(
|
|
baseUrl: baseUrl,
|
|
headers: {
|
|
'Authorization': 'AtField $atk',
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
),
|
|
);
|
|
|
|
final uploader = FileUploader(dio);
|
|
|
|
// Get File object
|
|
File fileObj;
|
|
if (file.path.isNotEmpty) {
|
|
fileObj = File(file.path);
|
|
// 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;
|
|
});
|
|
} 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;
|
|
}
|