createIncrementalBackup function

Future<IncrementalBackupResult> createIncrementalBackup({
  1. required Directory sourceDirectory,
  2. required Directory backupDirectory,
  3. Future<void> onProgress(
    1. BackupProgress progress
    )?,
})

Implementation

Future<IncrementalBackupResult> createIncrementalBackup({
  required Directory sourceDirectory,
  required Directory backupDirectory,
  Future<void> Function(BackupProgress progress)? onProgress,
}) async {
  await backupDirectory.create(recursive: true);
  final existingBackups = await listManagedLocalBackups(backupDirectory);
  final stateFile =
      File('${backupDirectory.path}/$_incrementalBackupStateName');
  final stateFileExists = await stateFile.exists();
  var forceFullBackup =
      await File('${backupDirectory.path}/$_forceFullBackupMarkerName')
          .exists();
  var previousState =
      forceFullBackup ? null : await _readState(backupDirectory);

  if (!forceFullBackup && stateFileExists && previousState == null) {
    forceFullBackup = true;
  }
  if (previousState != null &&
      !_isBackupChainComplete(previousState.backupChain, existingBackups)) {
    previousState = null;
    forceFullBackup = true;
  }

  final snapshot = await _readSourceSnapshot(
    sourceDirectory,
    reusableSignatures: previousState?.files,
  );
  previousState ??= forceFullBackup
      ? const _BackupState(files: {}, backupChain: [])
      : await _stateFromLegacyBackup(snapshot, existingBackups);
  previousState = await _recoverMissingContentHashes(
    previousState,
    snapshot,
    backupDirectory,
  );
  previousState = await _recoverMissingComparisonHashes(
    previousState,
    backupDirectory,
  );
  previousState = _withoutOperationalBackupFiles(previousState);
  final localIdMappings = await _readLocalIdMappings(sourceDirectory);
  final remappedState = _remapBackupState(
    previousState,
    localIdMappings,
    snapshot.signatures.keys.toSet(),
  );
  previousState = remappedState.state;
  final equivalentImageRemap = _remapEquivalentImagePaths(
    previousState,
    snapshot,
  );
  previousState = equivalentImageRemap.state;
  final movedFiles = <String, String>{
    ...previousState.pendingMovedFiles,
    ...remappedState.movedFiles,
    ...equivalentImageRemap.movedFiles,
  };

  final changedPaths = snapshot.signatures.keys
      .where((path) => previousState!.files[path] != snapshot.signatures[path])
      .toList()
    ..sort();
  final deletedPaths = previousState.files.keys
      .where((path) => !snapshot.signatures.containsKey(path))
      .toList()
    ..sort();

  debugPrint(
    'Incremental backup plan: ${snapshot.signatures.length} files, '
    '${changedPaths.length} changed, ${deletedPaths.length} deleted',
  );

  if (changedPaths.isEmpty && deletedPaths.isEmpty) {
    await _writeState(
      backupDirectory,
      _BackupState(
        files: snapshot.signatures,
        backupChain: previousState.backupChain,
        pendingMovedFiles: movedFiles,
      ),
    );
    return const IncrementalBackupResult(
      backupFile: null,
      changedFileCount: 0,
      deletedFileCount: 0,
    );
  }

  final backupFile = await _nextBackupFile(backupDirectory);
  final temporaryFile = File('${backupFile.path}.partial');
  File? completedBackup;

  try {
    final manifest = {
      'version': 1,
      'createdAt': DateTime.now().toUtc().toIso8601String(),
      'baseBackup': previousState.backupChain.isEmpty
          ? null
          : previousState.backupChain.last,
      'changedFiles': changedPaths,
      'deletedFiles': deletedPaths,
      'movedFiles': movedFiles,
      'snapshot': snapshot.signatures.map(
        (path, signature) => MapEntry(path, signature.toJson()),
      ),
      'backupChain': [
        ...previousState.backupChain,
        _basename(backupFile.path),
      ],
    };
    await _writeArchiveInBackground(
      sourceDirectory: sourceDirectory,
      temporaryFile: temporaryFile,
      changedPaths: changedPaths,
      manifestJson: jsonEncode(manifest),
      readableStructure: _buildReadableBackupStructure(snapshot),
      onProgress: onProgress,
    );

    completedBackup = await temporaryFile.rename(backupFile.path);
    final nextState = _BackupState(
      files: snapshot.signatures,
      backupChain: [
        ...previousState.backupChain,
        _basename(completedBackup.path),
      ],
    );
    await _writeState(backupDirectory, nextState);

    final forceMarker =
        File('${backupDirectory.path}/$_forceFullBackupMarkerName');
    if (await forceMarker.exists()) {
      await forceMarker.delete();
    }

    if (changedPaths.isEmpty) {
      await onProgress?.call(
        const BackupProgress(
          progress: _archiveProgressLimit,
          currentFile: 'Gelöschte Dateien',
          eta: 'weniger als 10 s',
          processedFiles: 0,
          totalFiles: 0,
        ),
      );
    }

    await onProgress?.call(
      BackupProgress(
        progress: 1,
        currentFile: 'Backup abgeschlossen',
        eta: '0 s',
        processedFiles: changedPaths.length,
        totalFiles: changedPaths.length,
      ),
    );

    return IncrementalBackupResult(
      backupFile: completedBackup,
      changedFileCount: changedPaths.length,
      deletedFileCount: deletedPaths.length,
    );
  } catch (_) {
    if (await temporaryFile.exists()) {
      await temporaryFile.delete();
    }
    if (completedBackup != null && await completedBackup.exists()) {
      await completedBackup.delete();
    }
    rethrow;
  }
}