file_upload_api.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import 'dart:collection';
  2. import 'dart:io';
  3. import 'package:file_upload_processor/handlers/base_api.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:mime/mime.dart';
  6. import 'package:shelf/shelf.dart' as shelf;
  7. import 'package:path/path.dart' as path;
  8. import 'package:http_parser/http_parser.dart';
  9. import 'package:archive/archive.dart';
  10. import 'package:supabase/supabase.dart';
  11. class FileUploadApi extends BaseApi {
  12. FileUploadApi(shelf.Request request) : super(request);
  13. final uploadFolder = "./uploaded";
  14. String get rawFolder => "$uploadFolder/raw";
  15. String get dataFolder => "$uploadFolder/data";
  16. String workingFolder = "";
  17. String get zipFolder => "$rawFolder/$workingFolder";
  18. String get extFolder => "$dataFolder/$workingFolder";
  19. SupabaseClient getSupabaseClient(shelf.Request request) {
  20. final supabaseUrl = request.headers['supabase-url'];
  21. if (supabaseUrl == null) {
  22. throw Exception('Supabase URL not provided in headers');
  23. }
  24. final authHeader = request.headers['authorization'];
  25. if (authHeader == null || !authHeader.startsWith('Bearer ')) {
  26. throw Exception('Invalid or missing Authorization Bearer token');
  27. }
  28. final token = authHeader.substring(7); // Remove 'Bearer ' prefix
  29. return SupabaseClient(
  30. supabaseUrl,
  31. token,
  32. );
  33. }
  34. String getTimestampedFilename(String originalFilename) {
  35. final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
  36. return '${timestamp}_$originalFilename';
  37. }
  38. Future<void> uploadToSupabase(
  39. String filePath, String filename, SupabaseClient supabaseClient,
  40. {bool timestamped = false,
  41. required String bucket,
  42. bool upsert = true}) async {
  43. try {
  44. final file = File(filePath);
  45. final timestampedFilename =
  46. timestamped ? getTimestampedFilename(filename) : filename;
  47. await supabaseClient.storage.from(bucket).upload(
  48. timestampedFilename,
  49. file,
  50. fileOptions: FileOptions(
  51. cacheControl: '3600',
  52. upsert: upsert,
  53. ),
  54. );
  55. print('File uploaded to Supabase: $timestampedFilename');
  56. } catch (e) {
  57. print('Error uploading to Supabase: $e');
  58. rethrow;
  59. }
  60. }
  61. Future<void> initializeDirectories() async {
  62. final directories = [
  63. Directory(rawFolder),
  64. Directory(dataFolder),
  65. Directory(zipFolder),
  66. Directory(extFolder),
  67. ];
  68. for (var dir in directories) {
  69. if (!await dir.exists()) {
  70. await dir.create(recursive: true);
  71. }
  72. }
  73. }
  74. bool isZipFile(List<int> bytes) {
  75. if (bytes.length < 4) return false;
  76. return bytes[0] == 0x50 &&
  77. bytes[1] == 0x4B &&
  78. bytes[2] == 0x03 &&
  79. bytes[3] == 0x04;
  80. }
  81. Future<List<String>> processZipFile(String filePath) async {
  82. List<String> files = [];
  83. final bytes = await File(filePath).readAsBytes();
  84. final archive = ZipDecoder().decodeBytes(bytes);
  85. for (final file in archive) {
  86. final filename = file.name;
  87. if (file.isFile) {
  88. final data = file.content as List<int>;
  89. final outFile = File(path.join(extFolder, filename));
  90. await outFile.parent.create(recursive: true);
  91. await outFile.writeAsBytes(data);
  92. files.add(path.join(extFolder, filename));
  93. }
  94. }
  95. return files;
  96. }
  97. @override
  98. response() async {
  99. workingFolder = DateTime.now().millisecondsSinceEpoch.toString();
  100. final supabaseClient = getSupabaseClient(request);
  101. await initializeDirectories();
  102. final contentType = request.headers['content-type'];
  103. if (contentType == null ||
  104. !contentType.toLowerCase().startsWith('multipart/form-data')) {
  105. return shelf.Response.badRequest(
  106. body: 'Content-Type must be multipart/form-data');
  107. }
  108. try {
  109. final mediaType = MediaType.parse(contentType);
  110. final boundary = mediaType.parameters['boundary'];
  111. if (boundary == null) {
  112. return shelf.Response.badRequest(body: 'Boundary not found');
  113. }
  114. final transformer = MimeMultipartTransformer(boundary);
  115. final bodyBytes = await request.read().expand((e) => e).toList();
  116. final stream = Stream.fromIterable([bodyBytes]);
  117. final parts = await transformer.bind(stream).toList();
  118. for (var part in parts) {
  119. final contentDisposition = part.headers['content-disposition'];
  120. if (contentDisposition == null) continue;
  121. final filenameMatch =
  122. RegExp(r'filename="([^"]*)"').firstMatch(contentDisposition);
  123. if (filenameMatch == null) continue;
  124. final filename = filenameMatch.group(1);
  125. if (filename == null) continue;
  126. final bytes = await part.fold<List<int>>(
  127. [],
  128. (prev, element) => [...prev, ...element],
  129. );
  130. final rawFilePath = path.join(zipFolder, filename);
  131. await File(rawFilePath).writeAsBytes(bytes);
  132. List<String> files = [];
  133. if (isZipFile(bytes)) {
  134. files.addAll(await processZipFile(rawFilePath));
  135. } else {
  136. final dataFilePath = path.join(extFolder, filename);
  137. await File(rawFilePath).copy(dataFilePath);
  138. files.add(dataFilePath);
  139. }
  140. bytes.clear();
  141. await uploadToSupabase(rawFilePath, filename, supabaseClient,
  142. bucket: 'csvhich_archive', timestamped: true, upsert: false);
  143. for (var file in files) {
  144. final fileProcess = FileProcess(file, supabaseClient);
  145. await fileProcess.go();
  146. }
  147. }
  148. return shelf.Response.ok('File processed and uploaded successfully');
  149. } catch (e, stackTrace) {
  150. //print('Error: $e\n$stackTrace');
  151. return shelf.Response.internalServerError(
  152. body: 'Error processing upload: $e');
  153. } finally {
  154. supabaseClient.dispose();
  155. await File(zipFolder).delete(recursive: true);
  156. await File(extFolder).delete(recursive: true);
  157. }
  158. }
  159. }
  160. class FileProcess {
  161. FileProcess(this.filepath, this.supabase);
  162. final String filepath;
  163. final SupabaseClient supabase;
  164. String get filename => filepath.replaceAll('\\', "/").split("/").last;
  165. final Map<String, String> tables = {
  166. "secondprgtype.txt": "aclegs_csv",
  167. "ExportPGRGPNmois.txt": "pnlegs_csv",
  168. "exportPGRGPN.txt": "pnlegs_csv",
  169. "exportlicence.txt": "licences_csv",
  170. };
  171. final Map<String, List<String>> _headers = {
  172. "secondprgtype.txt": [
  173. "leg_no",
  174. "fn_carrier",
  175. "fn_number",
  176. "fn_suffix",
  177. "day_of_origin",
  178. "ac_owner",
  179. "ac_subtype",
  180. "ac_version",
  181. "ac_registration",
  182. "dep_ap_actual",
  183. "dep_ap_sched",
  184. "dep_dt_est",
  185. "dep_sched_dt",
  186. "arr_ap_actual",
  187. "arr_ap_sched",
  188. "arr_dt_est",
  189. "arr_sched_dt",
  190. "slot_time_actual",
  191. "leg_type",
  192. "status",
  193. "employer_cockpit",
  194. "employer_cabin",
  195. "cycles",
  196. "delay_code_01",
  197. "delay_code_02",
  198. "delay_code_03",
  199. "delay_code_04",
  200. "delay_time_01",
  201. "delay_time_02",
  202. "delay_time_03",
  203. "delay_time_04",
  204. "subdelay_code_01",
  205. "subdelay_code_02",
  206. "subdelay_code_03",
  207. "subdelay_code_04",
  208. "pax_booked_c",
  209. "pax_booked_y",
  210. "pax_booked_trs_c",
  211. "pax_booked_trs_y",
  212. "pad_booked_c",
  213. "pad_booked_y",
  214. "offblock_dt_a",
  215. "airborne_dt_a",
  216. "landing_dt_a",
  217. "onblock_dt_a",
  218. "offblock_dt_f",
  219. "airborne_dt_f",
  220. "landing_dt_f",
  221. "onblock_dt_f",
  222. "offblock_dt_m",
  223. "airborne_dt_m",
  224. "landing_dt_m",
  225. "onblock_dt_m",
  226. "eet",
  227. ],
  228. "exportPGRGPN.txt": [
  229. "date",
  230. "tlc",
  231. "actype",
  232. "al",
  233. "fnum",
  234. "ddep",
  235. "hdep",
  236. "ddes",
  237. "hdes",
  238. "dep",
  239. "des",
  240. "label",
  241. "type",
  242. ],
  243. "ExportPGRGPNmois.txt": [
  244. "date",
  245. "tlc",
  246. "actype",
  247. "al",
  248. "fnum",
  249. "ddep",
  250. "hdep",
  251. "ddes",
  252. "hdes",
  253. "dep",
  254. "des",
  255. "label",
  256. "type",
  257. ],
  258. "exportlicence.txt": [
  259. "tlc",
  260. "fname",
  261. "mname",
  262. "lname",
  263. "expire",
  264. "ac",
  265. "college",
  266. "base",
  267. ],
  268. };
  269. final Map<String, String> scopes = {
  270. "secondprgtype.txt": "day_of_origin",
  271. "exportPGRGPN.txt": "date",
  272. "ExportPGRGPNmois.txt": "date",
  273. "exportlicence.txt": "tlc",
  274. };
  275. final Map<String, List<String>> idToRemove = {
  276. "secondprgtype.txt": ["day_of_origin"],
  277. "exportPGRGPN.txt": ["date", "tlc"],
  278. "ExportPGRGPNmois.txt": ["date", "tlc"],
  279. "exportlicence.txt": ["tlc"],
  280. };
  281. final Map<String, Map<String, dynamic>> ids = {
  282. "secondprgtype.txt": {
  283. "table": "aclegs_log",
  284. "headers": [
  285. "day_of_origin",
  286. "dep_sched_dt",
  287. "fn_carrier",
  288. "fn_number",
  289. "dep_ap_sched",
  290. "arr_ap_sched",
  291. // "dep_ap_actual",
  292. // "arr_ap_actual"
  293. ]
  294. },
  295. "exportPGRGPN.txt": {
  296. "table": "pnlegs_log",
  297. "headers": ["tlc", "date", "dep", "des", "al", "fnum,", "label"]
  298. },
  299. "ExportPGRGPNmois.txt": {
  300. "table": "pnlegs_log",
  301. "headers": ["tlc", "date", "dep", "des", "al", "fnum,", "label"]
  302. },
  303. "exportlicence.txt": {
  304. "table": "qualifs_log",
  305. "headers": ["tlc", "college", "ac", "base"]
  306. },
  307. };
  308. Future<List<Map<String, dynamic>>> parseCsv() async {
  309. final headers = _headers[filename] ?? [];
  310. if (headers.isEmpty) {
  311. throw Exception('No headers found for file: $filename');
  312. }
  313. // Initialize an empty list to hold the parsed data
  314. List<Map<String, dynamic>> data = [];
  315. // Read the CSV file
  316. final file = File(filepath);
  317. final lines = await file.readAsLines();
  318. // Iterate over each line in the CSV file
  319. for (int i = 0; i < lines.length; i++) {
  320. // Split the line into individual values
  321. final values = lines[i].split(',');
  322. if (values.length != headers.length) {
  323. // print('Skipping line $i: Incorrect number of values: line: $i');
  324. continue;
  325. }
  326. // Create a map for the current row
  327. Map<String, dynamic> row = {};
  328. // Assign each value to the corresponding header
  329. for (int j = 0; j < headers.length; j++) {
  330. row[headers[j]] = values[j].removeQuotes.trim().nullIfEmpty;
  331. }
  332. // Add the row map to the data list
  333. data.add(row);
  334. }
  335. // Return the parsed data
  336. return data;
  337. }
  338. List<String> get filesTomonitor => _headers.keys.toList();
  339. Future<void> go() async {
  340. if (!filesTomonitor.contains(filename)) return;
  341. final mapsToInsert = await parseCsv();
  342. final scopeName = scopes[filename] ?? "";
  343. final scopeInNew = mapsToInsert
  344. .fold(<String>{}, (t, e) => t..add(e[scopeName] ?? "")).toList();
  345. List<Map<String, dynamic>> oldIds = [];
  346. List<Map<String, dynamic>> oldComparable = [];
  347. //load old data
  348. for (var e in splitList(scopeInNew, headerToNb(scopeInNew))) {
  349. final res = await supabase
  350. .from(tables[filename]!)
  351. .select()
  352. .inFilter(scopeName, e)
  353. .limit(100000);
  354. oldIds.addAll(res.map((e) => filterMapByKeys(e, ["id"])).toList());
  355. oldComparable.addAll(res
  356. .map((e) => filterMapByKeys(e, _headers[filename] ?? []))
  357. .toList());
  358. }
  359. List<int> indexToRemove = [];
  360. List<int> indexToMaintain = [];
  361. final keys2check = idToRemove[filename] ?? [];
  362. for (int i = 0; i < oldComparable.length; i++) {
  363. final item = oldComparable[i];
  364. final index = findIndex(mapsToInsert, item);
  365. if (index > -1) {
  366. indexToMaintain.add(i);
  367. mapsToInsert.removeAt(index);
  368. } else {
  369. final mawjoudin = oldComparable.fold(<Map<String, dynamic>>[], (t, e) {
  370. return t..add(filterMapByKeys(e, keys2check));
  371. });
  372. final mawjood = mawjoudin
  373. .firstWhere((e) => mapEquals(filterMapByKeys(item, keys2check), e),
  374. orElse: () => {})
  375. .keys
  376. .isNotEmpty;
  377. if (mawjood) {
  378. indexToRemove.add(i);
  379. } else {
  380. print("saved item: $item");
  381. }
  382. }
  383. }
  384. // removing index to remove with id
  385. for (var e in splitList(indexToRemove.map((e) => oldIds[e]['id']).toList(),
  386. headerToNb(indexToRemove.map((e) => oldIds[e]['id']).toList()))) {
  387. await supabase
  388. .from(tables[filename]!) // Replace with your actual table name
  389. .delete()
  390. .inFilter('id', e);
  391. }
  392. // insering new data
  393. await supabase
  394. .from(tables[filename]!) // Replace with your actual table name
  395. .insert(mapsToInsert);
  396. print(
  397. " insert:${mapsToInsert.length} remove:${indexToRemove.length} maintain:${indexToMaintain.length}");
  398. }
  399. bool mapEquals(Map<String, dynamic> map1, Map<String, dynamic> map2) {
  400. //if (map1.length != map2.length) return false;
  401. for (var key in map1.keys) {
  402. if (map1[key] != map2[key]) return false;
  403. }
  404. return true;
  405. }
  406. int findIndex(List<dynamic> list, dynamic element) {
  407. for (int i = 0; i < list.length; i++) {
  408. if (mapEquals(list[i], element)) {
  409. return i;
  410. }
  411. }
  412. return -1; // Return -1 if the element is not found
  413. }
  414. Map<String, dynamic> filterMapByKeys(
  415. Map<String, dynamic> originalMap, List<String> keysToInclude) {
  416. // Create a new map to hold the filtered results
  417. Map<String, dynamic> filteredMap = {};
  418. // Iterate through the list of keys to include
  419. for (String key in keysToInclude) {
  420. // Check if the key exists in the original map
  421. if (originalMap.containsKey(key)) {
  422. filteredMap[key] = originalMap[key]; // Add to the new map
  423. }
  424. }
  425. return filteredMap;
  426. }
  427. List<List<T>> splitList<T>(List<T> originalList, int maxSize) {
  428. List<List<T>> sublists = [];
  429. for (int i = 0; i < originalList.length; i += maxSize) {
  430. // Create a sublist for the current chunk
  431. List<T> sublist = originalList.sublist(
  432. i,
  433. (i + maxSize > originalList.length) ? originalList.length : i + maxSize,
  434. );
  435. sublists.add(sublist);
  436. }
  437. return sublists;
  438. }
  439. int headerToNb(List list) {
  440. //header max of 16k
  441. final maxheader = 4 * 1024 * 8;
  442. if (list.isEmpty) {
  443. return list.length;
  444. }
  445. final length1 = (list.toString().length / list.length).ceil() * 8;
  446. final lengthurl = 200 * 8;
  447. final res = ((maxheader - lengthurl) / length1).floor();
  448. //print("header2nb: $res");
  449. return res;
  450. }
  451. }
  452. extension NullIfEmpty on String {
  453. String? get nullIfEmpty => isEmpty ? null : this;
  454. }
  455. extension RemoveQuotes on String {
  456. String get removeQuotes {
  457. if (isEmpty) return this; // Return if the string is empty
  458. // Check if the first character is a quote
  459. bool startsWithQuote = startsWith('"') || startsWith("'");
  460. // Check if the last character is a quote
  461. bool endsWithQuote = endsWith('"') || endsWith("'");
  462. // Remove the first and last characters if they are quotes
  463. String result = this;
  464. if (startsWithQuote) result = result.substring(1);
  465. if (endsWithQuote) result = result.substring(0, result.length - 1);
  466. return result;
  467. }
  468. }