scope cloud backups to dedicated folder

This commit is contained in:
ngfchl
2026-07-19 14:04:09 +08:00
parent c612e139c0
commit f3865a9a82
2 changed files with 112 additions and 82 deletions
+58 -60
View File
@@ -1281,66 +1281,17 @@ class _MediaLibraryPageState extends ConsumerState<MediaLibraryPage> {
height: (MediaQuery.sizeOf(dialogContext).height - 260)
.clamp(240.0, 360.0)
.toDouble(),
child: ListView.separated(
itemCount: backups.length,
separatorBuilder: (_, _) => const ShadSeparator.horizontal(),
itemBuilder: (itemContext, index) {
final backup = backups[index];
final cs = ShadTheme.of(itemContext).colorScheme;
return LayoutBuilder(
builder: (context, constraints) => ShadButton.ghost(
width: constraints.maxWidth,
expands: false,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 9,
),
onPressed: () => Navigator.of(dialogContext).pop(backup),
child: Row(
children: [
Icon(
Icons.storage_rounded,
size: 18,
color: cs.mutedForeground,
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
backup.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: cs.foreground,
),
),
const SizedBox(height: 3),
Text(
'${backup.formattedSize} · ${backup.modifiedAt}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: cs.mutedForeground,
),
),
],
),
),
Icon(
Icons.chevron_right_rounded,
color: cs.mutedForeground,
),
],
),
),
);
},
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var index = 0; index < backups.length; index++) ...[
_cloudBackupRestoreRow(dialogContext, backup: backups[index]),
if (index < backups.length - 1)
const ShadSeparator.horizontal(),
],
],
),
),
),
),
@@ -1354,6 +1305,53 @@ class _MediaLibraryPageState extends ConsumerState<MediaLibraryPage> {
}
}
Widget _cloudBackupRestoreRow(
BuildContext context, {
required CloudFile backup,
}) {
final cs = ShadTheme.of(context).colorScheme;
return LayoutBuilder(
builder: (context, constraints) => ShadButton.ghost(
width: constraints.maxWidth,
expands: false,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9),
onPressed: () => Navigator.of(context).pop(backup),
child: Row(
children: [
Icon(Icons.storage_rounded, size: 18, color: cs.mutedForeground),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
backup.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: cs.foreground,
),
),
const SizedBox(height: 3),
Text(
'${backup.formattedSize} · ${backup.modifiedAt}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 11, color: cs.mutedForeground),
),
],
),
),
Icon(Icons.chevron_right_rounded, color: cs.mutedForeground),
],
),
),
);
}
Future<void> _searchTMDB(String query) async {
final text = query.trim();
if (text.isEmpty || _tmdbApiKey.isEmpty) return;
+54 -22
View File
@@ -344,7 +344,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
'guangya-media-',
);
final backup = File(
'${temporaryDirectory.path}/media-library-${DateTime.now().millisecondsSinceEpoch}.sqlite3',
'${temporaryDirectory.path}/${_cloudBackupFileName(DateTime.now())}',
);
_appendBackupLog('正在导出本地刮削数据库');
await _store.exportBackupTo(backup.path);
@@ -413,6 +413,22 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
Future<_CloudBackupDestination> _resolveCloudBackupDestination() async {
const folderName = '小黄鸭备份';
final existing = await _findCloudBackupDestination();
if (existing != null) return existing;
final folderID = _findStringDeep(
await _api!.fsCreateDir(folderName, parentID: null),
const ['fileId', 'file_id', 'resId', 'res_id', 'id'],
);
if (folderID == null || folderID.isEmpty) {
throw Exception('无法创建云盘备份目录「$folderName');
}
await StorageManager.set(StorageKeys.cloudScrapedBackupFolderID, folderID);
_appendBackupLog('已创建云盘备份目录:云盘根目录/$folderName');
return _CloudBackupDestination(id: folderID, path: '云盘根目录/$folderName');
}
Future<_CloudBackupDestination?> _findCloudBackupDestination() async {
const folderName = '小黄鸭备份';
final storedID = StorageManager.get<String>(
StorageKeys.cloudScrapedBackupFolderID,
@@ -425,17 +441,9 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
root,
).where((file) => file.isDirectory && file.name == folderName);
final folder = existing.isEmpty ? null : existing.first;
final folderID =
folder?.id ??
_findStringDeep(
await _api!.fsCreateDir(folderName, parentID: null),
const ['fileId', 'file_id', 'resId', 'res_id', 'id'],
);
if (folderID == null || folderID.isEmpty) {
throw Exception('无法创建云盘备份目录「$folderName');
}
await StorageManager.set(StorageKeys.cloudScrapedBackupFolderID, folderID);
return _CloudBackupDestination(id: folderID, path: '云盘根目录/$folderName');
if (folder == null) return null;
await StorageManager.set(StorageKeys.cloudScrapedBackupFolderID, folder.id);
return _CloudBackupDestination(id: folder.id, path: '云盘根目录/$folderName');
}
Future<List<CloudFile>> cloudScrapedBackups() async {
@@ -453,17 +461,35 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
),
);
try {
final response = await _api!.searchFiles(
'media-library.sqlite3',
pageSize: 100,
final destination = await _findCloudBackupDestination();
if (destination == null) {
_appendBackupLog('未找到云盘备份目录,当前没有可恢复的备份');
state = state.copyWith(
cloudBackupSync: const CloudBackupSyncProgress(
phase: '未找到云盘备份',
destination: '云盘根目录/小黄鸭备份',
transferredBytes: 0,
totalBytes: 0,
isActive: false,
),
);
return const [];
}
_appendBackupLog('正在读取备份目录:${destination.path}');
final response = await _api!.fsFiles(
parentID: destination.id,
pageSize: 1000,
);
final backups = _extractFiles(response)
.where(
(file) =>
!file.isDirectory &&
file.name.toLowerCase().endsWith('.sqlite3'),
)
.toList();
final backups =
_extractFiles(response)
.where(
(file) =>
!file.isDirectory &&
file.name.toLowerCase().endsWith('.sqlite3') &&
file.name.startsWith('media-library-'),
)
.toList()
..sort((a, b) => b.modifiedAt.compareTo(a.modifiedAt));
_appendBackupLog('云盘备份查找完成,找到 ${backups.length} 个文件');
state = state.copyWith(
cloudBackupSync: const CloudBackupSyncProgress(
@@ -1190,6 +1216,12 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
.replaceFirst(RegExp(r'^DioException \[[^\]]+\]:\s*'), '网络请求失败:');
}
String _cloudBackupFileName(DateTime value) {
String two(int number) => number.toString().padLeft(2, '0');
return 'media-library-${value.year}${two(value.month)}${two(value.day)}-'
'${two(value.hour)}${two(value.minute)}${two(value.second)}.sqlite3';
}
List<MediaLibraryScanLog> _loadScanHistory() {
final raw = StorageManager.get<dynamic>(StorageKeys.mediaScanHistory);
if (raw is! List) return const [];