import 'dart:async'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/get_it/resources_model.dart'; import 'package:selfprivacy/logic/models/backup.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart'; import 'package:selfprivacy/logic/models/initialize_repository_input.dart'; import 'package:selfprivacy/logic/models/json/server_job.dart'; import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/providers/backups_providers/backups_provider.dart'; import 'package:selfprivacy/logic/providers/backups_providers/backups_provider_factory.dart'; import 'package:selfprivacy/logic/providers/provider_settings.dart'; part 'backups_event.dart'; part 'backups_state.dart'; class BackupsBloc extends Bloc { BackupsBloc() : super(BackupsInitial()) { on( _loadState, transformer: droppable(), ); on( _resetState, transformer: droppable(), ); on( _updateState, transformer: droppable(), ); on( _initializeRepository, transformer: droppable(), ); on( _forceSnapshotListUpdate, transformer: droppable(), ); on( _createBackups, transformer: sequential(), ); on( _restoreBackup, transformer: sequential(), ); on( _setAutobackupPeriod, transformer: restartable(), ); on( _setAutobackupQuotas, transformer: restartable(), ); on( _forgetSnapshot, transformer: sequential(), ); final connectionRepository = getIt(); _apiStatusSubscription = connectionRepository.connectionStatusStream .listen((final ConnectionStatus connectionStatus) { switch (connectionStatus) { case ConnectionStatus.nonexistent: add(const BackupsServerReset()); isLoaded = false; break; case ConnectionStatus.connected: if (!isLoaded) { add(const BackupsServerLoaded()); isLoaded = true; } break; default: break; } }); _apiDataSubscription = connectionRepository.dataStream.listen( (final ApiData apiData) { if (apiData.backups.data == null || apiData.backupConfig.data == null) { add(const BackupsServerReset()); isLoaded = false; } else { add( BackupsStateChanged( apiData.backups.data!, apiData.backupConfig.data, ), ); isLoaded = true; } }, ); if (connectionRepository.connectionStatus == ConnectionStatus.connected) { add(const BackupsServerLoaded()); isLoaded = true; } } Future _loadState( final BackupsServerLoaded event, final Emitter emit, ) async { BackblazeBucket? bucket = getIt().backblazeBucket; final backups = getIt().apiData.backups; final backupConfig = getIt().apiData.backupConfig; if (backupConfig.data == null || backups.data == null) { emit(BackupsLoading()); return; } if (bucket != null && backupConfig.data!.encryptionKey != bucket.encryptionKey) { bucket = bucket.copyWith( encryptionKey: backupConfig.data!.encryptionKey, ); await getIt().setBackblazeBucket(bucket); } if (backupConfig.data!.isInitialized) { emit( BackupsInitialized( backblazeBucket: bucket, backupConfig: backupConfig.data, backups: backups.data ?? [], ), ); } else { emit(BackupsUnititialized()); } } Future _resetState( final BackupsServerReset event, final Emitter emit, ) async { emit(BackupsInitial()); } Future _initializeRepository( final InitializeBackupsRepository event, final Emitter emit, ) async { if (state is! BackupsUnititialized) { return; } emit(BackupsInitializing()); final String? encryptionKey = getIt() .apiData .backupConfig .data ?.encryptionKey; if (encryptionKey == null) { emit(BackupsUnititialized()); getIt() .showSnackBar("Couldn't get encryption key from your server."); return; } final BackblazeBucket bucket; if (state.backblazeBucket == null) { final settings = BackupsProviderSettings( provider: BackupsProviderType.backblaze, tokenId: event.credential.keyId, token: event.credential.applicationKey, isAuthorized: true, ); final provider = BackupsProviderFactory.createBackupsProviderInterface(settings); final String domain = getIt() .serverDomain! .domainName .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); final int serverId = getIt().serverDetails!.id; String bucketName = '${DateTime.now().millisecondsSinceEpoch}-$serverId-$domain'; if (bucketName.length > 49) { bucketName = bucketName.substring(0, 49); } final createStorageResult = await provider.createStorage(bucketName); if (createStorageResult.success == false || createStorageResult.data.isEmpty) { getIt().showSnackBar( createStorageResult.message ?? "Couldn't create storage on your server.", ); emit(BackupsUnititialized()); return; } final String bucketId = createStorageResult.data; final BackupsApplicationKey? key = (await provider.createApplicationKey(bucketId)).data; if (key == null) { getIt().showSnackBar( "Couldn't create application key on your server.", ); emit(BackupsUnititialized()); return; } bucket = BackblazeBucket( bucketId: bucketId, bucketName: bucketName, applicationKey: key.applicationKey, applicationKeyId: key.applicationKeyId, encryptionKey: encryptionKey, ); await getIt().setBackblazeBucket(bucket); emit(state.copyWith(backblazeBucket: bucket)); } else { bucket = state.backblazeBucket!; } final GenericResult result = await getIt().api.initializeRepository( InitializeRepositoryInput( provider: BackupsProviderType.backblaze, locationId: bucket.bucketId, locationName: bucket.bucketName, login: bucket.applicationKeyId, password: bucket.applicationKey, ), ); if (result.success == false) { getIt().showSnackBar( result.message ?? "Couldn't initialize repository on your server.", ); emit(BackupsUnititialized()); return; } getIt().apiData.backupConfig.invalidate(); getIt().apiData.backups.invalidate(); await getIt().reload(null); getIt().showSnackBar( 'Backups repository is now initializing. It may take a while.', ); } Future _updateState( final BackupsStateChanged event, final Emitter emit, ) async { if (event.backupConfiguration == null || event.backupConfiguration!.isInitialized == false) { emit(BackupsUnititialized()); return; } final BackblazeBucket? bucket = getIt().backblazeBucket; emit( BackupsInitialized( backblazeBucket: bucket, backupConfig: event.backupConfiguration, backups: event.backups, ), ); } Future _forceSnapshotListUpdate( final ForceSnapshotListUpdate event, final Emitter emit, ) async { final currentState = state; if (currentState is BackupsInitialized) { emit(BackupsBusy.fromState(currentState)); getIt().showSnackBar('backup.refetching_list'.tr()); await getIt().api.forceBackupListReload(); getIt().apiData.backups.invalidate(); emit(currentState); } } Future _createBackups( final CreateBackups event, final Emitter emit, ) async { final currentState = state; if (currentState is BackupsInitialized) { emit(BackupsBusy.fromState(currentState)); for (final service in event.services) { final GenericResult result = await getIt().api.startBackup( service.id, ); if (result.success == false) { getIt() .showSnackBar(result.message ?? 'Unknown error'); } if (result.data != null) { getIt() .apiData .serverJobs .data ?.add(result.data!); } } emit(currentState); getIt().emitData(); } } Future _restoreBackup( final RestoreBackup event, final Emitter emit, ) async { final currentState = state; if (currentState is BackupsInitialized) { emit(BackupsBusy.fromState(currentState)); final GenericResult result = await getIt().api.restoreBackup( event.backupId, event.restoreStrategy, ); if (result.success == false) { getIt() .showSnackBar(result.message ?? 'Unknown error'); } emit(currentState); } } Future _setAutobackupPeriod( final SetAutobackupPeriod event, final Emitter emit, ) async { final currentState = state; if (currentState is BackupsInitialized) { emit(BackupsBusy.fromState(currentState)); final GenericResult result = await getIt().api.setAutobackupPeriod( period: event.period?.inMinutes, ); if (result.success == false) { getIt() .showSnackBar(result.message ?? 'Unknown error'); } if (result.success == true) { getIt().apiData.backupConfig.data = getIt() .apiData .backupConfig .data ?.copyWith( autobackupPeriod: event.period, ); } emit(currentState); getIt().emitData(); } } Future _setAutobackupQuotas( final SetAutobackupQuotas event, final Emitter emit, ) async { final currentState = state; if (currentState is BackupsInitialized) { emit(BackupsBusy.fromState(currentState)); final GenericResult result = await getIt().api.setAutobackupQuotas( event.quotas, ); if (result.success == false) { getIt() .showSnackBar(result.message ?? 'Unknown error'); } if (result.success == true) { getIt().apiData.backupConfig.data = getIt() .apiData .backupConfig .data ?.copyWith( autobackupQuotas: event.quotas, ); } emit(currentState); getIt().emitData(); } } Future _forgetSnapshot( final ForgetSnapshot event, final Emitter emit, ) async { final currentState = state; if (currentState is BackupsInitialized) { // Optimistically remove the snapshot from the list getIt().apiData.backups.data = getIt() .apiData .backups .data ?.where((final Backup backup) => backup.id != event.backupId) .toList(); emit(BackupsBusy.fromState(currentState)); final GenericResult result = await getIt().api.forgetSnapshot( event.backupId, ); if (result.success == false) { getIt() .showSnackBar(result.message ?? 'jobs.generic_error'.tr()); } else if (result.data == false) { getIt() .showSnackBar('backup.forget_snapshot_error'.tr()); } emit(currentState); } } @override Future close() { _apiStatusSubscription.cancel(); _apiDataSubscription.cancel(); return super.close(); } @override void onChange(final Change change) { super.onChange(change); } late StreamSubscription _apiStatusSubscription; late StreamSubscription _apiDataSubscription; bool isLoaded = false; }