feat: add local backup export import core
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -15,6 +15,8 @@ abstract interface class AppDependencies {
|
||||
WorkoutHistoryUseCases get workoutHistoryUseCases;
|
||||
ProgressionStatsUseCase get progressionStatsUseCase;
|
||||
ExercisePerformanceReferenceUseCase get exercisePerformanceReferenceUseCase;
|
||||
DataExportUseCase get dataExportUseCase;
|
||||
DataImportUseCase get dataImportUseCase;
|
||||
SyncUseCases get syncUseCases;
|
||||
ShareUseCases get shareUseCases;
|
||||
}
|
||||
@ -33,6 +35,8 @@ final class AppBootstrap implements AppDependencies {
|
||||
required this.workoutHistoryUseCases,
|
||||
required this.progressionStatsUseCase,
|
||||
required this.exercisePerformanceReferenceUseCase,
|
||||
required this.dataExportUseCase,
|
||||
required this.dataImportUseCase,
|
||||
required this.syncUseCases,
|
||||
required this.shareUseCases,
|
||||
required this.syncGateway,
|
||||
@ -62,6 +66,10 @@ final class AppBootstrap implements AppDependencies {
|
||||
@override
|
||||
final ExercisePerformanceReferenceUseCase exercisePerformanceReferenceUseCase;
|
||||
@override
|
||||
final DataExportUseCase dataExportUseCase;
|
||||
@override
|
||||
final DataImportUseCase dataImportUseCase;
|
||||
@override
|
||||
final SyncUseCases syncUseCases;
|
||||
@override
|
||||
final ShareUseCases shareUseCases;
|
||||
@ -90,6 +98,8 @@ final class AppBootstrap implements AppDependencies {
|
||||
);
|
||||
final performanceReferenceRepository =
|
||||
DriftExercisePerformanceReferenceRepository(database);
|
||||
final localDataBackupRepository = DriftLocalDataBackupRepository(database);
|
||||
const localBackupMediaStore = PathProviderLocalMediaStorage();
|
||||
final ids = LocalIdGenerator();
|
||||
const clock = SystemClock();
|
||||
const originDeviceId = 'local-device';
|
||||
@ -179,6 +189,16 @@ final class AppBootstrap implements AppDependencies {
|
||||
exercisePerformanceReferenceUseCase: ExercisePerformanceReferenceUseCase(
|
||||
repository: performanceReferenceRepository,
|
||||
),
|
||||
dataExportUseCase: DataExportUseCase(
|
||||
repository: localDataBackupRepository,
|
||||
mediaStore: localBackupMediaStore,
|
||||
clock: clock,
|
||||
),
|
||||
dataImportUseCase: DataImportUseCase(
|
||||
repository: localDataBackupRepository,
|
||||
mediaStore: localBackupMediaStore,
|
||||
clock: clock,
|
||||
),
|
||||
syncUseCases: SyncUseCases(
|
||||
tokenStore: const SecureStorageAuthTokenStore(),
|
||||
remoteSyncApi: remoteSyncApi,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../domain/domain.dart';
|
||||
|
||||
abstract interface class Clock {
|
||||
@ -34,6 +36,213 @@ final class TagUsage {
|
||||
final int count;
|
||||
}
|
||||
|
||||
enum LocalBackupImportMode { merge, replaceAll }
|
||||
|
||||
enum LocalBackupValidationError {
|
||||
invalidFile,
|
||||
incompatibleFormat,
|
||||
newerVersion,
|
||||
corrupted,
|
||||
activeWorkoutInProgress,
|
||||
importFailedNoMutation,
|
||||
}
|
||||
|
||||
final class LocalBackupException implements Exception {
|
||||
const LocalBackupException(this.error);
|
||||
|
||||
final LocalBackupValidationError error;
|
||||
|
||||
@override
|
||||
String toString() => error.name;
|
||||
}
|
||||
|
||||
final class LocalBackupDocument {
|
||||
const LocalBackupDocument({required this.fileName, required this.bytes});
|
||||
|
||||
final String fileName;
|
||||
final Uint8List bytes;
|
||||
}
|
||||
|
||||
final class LocalBackupCounts {
|
||||
const LocalBackupCounts({
|
||||
required this.exercises,
|
||||
required this.programs,
|
||||
required this.workoutTemplates,
|
||||
required this.workoutHistories,
|
||||
required this.mediaAssets,
|
||||
required this.embeddedMediaFiles,
|
||||
});
|
||||
|
||||
final int exercises;
|
||||
final int programs;
|
||||
final int workoutTemplates;
|
||||
final int workoutHistories;
|
||||
final int mediaAssets;
|
||||
final int embeddedMediaFiles;
|
||||
}
|
||||
|
||||
final class LocalBackupPreview {
|
||||
const LocalBackupPreview({
|
||||
required this.exportedAt,
|
||||
required this.counts,
|
||||
required this.hasEmbeddedMedia,
|
||||
required this.missingMediaCount,
|
||||
required this.hasNameDuplicates,
|
||||
});
|
||||
|
||||
final DateTime exportedAt;
|
||||
final LocalBackupCounts counts;
|
||||
final bool hasEmbeddedMedia;
|
||||
final int missingMediaCount;
|
||||
final bool hasNameDuplicates;
|
||||
}
|
||||
|
||||
final class LocalBackupImportResult {
|
||||
const LocalBackupImportResult({
|
||||
required this.insertedCount,
|
||||
required this.updatedCount,
|
||||
required this.ignoredOlderCount,
|
||||
required this.deletedByReplaceCount,
|
||||
required this.missingMediaCount,
|
||||
});
|
||||
|
||||
final int insertedCount;
|
||||
final int updatedCount;
|
||||
final int ignoredOlderCount;
|
||||
final int deletedByReplaceCount;
|
||||
final int missingMediaCount;
|
||||
|
||||
LocalBackupImportResult copyWith({int? missingMediaCount}) {
|
||||
return LocalBackupImportResult(
|
||||
insertedCount: insertedCount,
|
||||
updatedCount: updatedCount,
|
||||
ignoredOlderCount: ignoredOlderCount,
|
||||
deletedByReplaceCount: deletedByReplaceCount,
|
||||
missingMediaCount: missingMediaCount ?? this.missingMediaCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class LocalBackupResource {
|
||||
const LocalBackupResource({
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
required this.payload,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final DateTime updatedAt;
|
||||
final Map<String, Object?> payload;
|
||||
|
||||
LocalBackupResource copyWith({Map<String, Object?>? payload}) {
|
||||
return LocalBackupResource(
|
||||
id: id,
|
||||
updatedAt: updatedAt,
|
||||
payload: payload ?? this.payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class LocalDataExportSnapshot {
|
||||
const LocalDataExportSnapshot({
|
||||
required this.exportedAt,
|
||||
required this.appSchemaVersion,
|
||||
required this.originDeviceId,
|
||||
required this.mediaAssets,
|
||||
required this.exercises,
|
||||
required this.programs,
|
||||
required this.workoutTemplates,
|
||||
required this.workoutHistories,
|
||||
this.mediaFiles = const [],
|
||||
});
|
||||
|
||||
final DateTime exportedAt;
|
||||
final int appSchemaVersion;
|
||||
final String originDeviceId;
|
||||
final List<LocalBackupResource> mediaAssets;
|
||||
final List<LocalBackupResource> exercises;
|
||||
final List<LocalBackupResource> programs;
|
||||
final List<LocalBackupResource> workoutTemplates;
|
||||
final List<LocalBackupResource> workoutHistories;
|
||||
final List<EmbeddedBackupMediaFile> mediaFiles;
|
||||
|
||||
LocalBackupCounts get counts => LocalBackupCounts(
|
||||
exercises: exercises.length,
|
||||
programs: programs.length,
|
||||
workoutTemplates: workoutTemplates.length,
|
||||
workoutHistories: workoutHistories.length,
|
||||
mediaAssets: mediaAssets.length,
|
||||
embeddedMediaFiles: mediaFiles.length,
|
||||
);
|
||||
|
||||
LocalDataExportSnapshot copyWith({
|
||||
List<LocalBackupResource>? mediaAssets,
|
||||
List<EmbeddedBackupMediaFile>? mediaFiles,
|
||||
}) {
|
||||
return LocalDataExportSnapshot(
|
||||
exportedAt: exportedAt,
|
||||
appSchemaVersion: appSchemaVersion,
|
||||
originDeviceId: originDeviceId,
|
||||
mediaAssets: mediaAssets ?? this.mediaAssets,
|
||||
exercises: exercises,
|
||||
programs: programs,
|
||||
workoutTemplates: workoutTemplates,
|
||||
workoutHistories: workoutHistories,
|
||||
mediaFiles: mediaFiles ?? this.mediaFiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class EmbeddedBackupMediaFile {
|
||||
const EmbeddedBackupMediaFile({
|
||||
required this.mediaAssetId,
|
||||
required this.role,
|
||||
required this.fileName,
|
||||
required this.mimeType,
|
||||
required this.sizeBytes,
|
||||
required this.base64,
|
||||
});
|
||||
|
||||
final String mediaAssetId;
|
||||
final String role;
|
||||
final String fileName;
|
||||
final String? mimeType;
|
||||
final int sizeBytes;
|
||||
final String base64;
|
||||
}
|
||||
|
||||
final class RestoredMediaFile {
|
||||
const RestoredMediaFile({
|
||||
required this.mediaAssetId,
|
||||
required this.localUri,
|
||||
required this.sizeBytes,
|
||||
});
|
||||
|
||||
final String mediaAssetId;
|
||||
final String localUri;
|
||||
final int sizeBytes;
|
||||
}
|
||||
|
||||
abstract interface class LocalDataBackupRepository {
|
||||
Future<LocalDataExportSnapshot> readExportSnapshot(DateTime exportedAt);
|
||||
Future<bool> hasOpenActiveWorkoutSession();
|
||||
Future<bool> hasAnyUserData();
|
||||
Future<LocalBackupImportResult> applyImportSnapshot({
|
||||
required LocalDataExportSnapshot snapshot,
|
||||
required LocalBackupImportMode mode,
|
||||
required DateTime importedAt,
|
||||
});
|
||||
}
|
||||
|
||||
abstract interface class LocalBackupMediaStore {
|
||||
Future<List<EmbeddedBackupMediaFile>> readEmbeddableFiles(
|
||||
List<LocalBackupResource> mediaAssets,
|
||||
);
|
||||
Future<Map<String, RestoredMediaFile>> restoreEmbeddedFiles(
|
||||
List<EmbeddedBackupMediaFile> files,
|
||||
);
|
||||
}
|
||||
|
||||
enum ProgressionPeriod { fourWeeks, threeMonths, all }
|
||||
|
||||
enum ProgressionMeasure { manualScore, stopwatchScore, reps, time }
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../domain/domain.dart';
|
||||
import 'ports.dart';
|
||||
@ -49,6 +50,175 @@ List<TagUsage> tagSuggestionsFor<T>(
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
final class LocalBackupCodec {
|
||||
const LocalBackupCodec();
|
||||
|
||||
static const kind = 'gametime.localBackup';
|
||||
static const formatVersion = 1;
|
||||
static const minSupportedFormatVersion = 1;
|
||||
|
||||
Uint8List encode(LocalDataExportSnapshot snapshot) {
|
||||
final document = <String, Object?>{
|
||||
'kind': kind,
|
||||
'formatVersion': formatVersion,
|
||||
'minSupportedFormatVersion': minSupportedFormatVersion,
|
||||
'exportedAt': snapshot.exportedAt.toUtc().toIso8601String(),
|
||||
'appSchemaVersion': snapshot.appSchemaVersion,
|
||||
'originDeviceId': snapshot.originDeviceId,
|
||||
'counts': _countsToJson(snapshot.counts),
|
||||
'data': {
|
||||
'mediaAssets': snapshot.mediaAssets
|
||||
.map((resource) => resource.payload)
|
||||
.toList(),
|
||||
'exercises': snapshot.exercises
|
||||
.map((resource) => resource.payload)
|
||||
.toList(),
|
||||
'programs': snapshot.programs
|
||||
.map((resource) => resource.payload)
|
||||
.toList(),
|
||||
'workoutTemplates': snapshot.workoutTemplates
|
||||
.map((resource) => resource.payload)
|
||||
.toList(),
|
||||
'workoutHistories': snapshot.workoutHistories
|
||||
.map((resource) => resource.payload)
|
||||
.toList(),
|
||||
},
|
||||
'mediaFiles': snapshot.mediaFiles.map(_mediaFileToJson).toList(),
|
||||
};
|
||||
return Uint8List.fromList(utf8.encode(jsonEncode(document)));
|
||||
}
|
||||
|
||||
LocalDataExportSnapshot decode(Uint8List bytes) {
|
||||
final Object? decoded;
|
||||
try {
|
||||
decoded = jsonDecode(utf8.decode(bytes));
|
||||
} on FormatException {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
} on Object {
|
||||
throw const LocalBackupException(LocalBackupValidationError.invalidFile);
|
||||
}
|
||||
if (decoded is! Map) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.invalidFile);
|
||||
}
|
||||
final root = Map<String, Object?>.from(decoded);
|
||||
if (root['kind'] != kind) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.invalidFile);
|
||||
}
|
||||
final version = root['formatVersion'];
|
||||
final minSupported = root['minSupportedFormatVersion'];
|
||||
if (version is! int || minSupported is! int || version < 1) {
|
||||
throw const LocalBackupException(
|
||||
LocalBackupValidationError.incompatibleFormat,
|
||||
);
|
||||
}
|
||||
if (version > formatVersion || minSupported > formatVersion) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.newerVersion);
|
||||
}
|
||||
final data = root['data'];
|
||||
if (data is! Map) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
final dataMap = Map<String, Object?>.from(data);
|
||||
return LocalDataExportSnapshot(
|
||||
exportedAt: _dateTimeFromBackup(root['exportedAt']),
|
||||
appSchemaVersion: root['appSchemaVersion'] as int? ?? 1,
|
||||
originDeviceId: root['originDeviceId'] as String? ?? 'local-device',
|
||||
mediaAssets: _resourcesFromBackup(dataMap, 'mediaAssets'),
|
||||
exercises: _resourcesFromBackup(dataMap, 'exercises'),
|
||||
programs: _resourcesFromBackup(dataMap, 'programs'),
|
||||
workoutTemplates: _resourcesFromBackup(dataMap, 'workoutTemplates'),
|
||||
workoutHistories: _resourcesFromBackup(dataMap, 'workoutHistories'),
|
||||
mediaFiles: _mediaFilesFromBackup(root['mediaFiles']),
|
||||
);
|
||||
}
|
||||
|
||||
LocalBackupPreview preview(Uint8List bytes) {
|
||||
final snapshot = decode(bytes);
|
||||
return LocalBackupPreview(
|
||||
exportedAt: snapshot.exportedAt,
|
||||
counts: snapshot.counts,
|
||||
hasEmbeddedMedia: snapshot.mediaFiles.isNotEmpty,
|
||||
missingMediaCount: _missingMediaCount(snapshot),
|
||||
hasNameDuplicates: _hasNameDuplicates(snapshot),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class DataExportUseCase {
|
||||
const DataExportUseCase({
|
||||
required this.repository,
|
||||
required this.mediaStore,
|
||||
required this.clock,
|
||||
this.codec = const LocalBackupCodec(),
|
||||
});
|
||||
|
||||
final LocalDataBackupRepository repository;
|
||||
final LocalBackupMediaStore mediaStore;
|
||||
final Clock clock;
|
||||
final LocalBackupCodec codec;
|
||||
|
||||
Future<LocalBackupDocument> exportAll() async {
|
||||
final exportedAt = clock.now();
|
||||
final snapshot = await repository.readExportSnapshot(exportedAt);
|
||||
final mediaFiles = await mediaStore.readEmbeddableFiles(
|
||||
snapshot.mediaAssets,
|
||||
);
|
||||
final completeSnapshot = snapshot.copyWith(mediaFiles: mediaFiles);
|
||||
return LocalBackupDocument(
|
||||
fileName: _localBackupFileName(exportedAt),
|
||||
bytes: codec.encode(completeSnapshot),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class DataImportUseCase {
|
||||
const DataImportUseCase({
|
||||
required this.repository,
|
||||
required this.mediaStore,
|
||||
required this.clock,
|
||||
this.codec = const LocalBackupCodec(),
|
||||
});
|
||||
|
||||
final LocalDataBackupRepository repository;
|
||||
final LocalBackupMediaStore mediaStore;
|
||||
final Clock clock;
|
||||
final LocalBackupCodec codec;
|
||||
|
||||
Future<LocalBackupPreview> preview(Uint8List bytes) async {
|
||||
return codec.preview(bytes);
|
||||
}
|
||||
|
||||
Future<LocalBackupImportResult> importFrom(
|
||||
Uint8List bytes, {
|
||||
required LocalBackupImportMode mode,
|
||||
}) async {
|
||||
if (await repository.hasOpenActiveWorkoutSession()) {
|
||||
throw const LocalBackupException(
|
||||
LocalBackupValidationError.activeWorkoutInProgress,
|
||||
);
|
||||
}
|
||||
final decoded = codec.decode(bytes);
|
||||
final restoredMediaFiles = await mediaStore.restoreEmbeddedFiles(
|
||||
decoded.mediaFiles,
|
||||
);
|
||||
final snapshot = _withRestoredMediaUris(decoded, restoredMediaFiles);
|
||||
try {
|
||||
final result = await repository.applyImportSnapshot(
|
||||
snapshot: snapshot,
|
||||
mode: mode,
|
||||
importedAt: clock.now(),
|
||||
);
|
||||
return result.copyWith(missingMediaCount: _missingMediaCount(snapshot));
|
||||
} on LocalBackupException {
|
||||
rethrow;
|
||||
} on Object {
|
||||
throw const LocalBackupException(
|
||||
LocalBackupValidationError.importFailedNoMutation,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StarterSeedStatus { inserted, skippedAlreadyApplied, skippedNotEmpty }
|
||||
|
||||
final class StarterSeedResult {
|
||||
@ -4714,3 +4884,160 @@ String _copyName(String sourceName, Iterable<String> existingNames) {
|
||||
copyNumber += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> _countsToJson(LocalBackupCounts counts) => {
|
||||
'exercises': counts.exercises,
|
||||
'programs': counts.programs,
|
||||
'workoutTemplates': counts.workoutTemplates,
|
||||
'workoutHistories': counts.workoutHistories,
|
||||
'mediaAssets': counts.mediaAssets,
|
||||
'embeddedMediaFiles': counts.embeddedMediaFiles,
|
||||
};
|
||||
|
||||
Map<String, Object?> _mediaFileToJson(EmbeddedBackupMediaFile file) => {
|
||||
'mediaAssetId': file.mediaAssetId,
|
||||
'role': file.role,
|
||||
'fileName': file.fileName,
|
||||
'mimeType': file.mimeType,
|
||||
'sizeBytes': file.sizeBytes,
|
||||
'base64': file.base64,
|
||||
};
|
||||
|
||||
List<LocalBackupResource> _resourcesFromBackup(
|
||||
Map<String, Object?> data,
|
||||
String key,
|
||||
) {
|
||||
final value = data[key];
|
||||
if (value is! List) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
return value
|
||||
.map((item) {
|
||||
if (item is! Map) {
|
||||
throw const LocalBackupException(
|
||||
LocalBackupValidationError.corrupted,
|
||||
);
|
||||
}
|
||||
final payload = Map<String, Object?>.from(item);
|
||||
return LocalBackupResource(
|
||||
id: _stringFromBackup(payload, 'id'),
|
||||
updatedAt: _updatedAtFromBackupPayload(payload),
|
||||
payload: payload,
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
List<EmbeddedBackupMediaFile> _mediaFilesFromBackup(Object? value) {
|
||||
if (value == null) {
|
||||
return const [];
|
||||
}
|
||||
if (value is! List) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
return value
|
||||
.map((item) {
|
||||
if (item is! Map) {
|
||||
throw const LocalBackupException(
|
||||
LocalBackupValidationError.corrupted,
|
||||
);
|
||||
}
|
||||
final payload = Map<String, Object?>.from(item);
|
||||
return EmbeddedBackupMediaFile(
|
||||
mediaAssetId: _stringFromBackup(payload, 'mediaAssetId'),
|
||||
role: payload['role'] as String? ?? 'original',
|
||||
fileName: _stringFromBackup(payload, 'fileName'),
|
||||
mimeType: payload['mimeType'] as String?,
|
||||
sizeBytes: payload['sizeBytes'] as int? ?? 0,
|
||||
base64: _stringFromBackup(payload, 'base64'),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String _stringFromBackup(Map<String, Object?> payload, String key) {
|
||||
final value = payload[key];
|
||||
if (value is String && value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
|
||||
DateTime _updatedAtFromBackupPayload(Map<String, Object?> payload) {
|
||||
final metadata = payload['metadata'];
|
||||
if (metadata is! Map) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
return _dateTimeFromBackup(Map<String, Object?>.from(metadata)['updatedAt']);
|
||||
}
|
||||
|
||||
DateTime _dateTimeFromBackup(Object? value) {
|
||||
if (value is! String) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
final parsed = DateTime.tryParse(value);
|
||||
if (parsed == null) {
|
||||
throw const LocalBackupException(LocalBackupValidationError.corrupted);
|
||||
}
|
||||
return parsed.toUtc();
|
||||
}
|
||||
|
||||
int _missingMediaCount(LocalDataExportSnapshot snapshot) {
|
||||
final embeddedIds = snapshot.mediaFiles
|
||||
.map((file) => file.mediaAssetId)
|
||||
.toSet();
|
||||
return snapshot.mediaAssets
|
||||
.where((asset) => !embeddedIds.contains(asset.id))
|
||||
.length;
|
||||
}
|
||||
|
||||
bool _hasNameDuplicates(LocalDataExportSnapshot snapshot) {
|
||||
return _hasDuplicatesByName(snapshot.exercises) ||
|
||||
_hasDuplicatesByName(snapshot.programs) ||
|
||||
_hasDuplicatesByName(snapshot.workoutTemplates);
|
||||
}
|
||||
|
||||
bool _hasDuplicatesByName(List<LocalBackupResource> resources) {
|
||||
final names = <String>{};
|
||||
for (final resource in resources) {
|
||||
final name = resource.payload['name'];
|
||||
if (name is! String) {
|
||||
continue;
|
||||
}
|
||||
if (!names.add(name.trim().toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDataExportSnapshot _withRestoredMediaUris(
|
||||
LocalDataExportSnapshot snapshot,
|
||||
Map<String, RestoredMediaFile> restoredFiles,
|
||||
) {
|
||||
if (restoredFiles.isEmpty) {
|
||||
return snapshot;
|
||||
}
|
||||
final mediaAssets = [
|
||||
for (final asset in snapshot.mediaAssets)
|
||||
if (restoredFiles[asset.id] case final restored?)
|
||||
asset.copyWith(
|
||||
payload: {
|
||||
...asset.payload,
|
||||
'localUri': restored.localUri,
|
||||
'sizeBytes': restored.sizeBytes,
|
||||
},
|
||||
)
|
||||
else
|
||||
asset,
|
||||
];
|
||||
return snapshot.copyWith(mediaAssets: mediaAssets);
|
||||
}
|
||||
|
||||
String _localBackupFileName(DateTime exportedAt) {
|
||||
final utc = exportedAt.toUtc();
|
||||
final year = utc.year.toString().padLeft(4, '0');
|
||||
final month = utc.month.toString().padLeft(2, '0');
|
||||
final day = utc.day.toString().padLeft(2, '0');
|
||||
return 'gametime-sauvegarde-$year-$month-$day.gametime';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user