import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:OpenContacts/models/records/asset_digest.dart'; import 'package:OpenContacts/models/records/json_template.dart'; import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import 'package:OpenContacts/clients/api_client.dart'; import 'package:OpenContacts/models/records/asset_upload_data.dart'; import 'package:OpenContacts/models/records/resonite_db_asset.dart'; import 'package:OpenContacts/models/records/preprocess_status.dart'; import 'package:OpenContacts/models/records/record.dart'; import 'package:http_parser/http_parser.dart'; import 'package:path/path.dart'; class RecordApi { static Future getUserRecord(ApiClient client, {required String recordId, String? user}) async { final response = await client.get("/users/${user ?? client.userId}/records/$recordId"); client.checkResponse(response); final body = jsonDecode(response.body) as Map; return Record.fromMap(body); } static Future getGroupRecordByPath(ApiClient client, {required String path, required String groupId}) async { final response = await client.get("/groups/$groupId/records/$path"); client.checkResponse(response); final body = jsonDecode(response.body) as Map; return Record.fromMap(body); } static Future> getUserRecordsAt(ApiClient client, {required String path, String? user}) async { final response = await client.get(Uri.encodeFull("/users/${user ?? client.userId}/records?path=$path")); client.checkResponse(response); final body = jsonDecode(response.body) as List; return body.map((e) => Record.fromMap(e)).toList(); } static Future> getGroupRecordsAt(ApiClient client, {required String path, required String groupId}) async { final response = await client.get("/groups/$groupId/records?path=$path"); client.checkResponse(response); final body = jsonDecode(response.body) as List; return body.map((e) => Record.fromMap(e)).toList(); } static Future deleteRecord(ApiClient client, {required String recordId}) async { final response = await client.delete("/users/${client.userId}/records/$recordId"); client.checkResponse(response); } static Future preprocessRecord(ApiClient client, {required Record record}) async { final body = jsonEncode(record.toMap()); final response = await client.post( "/users/${record.ownerId}/records/${record.id}/preprocess", body: body); client.checkResponse(response); final resultBody = jsonDecode(response.body); return PreprocessStatus.fromMap(resultBody); } static Future getPreprocessStatus(ApiClient client, {required PreprocessStatus preprocessStatus}) async { final response = await client.get( "/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}" ); client.checkResponse(response); final body = jsonDecode(response.body); return PreprocessStatus.fromMap(body); } static Future tryPreprocessRecord(ApiClient client, {required Record record}) async { var status = await preprocessRecord(client, record: record); while (status.state == RecordPreprocessState.preprocessing) { await Future.delayed(const Duration(seconds: 1)); status = await getPreprocessStatus(client, preprocessStatus: status); } if (status.state != RecordPreprocessState.success) { throw "Record Preprocessing failed: ${status.failReason}"; } return status; } static Future beginUploadAsset(ApiClient client, {required ResoniteDBAsset asset}) async { final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks"); client.checkResponse(response); final body = jsonDecode(response.body); final res = AssetUploadData.fromMap(body); if (res.uploadState == UploadState.failed) throw body; return res; } static Future upsertRecord(ApiClient client, {required Record record}) async { final body = jsonEncode(record.toMap()); final response = await client.put("/users/${client.userId}/records/${record.id}", body: body); client.checkResponse(response); } static Future uploadAsset(ApiClient client, {required AssetUploadData uploadData, required String filename, required ResoniteDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async { for (int i = 0; i < uploadData.totalChunks; i++) { progressCallback?.call(i/uploadData.totalChunks); final offset = i * uploadData.chunkSize; final end = (i + 1) * uploadData.chunkSize; final request = http.MultipartRequest( "POST", ApiClient.buildFullUri("/users/${client.userId}/assets/${asset.hash}/chunks/$i"), ) ..files.add(http.MultipartFile.fromBytes( "file", data.getRange(offset, min(end, data.length)).toList(), filename: filename, contentType: MediaType.parse("multipart/form-data"))) ..headers.addAll(client.authorizationHeader); final response = await request.send(); final bodyBytes = await response.stream.toBytes(); client.checkResponse(http.Response.bytes(bodyBytes, response.statusCode)); progressCallback?.call(1); } } static Future finishUpload(ApiClient client, {required ResoniteDBAsset asset}) async { final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks"); client.checkResponse(response); } static Future uploadAssets(ApiClient client, {required List assets, void Function(double progress)? progressCallback}) async { progressCallback?.call(0); for (int i = 0; i < assets.length; i++) { final totalProgress = i/assets.length; progressCallback?.call(totalProgress); final entry = assets[i]; final uploadData = await beginUploadAsset(client, asset: entry.asset); if (uploadData.uploadState == UploadState.failed) { throw "Asset upload failed: ${uploadData.uploadState.name}"; } await uploadAsset(client, uploadData: uploadData, asset: entry.asset, data: entry.data, filename: entry.name, progressCallback: (progress) => progressCallback?.call(totalProgress + progress * 1/assets.length), ); await finishUpload(client, asset: entry.asset); } progressCallback?.call(1); } static Future uploadImage(ApiClient client, {required File image, required String machineId, void Function(double progress)? progressCallback}) async { progressCallback?.call(0); final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path)); final imageData = await decodeImageFromList(imageDigest.data); final filename = basenameWithoutExtension(image.path); final objectJson = jsonEncode( JsonTemplate.image(imageUri: imageDigest.dbUri, filename: filename, width: imageData.width, height: imageData.height).data); final objectBytes = Uint8List.fromList(utf8.encode(objectJson)); final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(image.path)}.json"); final digests = [imageDigest, objectDigest]; final record = Record.fromRequiredData( recordType: RecordType.texture, userId: client.userId, machineId: machineId, assetUri: objectDigest.dbUri, filename: filename, thumbnailUri: imageDigest.dbUri, digests: digests, extraTags: ["image"], ); progressCallback?.call(.1); final status = await tryPreprocessRecord(client, record: record); final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded); progressCallback?.call(.2); await uploadAssets( client, assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(), progressCallback: (progress) => progressCallback?.call(.2 + progress * .6)); await upsertRecord(client, record: record); progressCallback?.call(1); return record; } static Future uploadVoiceClip(ApiClient client, {required File voiceClip, required String machineId, void Function(double progress)? progressCallback}) async { progressCallback?.call(0); final voiceDigest = await AssetDigest.fromData(await voiceClip.readAsBytes(), basename(voiceClip.path)); final filename = basenameWithoutExtension(voiceClip.path); final digests = [voiceDigest]; final record = Record.fromRequiredData( recordType: RecordType.audio, userId: client.userId, machineId: machineId, assetUri: voiceDigest.dbUri, filename: filename, thumbnailUri: "", digests: digests, extraTags: ["voice", "message"], ); progressCallback?.call(.1); final status = await tryPreprocessRecord(client, record: record); final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded); progressCallback?.call(.2); await uploadAssets( client, assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(), progressCallback: (progress) => progressCallback?.call(.2 + progress * .6)); await upsertRecord(client, record: record); progressCallback?.call(1); return record; } static Future uploadRawFile(ApiClient client, {required File file, required String machineId, void Function(double progress)? progressCallback}) async { progressCallback?.call(0); final fileDigest = await AssetDigest.fromData(await file.readAsBytes(), basename(file.path)); final objectJson = jsonEncode(JsonTemplate.rawFile(assetUri: fileDigest.dbUri, filename: fileDigest.name).data); final objectBytes = Uint8List.fromList(utf8.encode(objectJson)); final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(file.path)}.json"); final digests = [fileDigest, objectDigest]; final record = Record.fromRequiredData( recordType: RecordType.texture, userId: client.userId, machineId: machineId, assetUri: objectDigest.dbUri, filename: fileDigest.name, thumbnailUri: JsonTemplate.thumbUrl, digests: digests, extraTags: ["document"], ); progressCallback?.call(.1); final status = await tryPreprocessRecord(client, record: record); final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded); progressCallback?.call(.2); await uploadAssets( client, assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(), progressCallback: (progress) => progressCallback?.call(.2 + progress * .6)); await upsertRecord(client, record: record); progressCallback?.call(1); return record; } }