Add asset upload models and apis
This commit is contained in:
parent
69d69d0aa4
commit
b6ade63caf
7 changed files with 536 additions and 0 deletions
137
lib/apis/record_api.dart
Normal file
137
lib/apis/record_api.dart
Normal file
|
@ -0,0 +1,137 @@
|
|||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||
import 'package:contacts_plus_plus/models/message.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
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';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class AssetApi {
|
||||
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 {
|
||||
final response = await client.post(
|
||||
"/users/${record.ownerId}/records/${record.id}/preprocess", body: jsonEncode(record.toMap()));
|
||||
ApiClient.checkResponse(response);
|
||||
final body = jsonDecode(response.body);
|
||||
return PreprocessStatus.fromMap(body);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
||||
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks?bytes=${asset.bytes}");
|
||||
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);
|
||||
}
|
||||
|
||||
static Future<String> uploadAsset(ApiClient client, {required NeosDBAsset asset, required Uint8List data}) async {
|
||||
final request = http.MultipartRequest(
|
||||
"POST",
|
||||
ApiClient.buildFullUri("/users/${client.userId}/assets/${asset.hash}"),
|
||||
)
|
||||
..files.add(http.MultipartFile.fromBytes("file", data));
|
||||
final response = await request.send();
|
||||
final body = jsonDecode(await response.stream.bytesToString());
|
||||
return body;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static Future<Record> uploadFile(ApiClient client, {required File file, required String machineId}) async {
|
||||
final data = await file.readAsBytes();
|
||||
final asset = NeosDBAsset.fromData(data);
|
||||
final assetUri = "neosdb://$machineId/${asset.hash}${extension(file.path)}";
|
||||
final combinedRecordId = RecordId(id: Record.generateId(), ownerId: client.userId, isValid: true);
|
||||
final record = Record(
|
||||
id: 0,
|
||||
recordId: combinedRecordId.id.toString(),
|
||||
combinedRecordId: combinedRecordId,
|
||||
assetUri: assetUri,
|
||||
name: basenameWithoutExtension(file.path),
|
||||
tags: [
|
||||
"message_item",
|
||||
"message_id:${Message.generateId()}"
|
||||
],
|
||||
recordType: RecordType.texture,
|
||||
thumbnailUri: assetUri,
|
||||
isPublic: false,
|
||||
isForPatreons: false,
|
||||
isListed: false,
|
||||
neosDBManifest: [
|
||||
asset,
|
||||
],
|
||||
globalVersion: 0,
|
||||
localVersion: 1,
|
||||
lastModifyingUserId: client.userId,
|
||||
lastModifyingMachineId: machineId,
|
||||
lastModificationTime: DateTime.now().toUtc(),
|
||||
creationTime: DateTime.now().toUtc(),
|
||||
ownerId: client.userId,
|
||||
isSynced: false,
|
||||
fetchedOn: DateTimeX.one,
|
||||
path: '',
|
||||
description: '',
|
||||
manifest: [
|
||||
assetUri
|
||||
],
|
||||
url: "neosrec://${client.userId}/${combinedRecordId.id}",
|
||||
isValidOwnerId: true,
|
||||
isValidRecordId: true,
|
||||
visits: 0,
|
||||
rating: 0,
|
||||
randomOrder: 0,
|
||||
);
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
final uploadData = await beginUploadAsset(client, asset: asset);
|
||||
if (uploadData.uploadState == UploadState.failed) {
|
||||
throw "Asset upload failed: ${uploadData.uploadState.name}";
|
||||
}
|
||||
|
||||
await uploadAsset(client, asset: asset, data: data);
|
||||
await finishUpload(client, asset: asset);
|
||||
return record;
|
||||
}
|
||||
}
|
|
@ -85,4 +85,9 @@ extension Format on Duration {
|
|||
return "$hh:$mm:$ss";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeX on DateTime {
|
||||
static DateTime epoch = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
static DateTime one = DateTime(1);
|
||||
}
|
34
lib/models/records/asset_diff.dart
Normal file
34
lib/models/records/asset_diff.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
|
||||
class AssetDiff {
|
||||
final String hash;
|
||||
final int bytes;
|
||||
final Diff state;
|
||||
final bool isUploaded;
|
||||
|
||||
const AssetDiff({required this.hash, required this.bytes, required this.state, required this.isUploaded});
|
||||
|
||||
factory AssetDiff.fromMap(Map map) {
|
||||
return AssetDiff(
|
||||
hash: map["hash"],
|
||||
bytes: map["bytes"],
|
||||
state: Diff.fromInt(map["state"]),
|
||||
isUploaded: map["isUploaded"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum Diff {
|
||||
added,
|
||||
unchanged,
|
||||
removed;
|
||||
|
||||
factory Diff.fromInt(int? idx) {
|
||||
return Diff.values[idx ?? 1];
|
||||
}
|
||||
|
||||
factory Diff.fromString(String? text) {
|
||||
return Diff.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
|
||||
orElse: () => Diff.unchanged,
|
||||
);
|
||||
}
|
||||
}
|
46
lib/models/records/asset_upload_data.dart
Normal file
46
lib/models/records/asset_upload_data.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
|
||||
enum UploadState {
|
||||
uploadingChunks,
|
||||
finalizing,
|
||||
uploaded,
|
||||
failed,
|
||||
unknown;
|
||||
|
||||
factory UploadState.fromString(String? text) {
|
||||
return UploadState.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
|
||||
orElse: () => UploadState.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AssetUploadData {
|
||||
final String signature;
|
||||
final String variant;
|
||||
final String ownerId;
|
||||
final int totalBytes;
|
||||
final int chunkSize;
|
||||
final int totalChunks;
|
||||
final UploadState uploadState;
|
||||
|
||||
const AssetUploadData({
|
||||
required this.signature,
|
||||
required this.variant,
|
||||
required this.ownerId,
|
||||
required this.totalBytes,
|
||||
required this.chunkSize,
|
||||
required this.totalChunks,
|
||||
required this.uploadState,
|
||||
});
|
||||
|
||||
factory AssetUploadData.fromMap(Map map) {
|
||||
return AssetUploadData(
|
||||
signature: map["signature"],
|
||||
variant: map["variant"] ?? "",
|
||||
ownerId: map["ownerId"] ?? "",
|
||||
totalBytes: map["totalBytes"] ?? -1,
|
||||
chunkSize: map["chunkSize"] ?? -1,
|
||||
totalChunks: map["totalChunks"] ?? -1,
|
||||
uploadState: UploadState.fromString(map["uploadStat"]),
|
||||
);
|
||||
}
|
||||
}
|
26
lib/models/records/neos_db_asset.dart
Normal file
26
lib/models/records/neos_db_asset.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
class NeosDBAsset {
|
||||
final String hash;
|
||||
final int bytes;
|
||||
|
||||
const NeosDBAsset({required this.hash, required this.bytes});
|
||||
|
||||
factory NeosDBAsset.fromMap(Map map) {
|
||||
return NeosDBAsset(hash: map["hash"] ?? "", bytes: map["bytes"] ?? -1);
|
||||
}
|
||||
|
||||
factory NeosDBAsset.fromData(Uint8List data) {
|
||||
final digest = sha256.convert(data);
|
||||
return NeosDBAsset(hash: digest.toString().replaceAll("-", "").toLowerCase(), bytes: data.length);
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
"hash": hash,
|
||||
"bytes": bytes,
|
||||
};
|
||||
}
|
||||
}
|
41
lib/models/records/preprocess_status.dart
Normal file
41
lib/models/records/preprocess_status.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
import 'package:contacts_plus_plus/models/records/asset_diff.dart';
|
||||
|
||||
enum RecordPreprocessState
|
||||
{
|
||||
preprocessing,
|
||||
success,
|
||||
failed;
|
||||
|
||||
factory RecordPreprocessState.fromString(String? text) {
|
||||
return RecordPreprocessState.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
|
||||
orElse: () => RecordPreprocessState.failed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PreprocessStatus {
|
||||
final String id;
|
||||
final String ownerId;
|
||||
final String recordId;
|
||||
final RecordPreprocessState state;
|
||||
final num progress;
|
||||
final String failReason;
|
||||
final List<AssetDiff> resultDiffs;
|
||||
|
||||
const PreprocessStatus({required this.id, required this.ownerId, required this.recordId, required this.state,
|
||||
required this.progress, required this.failReason, required this.resultDiffs,
|
||||
});
|
||||
|
||||
factory PreprocessStatus.fromMap(Map map) {
|
||||
return PreprocessStatus(
|
||||
id: map["id"],
|
||||
ownerId: map["ownerId"],
|
||||
recordId: map["recordId"],
|
||||
state: RecordPreprocessState.fromString(map["state"]),
|
||||
progress: map["progress"],
|
||||
failReason: map["failReason"] ?? "",
|
||||
resultDiffs: (map["resultDiffs"] as List? ?? []).map((e) => AssetDiff.fromMap(e)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
247
lib/models/records/record.dart
Normal file
247
lib/models/records/record.dart
Normal file
|
@ -0,0 +1,247 @@
|
|||
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum RecordType {
|
||||
unknown,
|
||||
link,
|
||||
object,
|
||||
directory,
|
||||
texture,
|
||||
audio;
|
||||
|
||||
factory RecordType.fromName(String? name) {
|
||||
return RecordType.values.firstWhere((element) => element.name.toLowerCase() == name?.toLowerCase().trim(), orElse: () => RecordType.unknown);
|
||||
}
|
||||
}
|
||||
|
||||
class RecordId {
|
||||
final String? id;
|
||||
final String? ownerId;
|
||||
final bool isValid;
|
||||
|
||||
const RecordId({required this.id, required this.ownerId, required this.isValid});
|
||||
|
||||
factory RecordId.fromMap(Map? map) {
|
||||
return RecordId(id: map?["id"], ownerId: map?["ownerId"], isValid: map?["isValid"] ?? false);
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
"id": id,
|
||||
"ownerId": ownerId,
|
||||
"isValid": isValid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Record {
|
||||
final int id;
|
||||
final RecordId combinedRecordId;
|
||||
final String recordId;
|
||||
final String ownerId;
|
||||
final String assetUri;
|
||||
final int globalVersion;
|
||||
final int localVersion;
|
||||
final String lastModifyingUserId;
|
||||
final String lastModifyingMachineId;
|
||||
final bool isSynced;
|
||||
final DateTime fetchedOn;
|
||||
final String name;
|
||||
final FormatNode formattedName;
|
||||
final String description;
|
||||
final RecordType recordType;
|
||||
final List<String> tags;
|
||||
final String path;
|
||||
final String thumbnailUri;
|
||||
final bool isPublic;
|
||||
final bool isForPatreons;
|
||||
final bool isListed;
|
||||
final DateTime lastModificationTime;
|
||||
final DateTime creationTime;
|
||||
final int visits;
|
||||
final int rating;
|
||||
final int randomOrder;
|
||||
final List<String> manifest;
|
||||
final List<NeosDBAsset> neosDBManifest;
|
||||
final String url;
|
||||
final bool isValidOwnerId;
|
||||
final bool isValidRecordId;
|
||||
|
||||
Record({
|
||||
required this.id,
|
||||
required this.combinedRecordId,
|
||||
required this.recordId,
|
||||
required this.isSynced,
|
||||
required this.fetchedOn,
|
||||
required this.path,
|
||||
required this.ownerId,
|
||||
required this.assetUri,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.tags,
|
||||
required this.recordType,
|
||||
required this.thumbnailUri,
|
||||
required this.isPublic,
|
||||
required this.isListed,
|
||||
required this.isForPatreons,
|
||||
required this.lastModificationTime,
|
||||
required this.neosDBManifest,
|
||||
required this.lastModifyingUserId,
|
||||
required this.lastModifyingMachineId,
|
||||
required this.creationTime,
|
||||
required this.manifest,
|
||||
required this.url,
|
||||
required this.isValidOwnerId,
|
||||
required this.isValidRecordId,
|
||||
required this.globalVersion,
|
||||
required this.localVersion,
|
||||
required this.visits,
|
||||
required this.rating,
|
||||
required this.randomOrder,
|
||||
}) : formattedName = FormatNode.fromText(name);
|
||||
|
||||
factory Record.fromMap(Map map) {
|
||||
return Record(
|
||||
id: map["id"] ?? 0,
|
||||
combinedRecordId: RecordId.fromMap(map["combinedRecordId"]),
|
||||
recordId: map["recordId"],
|
||||
ownerId: map["ownerId"] ?? "",
|
||||
assetUri: map["assetUri"] ?? "",
|
||||
globalVersion: map["globalVersion"] ?? 0,
|
||||
localVersion: map["localVersion"] ?? 0,
|
||||
name: map["name"] ?? "",
|
||||
description: map["description"] ?? "",
|
||||
tags: (map["tags"] as List? ?? []).map((e) => e.toString()).toList(),
|
||||
recordType: RecordType.fromName(map["recordType"]),
|
||||
thumbnailUri: map["thumbnailUri"] ?? "",
|
||||
isPublic: map["isPublic"] ?? false,
|
||||
isForPatreons: map["isForPatreons"] ?? false,
|
||||
isListed: map["isListed"] ?? false,
|
||||
lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
|
||||
neosDBManifest: (map["neosDBManifest"] as List? ?? []).map((e) => NeosDBAsset.fromMap(e)).toList(),
|
||||
lastModifyingUserId: map["lastModifyingUserId"] ?? "",
|
||||
lastModifyingMachineId: map["lastModifyingMachineId"] ?? "",
|
||||
creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
|
||||
isSynced: map["isSynced"] ?? false,
|
||||
fetchedOn: DateTime.tryParse(map["fetchedOn"]) ?? DateTimeX.epoch,
|
||||
path: map["path"] ?? "",
|
||||
manifest: (map["neosDBManifest"] as List? ?? []).map((e) => e.toString()).toList(),
|
||||
url: map["url"] ?? "",
|
||||
isValidOwnerId: map["isValidOwnerId"] ?? "",
|
||||
isValidRecordId: map["isValidRecordId"] ?? "",
|
||||
visits: map["visits"] ?? 0,
|
||||
rating: map["rating"] ?? 0,
|
||||
randomOrder: map["randomOrder"] ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
Record copyWith({
|
||||
int? id,
|
||||
String? ownerId,
|
||||
String? recordId,
|
||||
String? assetUri,
|
||||
int? globalVersion,
|
||||
int? localVersion,
|
||||
String? name,
|
||||
TextSpan? formattedName,
|
||||
String? description,
|
||||
List<String>? tags,
|
||||
RecordType? recordType,
|
||||
String? thumbnailUri,
|
||||
bool? isPublic,
|
||||
bool? isForPatreons,
|
||||
bool? isListed,
|
||||
bool? isDeleted,
|
||||
DateTime? lastModificationTime,
|
||||
List<NeosDBAsset>? neosDBManifest,
|
||||
String? lastModifyingUserId,
|
||||
String? lastModifyingMachineId,
|
||||
DateTime? creationTime,
|
||||
RecordId? combinedRecordId,
|
||||
bool? isSynced,
|
||||
DateTime? fetchedOn,
|
||||
String? path,
|
||||
List<String>? manifest,
|
||||
String? url,
|
||||
bool? isValidOwnerId,
|
||||
bool? isValidRecordId,
|
||||
int? visits,
|
||||
int? rating,
|
||||
int? randomOrder,
|
||||
}) {
|
||||
return Record(
|
||||
id: id ?? this.id,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
recordId: recordId ?? this.recordId,
|
||||
assetUri: assetUri ?? this.assetUri,
|
||||
globalVersion: globalVersion ?? this.globalVersion,
|
||||
localVersion: localVersion ?? this.localVersion,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
tags: tags ?? this.tags,
|
||||
recordType: recordType ?? this.recordType,
|
||||
thumbnailUri: thumbnailUri ?? this.thumbnailUri,
|
||||
isPublic: isPublic ?? this.isPublic,
|
||||
isForPatreons: isForPatreons ?? this.isForPatreons,
|
||||
isListed: isListed ?? this.isListed,
|
||||
lastModificationTime: lastModificationTime ?? this.lastModificationTime,
|
||||
neosDBManifest: neosDBManifest ?? this.neosDBManifest,
|
||||
lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId,
|
||||
lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId,
|
||||
creationTime: creationTime ?? this.creationTime,
|
||||
combinedRecordId: combinedRecordId ?? this.combinedRecordId,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
fetchedOn: fetchedOn ?? this.fetchedOn,
|
||||
path: path ?? this.path,
|
||||
manifest: manifest ?? this.manifest,
|
||||
url: url ?? this.url,
|
||||
isValidOwnerId: isValidOwnerId ?? this.isValidOwnerId,
|
||||
isValidRecordId: isValidRecordId ?? this.isValidRecordId,
|
||||
visits: visits ?? this.visits,
|
||||
rating: rating ?? this.rating,
|
||||
randomOrder: randomOrder ?? this.randomOrder,
|
||||
);
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
"id": id,
|
||||
"ownerId": ownerId,
|
||||
"recordId": recordId,
|
||||
"assetUri": assetUri,
|
||||
"globalVersion": globalVersion,
|
||||
"localVersion": localVersion,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"tags": tags,
|
||||
"recordType": recordType.name,
|
||||
"thumbnailUri": thumbnailUri,
|
||||
"isPublic": isPublic,
|
||||
"isForPatreons": isForPatreons,
|
||||
"isListed": isListed,
|
||||
"lastModificationTime": lastModificationTime.toUtc().toIso8601String(),
|
||||
"neosDBManifest": neosDBManifest.map((e) => e.toMap()).toList(),
|
||||
"lastModifyingUserId": lastModifyingUserId,
|
||||
"lastModifyingMachineId": lastModifyingMachineId,
|
||||
"creationTime": creationTime.toUtc().toIso8601String(),
|
||||
"combinedRecordId": combinedRecordId.toMap(),
|
||||
"isSynced": isSynced,
|
||||
"fetchedOn": fetchedOn.toUtc().toIso8601String(),
|
||||
"path": path,
|
||||
"manifest": manifest,
|
||||
"url": url,
|
||||
"isValidOwnerId": isValidOwnerId,
|
||||
"isValidRecordId": isValidRecordId,
|
||||
"visits": visits,
|
||||
"rating": rating,
|
||||
"randomOrder": randomOrder,
|
||||
};
|
||||
}
|
||||
|
||||
static String generateId() {
|
||||
return "R-${const Uuid().v4()}";
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue