2024-11-09 17:04:39 +00:00
|
|
|
import 'dart:collection';
|
2024-11-11 13:48:50 +00:00
|
|
|
import 'dart:math' as math;
|
2024-11-09 17:04:39 +00:00
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
import 'package:dio/dio.dart';
|
2024-11-09 04:04:03 +00:00
|
|
|
import 'package:flutter/widgets.dart';
|
2024-11-09 17:04:39 +00:00
|
|
|
import 'package:cross_file/cross_file.dart';
|
2024-11-09 04:04:03 +00:00
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:surface/providers/sn_network.dart';
|
|
|
|
import 'package:surface/types/attachment.dart';
|
|
|
|
|
2024-11-09 17:04:39 +00:00
|
|
|
const kConcurrentUploadChunks = 5;
|
|
|
|
|
2024-11-09 04:04:03 +00:00
|
|
|
class SnAttachmentProvider {
|
2024-11-09 11:32:21 +00:00
|
|
|
late final SnNetworkProvider _sn;
|
2024-11-09 04:04:03 +00:00
|
|
|
final Map<String, SnAttachment> _cache = {};
|
|
|
|
|
|
|
|
SnAttachmentProvider(BuildContext context) {
|
2024-11-09 11:32:21 +00:00
|
|
|
_sn = context.read<SnNetworkProvider>();
|
2024-11-09 04:04:03 +00:00
|
|
|
}
|
|
|
|
|
2024-11-17 16:55:39 +00:00
|
|
|
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
|
|
|
for (final item in items) {
|
2024-12-28 09:19:20 +00:00
|
|
|
if (item.isAnalyzed || noCheck) {
|
2024-11-17 16:55:39 +00:00
|
|
|
_cache[item.rid] = item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-09 04:04:03 +00:00
|
|
|
Future<SnAttachment> getOne(String rid, {noCache = false}) async {
|
|
|
|
if (!noCache && _cache.containsKey(rid)) {
|
|
|
|
return _cache[rid]!;
|
|
|
|
}
|
|
|
|
|
2024-11-09 11:32:21 +00:00
|
|
|
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
2024-11-09 04:04:03 +00:00
|
|
|
final out = SnAttachment.fromJson(resp.data);
|
2024-12-28 09:19:20 +00:00
|
|
|
if (out.isAnalyzed) {
|
2024-11-17 16:55:39 +00:00
|
|
|
_cache[rid] = out;
|
|
|
|
}
|
2024-11-09 04:04:03 +00:00
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
2024-12-21 16:48:06 +00:00
|
|
|
Future<List<SnAttachment?>> getMultiple(List<String> rids, {noCache = false}) async {
|
2024-11-17 16:55:39 +00:00
|
|
|
final result = List<SnAttachment?>.filled(rids.length, null);
|
|
|
|
final Map<String, int> randomMapping = {};
|
|
|
|
for (int i = 0; i < rids.length; i++) {
|
|
|
|
final rid = rids[i];
|
|
|
|
if (noCache || !_cache.containsKey(rid)) {
|
|
|
|
randomMapping[rid] = i;
|
|
|
|
} else {
|
|
|
|
result[i] = _cache[rid]!;
|
|
|
|
}
|
2024-11-09 04:04:03 +00:00
|
|
|
}
|
2024-11-17 16:55:39 +00:00
|
|
|
final pendingFetch = randomMapping.keys;
|
2024-11-09 04:04:03 +00:00
|
|
|
|
2024-11-17 16:55:39 +00:00
|
|
|
if (pendingFetch.isNotEmpty) {
|
|
|
|
final resp = await _sn.client.get(
|
|
|
|
'/cgi/uc/attachments',
|
|
|
|
queryParameters: {
|
|
|
|
'take': pendingFetch.length,
|
|
|
|
'id': pendingFetch.join(','),
|
|
|
|
},
|
|
|
|
);
|
2024-12-28 09:19:20 +00:00
|
|
|
final List<SnAttachment?> out =
|
|
|
|
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
|
2024-11-09 04:04:03 +00:00
|
|
|
|
2024-11-17 16:55:39 +00:00
|
|
|
for (final item in out) {
|
2024-11-19 16:13:36 +00:00
|
|
|
if (item == null) continue;
|
2024-12-28 09:19:20 +00:00
|
|
|
if (item.isAnalyzed) {
|
2024-11-17 16:55:39 +00:00
|
|
|
_cache[item.rid] = item;
|
|
|
|
}
|
|
|
|
result[randomMapping[item.rid]!] = item;
|
|
|
|
}
|
2024-11-09 04:04:03 +00:00
|
|
|
}
|
2024-11-12 16:25:02 +00:00
|
|
|
|
2024-11-17 16:55:39 +00:00
|
|
|
return result;
|
2024-11-09 04:04:03 +00:00
|
|
|
}
|
2024-11-09 17:04:39 +00:00
|
|
|
|
2024-12-21 16:48:06 +00:00
|
|
|
static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'};
|
2024-11-09 17:04:39 +00:00
|
|
|
|
|
|
|
Future<SnAttachment> directUploadOne(
|
|
|
|
Uint8List data,
|
|
|
|
String filename,
|
|
|
|
String pool,
|
|
|
|
Map<String, dynamic>? metadata, {
|
|
|
|
String? mimetype,
|
|
|
|
Function(double progress)? onProgress,
|
2024-12-28 18:13:31 +00:00
|
|
|
bool analyzeNow = false,
|
2024-11-09 17:04:39 +00:00
|
|
|
}) async {
|
|
|
|
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
2024-12-21 16:48:06 +00:00
|
|
|
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
|
|
|
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
2024-11-09 17:04:39 +00:00
|
|
|
|
|
|
|
String? mimetypeOverride;
|
|
|
|
if (mimetype != null) {
|
|
|
|
mimetypeOverride = mimetype;
|
|
|
|
} else if (mimetypeOverrides.keys.contains(fileExt)) {
|
|
|
|
mimetypeOverride = mimetypeOverrides[fileExt];
|
|
|
|
}
|
|
|
|
|
|
|
|
final formData = FormData.fromMap({
|
|
|
|
'alt': fileAlt,
|
|
|
|
'file': filePayload,
|
|
|
|
'pool': pool,
|
|
|
|
'metadata': metadata,
|
|
|
|
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
|
|
|
});
|
|
|
|
final resp = await _sn.client.post(
|
|
|
|
'/cgi/uc/attachments',
|
|
|
|
data: formData,
|
2024-12-28 18:13:31 +00:00
|
|
|
queryParameters: {'analyzeNow': analyzeNow},
|
2024-11-09 17:04:39 +00:00
|
|
|
onSendProgress: (count, total) {
|
|
|
|
if (onProgress != null) {
|
|
|
|
onProgress(count / total);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
return SnAttachment.fromJson(resp.data);
|
|
|
|
}
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
Future<(SnAttachmentFragment, int)> chunkedUploadInitialize(
|
2024-11-09 17:04:39 +00:00
|
|
|
int size,
|
|
|
|
String filename,
|
|
|
|
String pool,
|
2024-11-21 16:28:29 +00:00
|
|
|
Map<String, dynamic>? metadata, {
|
|
|
|
String? mimetype,
|
|
|
|
}) async {
|
2024-12-21 16:48:06 +00:00
|
|
|
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
|
|
|
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
2024-11-09 17:04:39 +00:00
|
|
|
|
|
|
|
String? mimetypeOverride;
|
2024-11-21 16:28:29 +00:00
|
|
|
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
|
2024-11-09 17:04:39 +00:00
|
|
|
mimetypeOverride = mimetypeOverrides[fileExt];
|
2024-11-21 16:28:29 +00:00
|
|
|
} else {
|
|
|
|
mimetypeOverride = mimetype;
|
2024-11-09 17:04:39 +00:00
|
|
|
}
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
final resp = await _sn.client.post('/cgi/uc/fragments', data: {
|
2024-11-09 17:04:39 +00:00
|
|
|
'alt': fileAlt,
|
|
|
|
'name': filename,
|
|
|
|
'pool': pool,
|
|
|
|
'metadata': metadata,
|
|
|
|
'size': size,
|
|
|
|
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
|
|
|
});
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
|
2024-11-09 17:04:39 +00:00
|
|
|
}
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
Future<dynamic> _chunkedUploadOnePart(
|
2024-11-09 17:04:39 +00:00
|
|
|
Uint8List data,
|
|
|
|
String rid,
|
|
|
|
String cid, {
|
|
|
|
Function(double progress)? onProgress,
|
|
|
|
}) async {
|
|
|
|
final resp = await _sn.client.post(
|
2024-12-28 09:19:20 +00:00
|
|
|
'/cgi/uc/fragments/$rid/$cid',
|
2024-11-09 17:04:39 +00:00
|
|
|
data: data,
|
|
|
|
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
|
|
|
onSendProgress: (count, total) {
|
|
|
|
if (onProgress != null) {
|
|
|
|
onProgress(count / total);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
if (resp.data['attachment'] != null) {
|
|
|
|
return SnAttachment.fromJson(resp.data['attachment']);
|
|
|
|
} else {
|
|
|
|
return SnAttachmentFragment.fromJson(resp.data['fragment']);
|
|
|
|
}
|
2024-11-09 17:04:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<SnAttachment> chunkedUploadParts(
|
|
|
|
XFile file,
|
2024-12-28 09:19:20 +00:00
|
|
|
SnAttachmentFragment place,
|
2024-11-09 17:04:39 +00:00
|
|
|
int chunkSize, {
|
|
|
|
Function(double progress)? onProgress,
|
|
|
|
}) async {
|
2024-12-28 09:19:20 +00:00
|
|
|
final Map<String, dynamic> chunks = place.fileChunks;
|
2024-12-28 11:23:49 +00:00
|
|
|
var completedTasks = 0;
|
2024-11-09 17:04:39 +00:00
|
|
|
|
|
|
|
final queue = Queue<Future<void>>();
|
|
|
|
final activeTasks = <Future<void>>[];
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
late SnAttachment out;
|
|
|
|
|
2024-11-09 17:04:39 +00:00
|
|
|
for (final entry in chunks.entries) {
|
|
|
|
queue.add(() async {
|
|
|
|
final beginCursor = entry.value * chunkSize;
|
2024-11-11 13:48:50 +00:00
|
|
|
final endCursor = math.min<int>(
|
|
|
|
(entry.value + 1) * chunkSize,
|
|
|
|
await file.length(),
|
|
|
|
);
|
2024-12-21 16:48:06 +00:00
|
|
|
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
|
2024-11-09 17:04:39 +00:00
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
final result = await _chunkedUploadOnePart(
|
2024-11-09 17:04:39 +00:00
|
|
|
data,
|
|
|
|
place.rid,
|
|
|
|
entry.key,
|
2024-12-28 09:37:58 +00:00
|
|
|
onProgress: (progress) {
|
2024-12-28 11:23:49 +00:00
|
|
|
final overallProgress = (completedTasks + progress) / chunks.length;
|
2024-12-28 09:37:58 +00:00
|
|
|
onProgress?.call(overallProgress);
|
|
|
|
},
|
2024-11-09 17:04:39 +00:00
|
|
|
);
|
|
|
|
|
2024-12-28 11:23:49 +00:00
|
|
|
completedTasks++;
|
|
|
|
final overallProgress = completedTasks / chunks.length;
|
2024-12-21 16:48:06 +00:00
|
|
|
onProgress?.call(overallProgress);
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
if (result is SnAttachmentFragment) {
|
|
|
|
place = result;
|
|
|
|
} else {
|
|
|
|
out = result as SnAttachment;
|
|
|
|
}
|
2024-11-09 17:04:39 +00:00
|
|
|
}());
|
|
|
|
}
|
|
|
|
|
|
|
|
while (queue.isNotEmpty || activeTasks.isNotEmpty) {
|
|
|
|
while (activeTasks.length < kConcurrentUploadChunks && queue.isNotEmpty) {
|
|
|
|
final task = queue.removeFirst();
|
|
|
|
activeTasks.add(task);
|
|
|
|
|
|
|
|
task.then((_) => activeTasks.remove(task));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (activeTasks.isNotEmpty) {
|
|
|
|
await Future.any(activeTasks);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-28 09:19:20 +00:00
|
|
|
return out;
|
2024-11-09 17:04:39 +00:00
|
|
|
}
|
2024-12-25 16:02:25 +00:00
|
|
|
|
|
|
|
Future<SnAttachment> updateOne(
|
2024-12-28 10:16:59 +00:00
|
|
|
SnAttachment item, {
|
2024-12-26 15:01:00 +00:00
|
|
|
String? alt,
|
2024-12-28 09:37:58 +00:00
|
|
|
int? thumbnailId,
|
|
|
|
int? compressedId,
|
2024-12-26 15:01:00 +00:00
|
|
|
Map<String, dynamic>? metadata,
|
|
|
|
bool? isIndexable,
|
2024-12-25 16:02:25 +00:00
|
|
|
}) async {
|
2024-12-28 10:16:59 +00:00
|
|
|
final resp = await _sn.client.put('/cgi/uc/attachments/${item.id}', data: {
|
|
|
|
'alt': alt ?? item.alt,
|
|
|
|
'thumbnail': thumbnailId ?? item.thumbnailId,
|
|
|
|
'compressed': compressedId ?? item.compressedId,
|
|
|
|
'metadata': metadata ?? item.usermeta,
|
|
|
|
'is_indexable': isIndexable ?? item.isIndexable,
|
2024-12-25 16:02:25 +00:00
|
|
|
});
|
|
|
|
return SnAttachment.fromJson(resp.data);
|
|
|
|
}
|
2024-11-09 04:04:03 +00:00
|
|
|
}
|