161 lines
5.7 KiB
Dart
161 lines
5.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:immich_mobile/domain/models/file_vault.model.dart';
|
|
import 'package:immich_mobile/platform/file_vault_api.g.dart';
|
|
import 'package:immich_mobile/services/api.service.dart';
|
|
import 'package:openapi/api.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
class FileVaultApiData {
|
|
FileVaultApiData(this._apiService, this.platform, {Future<Directory> Function()? temporaryDirectory, this.onChanged})
|
|
: _temporaryDirectory = temporaryDirectory ?? getTemporaryDirectory;
|
|
|
|
final ApiService _apiService;
|
|
final FileVaultPlatform platform;
|
|
final Future<Directory> Function() _temporaryDirectory;
|
|
final void Function()? onChanged;
|
|
FileVaultApi get _api => FileVaultApi(_apiService.apiClient);
|
|
|
|
VaultEntry _entry(FileVaultEntryDto dto) => VaultEntry(
|
|
id: dto.id,
|
|
name: dto.name,
|
|
parentId: dto.parentId,
|
|
isFolder: dto.isFolder,
|
|
size: dto.size,
|
|
checksum: dto.checksum,
|
|
);
|
|
VaultFolder _folder(FileVaultFolderDto dto) => VaultFolder(id: dto.id, name: dto.name, parentId: dto.parentId);
|
|
|
|
Future<VaultListing> list({String? parentId, bool trash = false, int offset = 0}) async {
|
|
final dto = await _api.listFiles(parentId: parentId, trash: trash ? 'true' : 'false', offset: offset);
|
|
if (dto == null) {
|
|
throw const FormatException('Empty file listing');
|
|
}
|
|
return VaultListing(
|
|
entries: dto.entries.map(_entry).toList(),
|
|
breadcrumbs: dto.breadcrumbs.map(_folder).toList(),
|
|
hasMore: dto.hasMore,
|
|
usedBytes: dto.usedBytes,
|
|
maxFileBytes: dto.maxFileBytes,
|
|
);
|
|
}
|
|
|
|
Future<List<VaultFolder>> folders() async => (await _api.listFileFolders() ?? []).map(_folder).toList();
|
|
Future<void> createFolder(String name, String? parentId) async {
|
|
await _api.createFileFolder(FileVaultCreateFolderDto(name: name, parentId: Optional.present(parentId)));
|
|
onChanged?.call();
|
|
}
|
|
|
|
Future<void> rename(String id, String name) async {
|
|
await _api.updateFile(id, FileVaultUpdateDto(name: Optional.present(name)));
|
|
onChanged?.call();
|
|
}
|
|
|
|
Future<void> move(String id, String? parentId) async {
|
|
await _api.updateFile(id, FileVaultUpdateDto(parentId: Optional.present(parentId)));
|
|
onChanged?.call();
|
|
}
|
|
|
|
Future<void> trash(String id) async {
|
|
await _api.trashFile(id);
|
|
onChanged?.call();
|
|
}
|
|
|
|
Future<void> restore(String id) async {
|
|
await _api.restoreFile(id);
|
|
onChanged?.call();
|
|
}
|
|
|
|
Future<void> purge(String id) async {
|
|
await _api.permanentlyDeleteFile(id);
|
|
onChanged?.call();
|
|
}
|
|
|
|
Future<void> upload(String path, String name, String? parentId, void Function(int, int) onProgress) async {
|
|
final file = File(path);
|
|
final length = await file.length();
|
|
var sent = 0;
|
|
final body = http.MultipartFile(
|
|
'file',
|
|
file.openRead().map((chunk) {
|
|
sent += chunk.length;
|
|
onProgress(sent, length);
|
|
return chunk;
|
|
}),
|
|
length,
|
|
filename: name,
|
|
);
|
|
final response = await _apiService.apiClient.invokeAPI(
|
|
'/file-vault/upload',
|
|
'POST',
|
|
[QueryParam('name', name), if (parentId != null) QueryParam('parentId', parentId)],
|
|
body,
|
|
{},
|
|
{},
|
|
'application/octet-stream',
|
|
);
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw ApiException(response.statusCode, utf8.decode(response.bodyBytes));
|
|
}
|
|
final dto = FileVaultEntryDto.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
|
if (dto == null) {
|
|
throw const FormatException('Missing upload response');
|
|
}
|
|
onChanged?.call();
|
|
final hash = await sha256.bind(file.openRead()).first;
|
|
if (dto.checksum != hash.toString()) {
|
|
throw const FormatException('File integrity verification failed');
|
|
}
|
|
}
|
|
|
|
Future<File> download(VaultEntry entry, void Function(int, int) onProgress) async {
|
|
if (!RegExp(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$').hasMatch(entry.id) ||
|
|
entry.size < 0) {
|
|
throw const FormatException('Invalid file metadata');
|
|
}
|
|
final temporary = await _temporaryDirectory();
|
|
final directory = Directory(p.join(temporary.path, 'file-vault-downloads', entry.id));
|
|
await directory.create(recursive: true);
|
|
final target = File(p.join(directory.path, p.basename(entry.name) + (entry.isFolder ? '.zip' : '')));
|
|
final partial = File('${target.path}.part');
|
|
final request = http.Request('GET', Uri.parse('${_apiService.apiClient.basePath}/file-vault/${entry.id}/download'));
|
|
request.headers.addAll(_apiService.apiClient.defaultHeaderMap);
|
|
final response = await _apiService.apiClient.client.send(request);
|
|
if (response.statusCode != 200) {
|
|
await response.stream.drain<void>();
|
|
throw ApiException(response.statusCode, 'Download failed');
|
|
}
|
|
var received = 0;
|
|
final output = partial.openWrite();
|
|
try {
|
|
await output.addStream(
|
|
response.stream.map((chunk) {
|
|
received += chunk.length;
|
|
if (!entry.isFolder && received > entry.size) {
|
|
throw const FormatException('Download exceeds the expected size');
|
|
}
|
|
onProgress(received, entry.size);
|
|
return chunk;
|
|
}),
|
|
);
|
|
await output.flush();
|
|
await output.close();
|
|
final hash = await sha256.bind(partial.openRead()).first;
|
|
if (entry.isFolder ? received == 0 : received != entry.size || hash.toString() != entry.checksum) {
|
|
throw const FormatException('File integrity verification failed');
|
|
}
|
|
return await partial.rename(target.path);
|
|
} catch (_) {
|
|
await output.close();
|
|
if (partial.existsSync()) {
|
|
await partial.delete();
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|