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 Function()? temporaryDirectory, this.onChanged}) : _temporaryDirectory = temporaryDirectory ?? getTemporaryDirectory; final ApiService _apiService; final FileVaultPlatform platform; final Future 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 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> folders() async => (await _api.listFileFolders() ?? []).map(_folder).toList(); Future createFolder(String name, String? parentId) async { await _api.createFileFolder(FileVaultCreateFolderDto(name: name, parentId: Optional.present(parentId))); onChanged?.call(); } Future rename(String id, String name) async { await _api.updateFile(id, FileVaultUpdateDto(name: Optional.present(name))); onChanged?.call(); } Future move(String id, String? parentId) async { await _api.updateFile(id, FileVaultUpdateDto(parentId: Optional.present(parentId))); onChanged?.call(); } Future trash(String id) async { await _api.trashFile(id); onChanged?.call(); } Future restore(String id) async { await _api.restoreFile(id); onChanged?.call(); } Future purge(String id) async { await _api.permanentlyDeleteFile(id); onChanged?.call(); } Future 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 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(); 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; } } }