|
@@ -0,0 +1,172 @@
|
|
|
|
|
+import 'dart:io';
|
|
|
|
|
+import 'package:intl/intl.dart';
|
|
|
|
|
+import 'package:mime/mime.dart';
|
|
|
|
|
+import 'package:shelf/shelf.dart' as shelf;
|
|
|
|
|
+//import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
|
|
|
+import 'package:shelf_router/shelf_router.dart';
|
|
|
|
|
+import 'package:path/path.dart' as path;
|
|
|
|
|
+import 'package:http_parser/http_parser.dart';
|
|
|
|
|
+import 'package:archive/archive.dart';
|
|
|
|
|
+import 'package:supabase/supabase.dart';
|
|
|
|
|
+
|
|
|
|
|
+class FileUploadApi {
|
|
|
|
|
+ SupabaseClient getSupabaseClient(shelf.Request request) {
|
|
|
|
|
+ final supabaseUrl = request.headers['supabase-url'];
|
|
|
|
|
+ if (supabaseUrl == null) {
|
|
|
|
|
+ throw Exception('Supabase URL not provided in headers');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ final authHeader = request.headers['authorization'];
|
|
|
|
|
+ if (authHeader == null || !authHeader.startsWith('Bearer ')) {
|
|
|
|
|
+ throw Exception('Invalid or missing Authorization Bearer token');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ final token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
|
|
|
+
|
|
|
|
|
+ return SupabaseClient(
|
|
|
|
|
+ supabaseUrl,
|
|
|
|
|
+ token,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String getTimestampedFilename(String originalFilename) {
|
|
|
|
|
+ final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
|
|
|
|
+ return '${timestamp}_$originalFilename';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Future<void> uploadToSupabase(
|
|
|
|
|
+ String filePath, String filename, SupabaseClient supabaseClient,
|
|
|
|
|
+ {bool timestamped = false,
|
|
|
|
|
+ required String bucket,
|
|
|
|
|
+ bool upsert = true}) async {
|
|
|
|
|
+ try {
|
|
|
|
|
+ final file = File(filePath);
|
|
|
|
|
+ final timestampedFilename =
|
|
|
|
|
+ timestamped ? getTimestampedFilename(filename) : filename;
|
|
|
|
|
+
|
|
|
|
|
+ await supabaseClient.storage.from(bucket).upload(
|
|
|
|
|
+ timestampedFilename,
|
|
|
|
|
+ file,
|
|
|
|
|
+ fileOptions: FileOptions(
|
|
|
|
|
+ cacheControl: '3600',
|
|
|
|
|
+ upsert: upsert,
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ print('File uploaded to Supabase: $timestampedFilename');
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ print('Error uploading to Supabase: $e');
|
|
|
|
|
+ rethrow;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Future<void> initializeDirectories() async {
|
|
|
|
|
+ final directories = [
|
|
|
|
|
+ Directory('./uploaded/raw'),
|
|
|
|
|
+ Directory('./uploaded/data'),
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ for (var dir in directories) {
|
|
|
|
|
+ if (!await dir.exists()) {
|
|
|
|
|
+ await dir.create(recursive: true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ bool isZipFile(List<int> bytes) {
|
|
|
|
|
+ if (bytes.length < 4) return false;
|
|
|
|
|
+ return bytes[0] == 0x50 &&
|
|
|
|
|
+ bytes[1] == 0x4B &&
|
|
|
|
|
+ bytes[2] == 0x03 &&
|
|
|
|
|
+ bytes[3] == 0x04;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Future<void> processZipFile(String filePath) async {
|
|
|
|
|
+ final bytes = await File(filePath).readAsBytes();
|
|
|
|
|
+ final archive = ZipDecoder().decodeBytes(bytes);
|
|
|
|
|
+
|
|
|
|
|
+ for (final file in archive) {
|
|
|
|
|
+ final filename = file.name;
|
|
|
|
|
+ if (file.isFile) {
|
|
|
|
|
+ final data = file.content as List<int>;
|
|
|
|
|
+ final outFile = File(path.join('./uploaded/data', filename));
|
|
|
|
|
+ await outFile.parent.create(recursive: true);
|
|
|
|
|
+ await outFile.writeAsBytes(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Router get router {
|
|
|
|
|
+ final router = Router();
|
|
|
|
|
+
|
|
|
|
|
+ router.get('/', (shelf.Request request) {
|
|
|
|
|
+ return shelf.Response.ok('Hello World');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ router.post('/upload', (shelf.Request request) async {
|
|
|
|
|
+ final supabaseClient = getSupabaseClient(request);
|
|
|
|
|
+ await initializeDirectories();
|
|
|
|
|
+
|
|
|
|
|
+ final contentType = request.headers['content-type'];
|
|
|
|
|
+ if (contentType == null ||
|
|
|
|
|
+ !contentType.toLowerCase().startsWith('multipart/form-data')) {
|
|
|
|
|
+ return shelf.Response.badRequest(
|
|
|
|
|
+ body: 'Content-Type must be multipart/form-data');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ final mediaType = MediaType.parse(contentType);
|
|
|
|
|
+ final boundary = mediaType.parameters['boundary'];
|
|
|
|
|
+ if (boundary == null) {
|
|
|
|
|
+ return shelf.Response.badRequest(body: 'Boundary not found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ final transformer = MimeMultipartTransformer(boundary);
|
|
|
|
|
+ final bodyBytes = await request.read().expand((e) => e).toList();
|
|
|
|
|
+ final stream = Stream.fromIterable([bodyBytes]);
|
|
|
|
|
+ final parts = await transformer.bind(stream).toList();
|
|
|
|
|
+
|
|
|
|
|
+ for (var part in parts) {
|
|
|
|
|
+ final contentDisposition = part.headers['content-disposition'];
|
|
|
|
|
+ if (contentDisposition == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ final filenameMatch =
|
|
|
|
|
+ RegExp(r'filename="([^"]*)"').firstMatch(contentDisposition);
|
|
|
|
|
+ if (filenameMatch == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ final filename = filenameMatch.group(1);
|
|
|
|
|
+ if (filename == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ final bytes = await part.fold<List<int>>(
|
|
|
|
|
+ [],
|
|
|
|
|
+ (prev, element) => [...prev, ...element],
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ final rawFilePath = path.join('./uploaded/raw', filename);
|
|
|
|
|
+ await File(rawFilePath).writeAsBytes(bytes);
|
|
|
|
|
+
|
|
|
|
|
+ if (isZipFile(bytes)) {
|
|
|
|
|
+ await processZipFile(rawFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ final dataFilePath = path.join('./uploaded/data', filename);
|
|
|
|
|
+ await File(rawFilePath).copy(dataFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ await uploadToSupabase(rawFilePath, filename, supabaseClient,
|
|
|
|
|
+ bucket: 'csvhich_archive', timestamped: true, upsert: false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return shelf.Response.ok('File processed and uploaded successfully');
|
|
|
|
|
+ } catch (e, stackTrace) {
|
|
|
|
|
+ print('Error: $e\n$stackTrace');
|
|
|
|
|
+ return shelf.Response.internalServerError(
|
|
|
|
|
+ body: 'Error processing upload: $e');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ supabaseClient.dispose();
|
|
|
|
|
+ await File('./uploaded/raw').delete(recursive: true);
|
|
|
|
|
+ await File('./uploaded/data').delete(recursive: true);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return router;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|