feat: add local backup export import core

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:23:30 +02:00
parent b256595e08
commit c3d5273869
6 changed files with 1380 additions and 4 deletions

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
@ -7,7 +8,8 @@ import 'package:path_provider/path_provider.dart';
import '../../application/application.dart';
import '../../domain/domain.dart';
final class PathProviderLocalMediaStorage implements LocalMediaStorage {
final class PathProviderLocalMediaStorage
implements LocalMediaStorage, LocalBackupMediaStore {
const PathProviderLocalMediaStorage();
static const _mediaDirectoryName = 'gametime_media';
@ -77,6 +79,64 @@ final class PathProviderLocalMediaStorage implements LocalMediaStorage {
}
}
@override
Future<List<EmbeddedBackupMediaFile>> readEmbeddableFiles(
List<LocalBackupResource> mediaAssets,
) async {
final files = <EmbeddedBackupMediaFile>[];
for (final resource in mediaAssets) {
final localUri = resource.payload['localUri'];
if (localUri is! String || localUri.isEmpty) {
continue;
}
final file = _fileFromLocalUri(localUri);
if (!await file.exists()) {
continue;
}
final bytes = await file.readAsBytes();
files.add(
EmbeddedBackupMediaFile(
mediaAssetId: resource.id,
role: 'original',
fileName: p.basename(file.path),
mimeType: resource.payload['mimeType'] as String?,
sizeBytes: bytes.length,
base64: base64Encode(bytes),
),
);
}
return files;
}
@override
Future<Map<String, RestoredMediaFile>> restoreEmbeddedFiles(
List<EmbeddedBackupMediaFile> files,
) async {
final restored = <String, RestoredMediaFile>{};
final directory = await _restoredBackupDirectory();
await directory.create(recursive: true);
for (final file in files) {
final bytes = switch (_decodeBase64OrNull(file.base64)) {
final decoded? => decoded,
null => null,
};
if (bytes == null) {
continue;
}
final extension = p.extension(file.fileName).toLowerCase();
final target = File(
p.join(directory.path, '${file.mediaAssetId}$extension'),
);
await target.writeAsBytes(bytes, flush: true);
restored[file.mediaAssetId] = RestoredMediaFile(
mediaAssetId: file.mediaAssetId,
localUri: target.uri.toString(),
sizeBytes: bytes.length,
);
}
return restored;
}
Future<Directory> _kindDirectory(MediaKind kind) async {
final root = await _rootDirectory();
final child = switch (kind) {
@ -90,6 +150,27 @@ final class PathProviderLocalMediaStorage implements LocalMediaStorage {
final documents = await getApplicationDocumentsDirectory();
return Directory(p.join(documents.path, _mediaDirectoryName));
}
Future<Directory> _restoredBackupDirectory() async {
final root = await _rootDirectory();
return Directory(p.join(root.path, 'restored'));
}
}
File _fileFromLocalUri(String localUri) {
final uri = Uri.tryParse(localUri);
if (uri != null && uri.scheme == 'file') {
return File.fromUri(uri);
}
return File(localUri);
}
Uint8List? _decodeBase64OrNull(String value) {
try {
return base64Decode(value);
} on FormatException {
return null;
}
}
final class _ImageDimensions {