Skip to content

Commit 23cc414

Browse files
author
Fastace
committed
增加Restic S3 文件备份、恢复逻辑,BUG:目前文件恢复逻辑列表有BUG,没有按照时间-媒体名称维度分开列
1 parent 28c24ef commit 23cc414

9 files changed

Lines changed: 1401 additions & 245 deletions

File tree

source/app/src/main/kotlin/com/xayah/databackup/MainActivity.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import com.xayah.core.ui.util.LocalNavController
2222
import com.xayah.core.util.command.BaseUtil
2323
import com.xayah.core.util.decodeURL
2424
import com.xayah.feature.main.cloud.PageCloud
25+
import com.xayah.feature.main.restore.CloudFilesRestorePage
2526
import com.xayah.feature.main.restore.CloudRestorePage
27+
import com.xayah.feature.main.restore.CloudFilesBackupDetailPage
2628
import com.xayah.feature.main.cloud.add.PageCloudAddAccount
2729
import com.xayah.feature.main.cloud.add.PageFTPSetup
2830
import com.xayah.feature.main.cloud.add.PageSFTPSetup
@@ -312,6 +314,47 @@ class MainActivity : AppCompatActivity() {
312314
}
313315
}
314316

317+
composable(
318+
route = MainRoutes.CloudFilesBackupDetail.route,
319+
arguments = listOf(
320+
navArgument(MainRoutes.ARG_GROUP) { type = NavType.StringType },
321+
navArgument(MainRoutes.ARG_ACCOUNT_NAME) { type = NavType.StringType }
322+
)
323+
) { backStackEntry ->
324+
val groupJsonEncoded = backStackEntry.arguments?.getString(MainRoutes.ARG_GROUP)
325+
val accountName = backStackEntry.arguments?.getString(MainRoutes.ARG_ACCOUNT_NAME) ?: ""
326+
327+
val group = groupJsonEncoded?.let { encodedJson ->
328+
try {
329+
val groupJsonDecoded = URLDecoder.decode(encodedJson, "UTF-8")
330+
val decodedGroup = Json.decodeFromString<ResticFileBackupGroup>(groupJsonDecoded)
331+
decodedGroup
332+
} catch (e: Exception) {
333+
Log.e("MainActivity", "Failed to decode CloudFilesBackupDetail: ${e.message}", e)
334+
null
335+
}
336+
}
337+
338+
group?.let {
339+
CloudFilesBackupDetailPage(navController = navController, group = it, accountName = accountName)
340+
} ?: run {
341+
navController.popBackStack()
342+
}
343+
}
344+
345+
composable(
346+
route = MainRoutes.CloudFilesRestore.route,
347+
arguments = listOf(
348+
navArgument(MainRoutes.ARG_ACCOUNT_NAME) {
349+
type = NavType.StringType
350+
nullable = false
351+
}
352+
)
353+
) { backStackEntry ->
354+
val accountName = backStackEntry.arguments?.getString(MainRoutes.ARG_ACCOUNT_NAME)?.decodeURL() ?: ""
355+
CloudFilesRestorePage(navController = navController, accountName = accountName)
356+
}
357+
315358
composable(MainRoutes.Reload.route) {
316359
PageReload()
317360
}

source/core/restic/src/main/kotlin/com/xayah/core/restic/ResticRepository.kt

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ class ResticRepository @Inject constructor(
312312
val mediaName = parts.dropLast(2).joinToString("-")
313313
val timestamp = parts.last().toLongOrNull() ?: 0L
314314
val dataType = when (parts[parts.size - 2]) {
315-
"media" -> DataType.PACKAGE_MEDIA
316-
"config" -> DataType.PACKAGE_CONFIG
315+
"filesbackup" -> DataType.PACKAGE_MEDIA
316+
"filesconfig" -> DataType.PACKAGE_CONFIG
317317
else -> null
318318
}
319319
if (dataType != null) {
@@ -373,6 +373,111 @@ class ResticRepository @Inject constructor(
373373
return apps
374374
}
375375

376+
suspend fun listBackedUpFilesFromS3(cloudEntity: CloudEntity, password: String): List<ResticBackupFiles> {
377+
Log.d(TAG, "=== listBackedUpFilesFromS3 启动 ===")
378+
Log.d(TAG, "账户名称: ${cloudEntity.name}, 远程路径: ${cloudEntity.remote}")
379+
380+
return try {
381+
// 1. 解析 S3 配置
382+
val extra = json.decodeFromString<S3Extra>(cloudEntity.extra) ?: run {
383+
Log.e(TAG, "错误: 无法解析 S3Extra 配置")
384+
return emptyList()
385+
}
386+
387+
// 2. 构建统一的 S3 URL
388+
val repoUrl = buildS3ResticUrl(extra, cloudEntity.remote)
389+
Log.d(TAG, "生成的完整仓库 URL: $repoUrl")
390+
391+
// 3. 准备环境变量
392+
val env = mutableMapOf(
393+
"AWS_ACCESS_KEY_ID" to extra.accessKeyId,
394+
"AWS_SECRET_ACCESS_KEY" to extra.secretAccessKey,
395+
"RESTIC_PASSWORD" to password
396+
)
397+
if (extra.region.isNotEmpty()) {
398+
env["AWS_DEFAULT_REGION"] = extra.region
399+
}
400+
401+
// 4. 构建命令行参数
402+
val args = mutableListOf(
403+
"snapshots",
404+
"--repo", "\"$repoUrl\"",
405+
"--json",
406+
"-o", "s3.bucket-lookup=dns"
407+
)
408+
409+
if (extra.region.isNotEmpty()) {
410+
args.add("-o")
411+
args.add("s3.region=${extra.region}")
412+
}
413+
414+
Log.d(TAG, "正在执行 restic snapshots...")
415+
val result = executeRestic(*args.toTypedArray(), env = env)
416+
417+
// 5. 处理结果
418+
if (result.isSuccess) {
419+
val jsonStr = result.out
420+
.filter { it.trim().startsWith("[") || it.trim().startsWith("{") }
421+
.joinToString("")
422+
423+
if (jsonStr.isEmpty()) {
424+
Log.w(TAG, "命令成功但未返回任何快照内容 (JSON 为空)")
425+
return emptyList()
426+
}
427+
428+
// 解析快照列表
429+
val snapshots = json.decodeFromString<List<ResticSnapshot>>(jsonStr)
430+
Log.d(TAG, "成功获取 ${snapshots.size} 个快照,开始解析文件标签...")
431+
432+
val files = mutableListOf<ResticBackupFiles>()
433+
snapshots.forEach { snapshot ->
434+
snapshot.tags.forEach { tag ->
435+
val parts = tag.split("-")
436+
// 预期的标签格式: mediaName-timestamp-filesbackup/filesconfig
437+
if (parts.size >= 3) {
438+
try {
439+
val mediaName = parts.dropLast(2).joinToString("-")
440+
val timestamp = parts.last().toLongOrNull() ?: 0L
441+
val dataType = when (parts.last()) {
442+
"filesbackup" -> DataType.PACKAGE_MEDIA
443+
"filesconfig" -> DataType.PACKAGE_CONFIG
444+
else -> null
445+
}
446+
447+
448+
if (dataType != null) {
449+
val fullPath = snapshot.paths.firstOrNull() ?: ""
450+
files.add(
451+
ResticBackupFiles(
452+
mediaName = mediaName,
453+
fullPath = fullPath,
454+
timestamp = timestamp,
455+
dataType = dataType,
456+
snapshotId = snapshot.id,
457+
snapshotTime = snapshot.time,
458+
tags = snapshot.tags
459+
)
460+
)
461+
}
462+
} catch (e: Exception) {
463+
Log.e(TAG, "解析文件标签出错: $tag", e)
464+
}
465+
}
466+
}
467+
}
468+
Log.d(TAG, "最终成功提取出 ${files.size} 个文件备份项")
469+
files
470+
} else {
471+
Log.e(TAG, "Restic 查询失败 (Exit Code: ${result.code})")
472+
Log.e(TAG, "错误输出: ${result.err.joinToString("\n")}")
473+
emptyList()
474+
}
475+
} catch (e: Exception) {
476+
Log.e(TAG, "listBackedUpFilesFromS3 发生严重异常", e)
477+
emptyList()
478+
}
479+
}
480+
376481
/**
377482
* 从 S3 仓库获取备份的应用列表
378483
*/

0 commit comments

Comments
 (0)