Files
Immich/mobile/lib/data/server/file_vault_api.dart
T

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;
}
}
}