OpenContacts/lib/apis/record_api.dart

220 lines
9.4 KiB
Dart
Raw Normal View History

2023-05-16 09:59:31 -04:00
import 'dart:convert';
import 'dart:io';
import 'dart:math';
2023-05-16 09:59:31 -04:00
import 'dart:typed_data';
import 'package:collection/collection.dart';
2023-05-17 10:32:23 -04:00
import 'package:contacts_plus_plus/models/records/asset_digest.dart';
2023-05-20 09:40:03 -04:00
import 'package:contacts_plus_plus/models/records/json_template.dart';
2023-05-16 09:59:31 -04:00
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
2023-05-16 09:59:31 -04:00
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/records/asset_upload_data.dart';
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
import 'package:contacts_plus_plus/models/records/preprocess_status.dart';
import 'package:contacts_plus_plus/models/records/record.dart';
2023-05-17 02:07:10 -04:00
import 'package:http_parser/http_parser.dart';
2023-05-16 09:59:31 -04:00
import 'package:path/path.dart';
2023-05-17 02:07:10 -04:00
class RecordApi {
2023-05-16 09:59:31 -04:00
static Future<List<Record>> getRecordsAt(ApiClient client, {required String path}) async {
final response = await client.get("/users/${client.userId}/records?path=$path");
ApiClient.checkResponse(response);
final body = jsonDecode(response.body) as List;
return body.map((e) => Record.fromMap(e)).toList();
}
static Future<PreprocessStatus> preprocessRecord(ApiClient client, {required Record record}) async {
2023-05-17 02:07:10 -04:00
final body = jsonEncode(record.toMap());
2023-05-16 09:59:31 -04:00
final response = await client.post(
2023-05-17 02:07:10 -04:00
"/users/${record.ownerId}/records/${record.id}/preprocess", body: body);
2023-05-16 09:59:31 -04:00
ApiClient.checkResponse(response);
2023-05-17 02:07:10 -04:00
final resultBody = jsonDecode(response.body);
return PreprocessStatus.fromMap(resultBody);
2023-05-16 09:59:31 -04:00
}
static Future<PreprocessStatus> getPreprocessStatus(ApiClient client,
{required PreprocessStatus preprocessStatus}) async {
final response = await client.get(
"/users/${preprocessStatus.ownerId}/records/${preprocessStatus.recordId}/preprocess/${preprocessStatus.id}"
);
ApiClient.checkResponse(response);
final body = jsonDecode(response.body);
return PreprocessStatus.fromMap(body);
}
2023-05-17 10:32:23 -04:00
static Future<PreprocessStatus> 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;
}
2023-05-16 09:59:31 -04:00
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
2023-05-17 02:07:10 -04:00
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
2023-05-16 09:59:31 -04:00
ApiClient.checkResponse(response);
final body = jsonDecode(response.body);
final res = AssetUploadData.fromMap(body);
if (res.uploadState == UploadState.failed) throw body;
return res;
}
static Future<void> 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);
ApiClient.checkResponse(response);
}
2023-05-17 10:32:23 -04:00
static Future<void> uploadAsset(ApiClient client,
2023-05-18 04:52:32 -04:00
{required AssetUploadData uploadData, required String filename, required NeosDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async {
for (int i = 0; i < uploadData.totalChunks; i++) {
2023-05-18 04:52:32 -04:00
progressCallback?.call(i/uploadData.totalChunks);
2023-05-17 10:32:23 -04:00
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"),
2023-05-17 10:32:23 -04:00
)
..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();
ApiClient.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
2023-05-18 04:52:32 -04:00
progressCallback?.call(1);
}
2023-05-16 09:59:31 -04:00
}
static Future<void> finishUpload(ApiClient client, {required NeosDBAsset asset}) async {
final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
ApiClient.checkResponse(response);
}
2023-05-18 04:52:32 -04:00
static Future<void> uploadAssets(ApiClient client, {required List<AssetDigest> 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];
2023-05-17 10:32:23 -04:00
final uploadData = await beginUploadAsset(client, asset: entry.asset);
if (uploadData.uploadState == UploadState.failed) {
throw "Asset upload failed: ${uploadData.uploadState.name}";
}
2023-05-18 04:52:32 -04:00
await uploadAsset(client,
uploadData: uploadData,
asset: entry.asset,
data: entry.data,
filename: entry.name,
progressCallback: (progress) => progressCallback?.call(totalProgress + progress * 1/assets.length),
);
2023-05-17 10:32:23 -04:00
await finishUpload(client, asset: entry.asset);
}
2023-05-18 04:52:32 -04:00
progressCallback?.call(1);
2023-05-17 10:32:23 -04:00
}
2023-05-16 09:59:31 -04:00
2023-05-18 04:52:32 -04:00
static Future<Record> uploadImage(ApiClient client, {required File image, required String machineId, void Function(double progress)? progressCallback}) async {
progressCallback?.call(0);
2023-05-17 10:32:23 -04:00
final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path));
final imageData = await decodeImageFromList(imageDigest.data);
final objectJson = jsonEncode(
2023-05-20 09:40:03 -04:00
JsonTemplate.image(imageUri: imageDigest.dbUri, width: imageData.width, height: imageData.height).data);
2023-05-17 10:32:23 -04:00
final objectBytes = Uint8List.fromList(utf8.encode(objectJson));
final objectDigest = await AssetDigest.fromData(objectBytes, "${basenameWithoutExtension(image.path)}.json");
final filename = basenameWithoutExtension(image.path);
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,
);
2023-05-18 04:52:32 -04:00
progressCallback?.call(.1);
2023-05-17 10:32:23 -04:00
final status = await tryPreprocessRecord(client, record: record);
final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded);
2023-05-18 04:52:32 -04:00
progressCallback?.call(.2);
2023-05-17 10:32:23 -04:00
await uploadAssets(
2023-05-18 04:52:32 -04:00
client,
assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(),
progressCallback: (progress) => progressCallback?.call(.2 + progress * .6));
progressCallback?.call(1);
2023-05-16 09:59:31 -04:00
return record;
}
static Future<Record> 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(
2023-05-20 15:09:22 -04:00
recordType: RecordType.audio,
userId: client.userId,
machineId: machineId,
assetUri: voiceDigest.dbUri,
filename: filename,
thumbnailUri: "",
digests: digests,
);
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));
progressCallback?.call(1);
return record;
}
2023-05-20 09:40:03 -04:00
static Future<Record> 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,
);
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));
progressCallback?.call(1);
return record;
}
2023-05-17 10:32:23 -04:00
}