Skip to content

Commit 3484a2c

Browse files
author
Fastace
committed
完善本地、云端快照删除逻辑
1 parent 9a15521 commit 3484a2c

7 files changed

Lines changed: 552 additions & 168 deletions

File tree

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,50 @@ class ResticRepository @Inject constructor(
739739
}
740740
}
741741

742+
suspend fun forgetSnapshot(
743+
repoPath: String,
744+
password: String,
745+
snapshotId: String
746+
): Boolean = withContext(Dispatchers.IO) {
747+
try {
748+
val env = mapOf("RUSTIC_PASSWORD" to password)
749+
val args = arrayOf("forget", snapshotId, "-r", repoPath)
750+
val result = executeRestic(*args, env = env, usePty = false)
751+
752+
Log.d(TAG, "Forget snapshot 结果: exitCode=${result.code}")
753+
result.code == 0
754+
} catch (e: Exception) {
755+
Log.e(TAG, "删除快照失败: ${e.message}", e)
756+
false
757+
}
758+
}
759+
760+
suspend fun pruneRepository(
761+
repoPath: String,
762+
password: String
763+
): Boolean = withContext(Dispatchers.IO) {
764+
try {
765+
val env = mapOf("RUSTIC_PASSWORD" to password)
766+
767+
// 本地仓库使用 --max-unused 10% 以节省空间
768+
// 重整速度快,可以接受 repack 开销
769+
val args = arrayOf(
770+
"prune",
771+
"-r", repoPath,
772+
"--max-unused", "10%" // 允许最多 10% 未使用数据
773+
)
774+
775+
Log.d(TAG, "执行本地仓库 prune (10% max-unused 模式)")
776+
val result = executeRestic(*args, env = env, usePty = false)
777+
778+
Log.d(TAG, "Prune 结果: exitCode=${result.code}")
779+
result.code == 0
780+
} catch (e: Exception) {
781+
Log.e(TAG, "Prune 失败: ${e.message}", e)
782+
false
783+
}
784+
}
785+
742786
suspend fun validateRepository(repoPath: String, password: String): Boolean =
743787
executeRestic("check", "-r", repoPath,
744788
env = mapOf("RUSTIC_PASSWORD" to password),

source/feature/main/restore/src/main/kotlin/com/xayah/feature/main/restore/CloudFilesBackupDetailPage.kt

Lines changed: 108 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import com.xayah.core.ui.route.MainRoutes
3838
import com.xayah.core.model.OpType
3939
import com.xayah.core.util.navigateSingle
4040
import com.xayah.core.util.localBackupSaveDir
41+
import com.xayah.core.ui.component.LocalSlotScope
42+
import com.xayah.core.ui.component.confirm
4143
import com.xayah.core.ui.component.BodyMediumText
4244
import com.xayah.core.ui.component.ProgressButton
4345
import com.xayah.core.ui.component.Title
@@ -60,17 +62,32 @@ fun CloudFilesBackupDetailPage(
6062
viewModel: CloudFilesRestoreViewModel = hiltViewModel()
6163
) {
6264
val resticProgress by viewModel.resticProgress.collectAsStateWithLifecycle()
65+
val dialogState = LocalSlotScope.current!!.dialogSlot
66+
val coroutineScope = rememberCoroutineScope()
67+
val context = LocalContext.current
68+
6369
LaunchedEffect(accountName) {
6470
viewModel.setCloudEntity(accountName)
6571
}
72+
73+
// 状态变量
74+
val isDeleting = resticProgress.isDeleting
6675
val isRestoring = resticProgress.totalDataTypes > 0 &&
67-
resticProgress.currentDataTypeIndex < resticProgress.totalDataTypes
68-
val isCompleted = resticProgress.isCompleted
69-
val buttonEnabled = !isRestoring && !isCompleted
70-
val coroutineScope = rememberCoroutineScope()
71-
val context = LocalContext.current
76+
resticProgress.currentDataTypeIndex < resticProgress.totalDataTypes &&
77+
!isDeleting
78+
val isCompleted = resticProgress.isCompleted && !isDeleting
79+
val deleteButtonEnabled = !isRestoring && !isCompleted && !isDeleting
80+
val restoreButtonEnabled = !isRestoring && !isCompleted && !isDeleting
7281

73-
// 计算进度信息
82+
val totalSnapshots = group.backups.size
83+
val totalSteps = totalSnapshots + 1
84+
val currentStep = if (isDeleting) {
85+
resticProgress.currentDataTypeIndex + 1
86+
} else {
87+
0
88+
}
89+
90+
// 进度信息
7491
val currentProgress = if (resticProgress.bytesTotal > 0) {
7592
resticProgress.bytesWritten.toFloat() / resticProgress.bytesTotal
7693
} else 0f
@@ -99,58 +116,97 @@ fun CloudFilesBackupDetailPage(
99116
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
100117
title = "云端文件备份详情",
101118
actions = {
102-
ProgressButton(
119+
Column(
103120
modifier = Modifier.fillMaxWidth(),
104-
progress = currentProgress,
105-
currentIndex = currentIndex,
106-
totalCount = totalCount,
107-
speed = speed,
108-
progressSize = progressSize,
109-
enabled = buttonEnabled,
110-
text = when {
111-
isRestoring -> {
112-
val currentDataType = getCurrentDataTypeName(group, currentIndex)
113-
"正在恢复${currentDataType}快照"
121+
verticalArrangement = Arrangement.spacedBy(SizeTokens.Level8)
122+
) {
123+
// 删除按钮
124+
ProgressButton(
125+
modifier = Modifier.fillMaxWidth(),
126+
progress = 0f,
127+
currentIndex = if (isDeleting) currentStep else 0,
128+
totalCount = if (isDeleting) totalSteps else 0,
129+
speed = "",
130+
progressSize = "",
131+
enabled = deleteButtonEnabled,
132+
text = if (isDeleting) {
133+
if (currentStep <= totalSnapshots) {
134+
val currentDataType = getCurrentDataTypeName(group, currentIndex)
135+
"正在删除${currentDataType}快照 ($currentStep/$totalSteps)"
136+
} else {
137+
"正在清理存储空间 ($totalSteps/$totalSteps)"
138+
}
139+
} else {
140+
"删除云端文件快照"
141+
},
142+
onClick = {
143+
if (!isDeleting) {
144+
coroutineScope.launch {
145+
if (dialogState.confirm(
146+
title = "提示",
147+
text = "确认删除该文件的所有云端快照?\n共计 ${group.backups.size} 个快照"
148+
)) {
149+
val success = viewModel.deleteCloudFileSnapshots(group)
150+
if (success) {
151+
navController.popBackStack()
152+
}
153+
}
154+
}
155+
}
114156
}
115-
isCompleted -> "云端文件恢复已完成"
116-
else -> "恢复云端文件快照"
117-
},
118-
onClick = {
119-
if (!isRestoring && !isCompleted) {
120-
coroutineScope.launch {
121-
try {
122-
Log.d("CloudFilesRestore", "用户点击恢复按钮,开始云端文件恢复流程")
123-
val success = viewModel.restoreFromCloudFileSnapshots(group)
124-
Log.d("CloudFilesRestore", "云端文件恢复结果: $success")
125-
126-
if (success) {
127-
Log.d("CloudFilesRestore", "云端文件恢复成功,准备读取备份目录")
128-
val backupDir = "${context.localBackupSaveDir()}/restore/"
129-
Log.d("CloudFilesRestore", "导航到文件恢复页面,备份目录: $backupDir")
130-
viewModel.refreshLocalDatabase(backupDir)
131-
132-
// 在数据库刷新完成后计算文件大小
133-
viewModel.calculateSizesForActivatedMedia()
134-
val route = MainRoutes.MediumRestoreProcessingGraph.getRoute(
135-
cloudName = URLEncoder.encode("", "UTF-8"), // 与本地恢复一致
136-
backupDir = URLEncoder.encode(backupDir, "UTF-8"),
137-
mediaName = URLEncoder.encode(group.mediaName, "UTF-8")
138-
)
139-
Log.d("Navigation", "构建路由: $route")
140-
navController.navigateSingle(route)
141-
Log.d("Navigation", "导航完成: CloudFilesBackupDetailPage → MediumRestoreProcessingGraph")
142-
} else {
143-
Log.e("CloudFilesRestore", "云端文件恢复失败")
157+
)
158+
159+
// 恢复按钮
160+
ProgressButton(
161+
modifier = Modifier.fillMaxWidth(),
162+
progress = currentProgress,
163+
currentIndex = currentIndex,
164+
totalCount = totalCount,
165+
speed = speed,
166+
progressSize = progressSize,
167+
enabled = restoreButtonEnabled,
168+
text = when {
169+
isRestoring -> {
170+
val currentDataType = getCurrentDataTypeName(group, currentIndex)
171+
"正在恢复${currentDataType}快照"
172+
}
173+
isCompleted -> "云端文件恢复已完成"
174+
else -> "恢复云端文件快照"
175+
},
176+
onClick = {
177+
if (!isRestoring && !isCompleted && !isDeleting) {
178+
coroutineScope.launch {
179+
try {
180+
Log.d("CloudFilesRestore", "用户点击恢复按钮,开始云端文件恢复流程")
181+
val success = viewModel.restoreFromCloudFileSnapshots(group)
182+
Log.d("CloudFilesRestore", "云端文件恢复结果: $success")
183+
184+
if (success) {
185+
Log.d("CloudFilesRestore", "云端文件恢复成功,准备读取备份目录")
186+
val backupDir = "${context.localBackupSaveDir()}/restore/"
187+
Log.d("CloudFilesRestore", "导航到文件恢复页面,备份目录: $backupDir")
188+
viewModel.refreshLocalDatabase(backupDir)
189+
190+
viewModel.calculateSizesForActivatedMedia()
191+
val route = MainRoutes.MediumRestoreProcessingGraph.getRoute(
192+
cloudName = URLEncoder.encode("", "UTF-8"),
193+
backupDir = URLEncoder.encode(backupDir, "UTF-8"),
194+
mediaName = URLEncoder.encode(group.mediaName, "UTF-8")
195+
)
196+
Log.d("Navigation", "构建路由: $route")
197+
navController.navigateSingle(route)
198+
Log.d("Navigation", "导航完成: CloudFilesBackupDetailPage → MediumRestoreProcessingGraph")
199+
} else {
200+
Log.e("CloudFilesRestore", "云端文件恢复失败")
201+
}
202+
} catch (e: Exception) {
203+
Log.e("CloudFilesRestore", "云端文件恢复流程异常: ${e.message}", e)
144204
}
145-
} catch (e: Exception) {
146-
Log.e("CloudFilesRestore", "云端文件恢复流程异常: ${e.message}", e)
147205
}
148206
}
149-
} else {
150-
Log.d("CloudFilesRestore", "云端文件恢复正在进行中,忽略点击")
151207
}
152-
}
153-
)
208+
)
209+
}
154210
}
155211
) {
156212
Column(

source/feature/main/restore/src/main/kotlin/com/xayah/feature/main/restore/CloudFilesRestoreViewModel.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,59 @@ class CloudFilesRestoreViewModel @Inject constructor(
160160
Log.d(TAG, "=== loadCloudBackedUpFiles 结束 ===")
161161
}
162162

163+
suspend fun deleteCloudFileSnapshots(group: ResticFileBackupGroup): Boolean = withContext(Dispatchers.IO) {
164+
try {
165+
val cloudEntity = cloudRepo.queryByName(accountName) ?: return@withContext false
166+
val password = context.readS3ResticPassword() ?: return@withContext false
167+
168+
val sortedBackups = group.backups.sortedBy { backup ->
169+
when (backup.dataType) {
170+
DataType.PACKAGE_MEDIA -> 0
171+
DataType.PACKAGE_CONFIG -> 1
172+
else -> 2
173+
}
174+
}
175+
176+
val totalSteps = sortedBackups.size + 1
177+
178+
_resticProgress.value = ResticProgressState(
179+
totalDataTypes = totalSteps,
180+
currentDataTypeIndex = 0,
181+
isDeleting = true
182+
)
183+
184+
sortedBackups.forEachIndexed { index, backup ->
185+
_resticProgress.value = _resticProgress.value.copy(
186+
currentDataTypeIndex = index
187+
)
188+
189+
val success = resticRepo.forgetSnapshotFromS3(
190+
cloudEntity = cloudEntity,
191+
password = password,
192+
snapshotId = backup.snapshotId
193+
)
194+
195+
if (!success) {
196+
_resticProgress.value = ResticProgressState()
197+
return@withContext false
198+
}
199+
}
200+
201+
_resticProgress.value = _resticProgress.value.copy(
202+
currentDataTypeIndex = sortedBackups.size
203+
)
204+
205+
val pruneSuccess = resticRepo.pruneS3Repository(cloudEntity, password)
206+
_resticProgress.value = ResticProgressState()
207+
208+
pruneSuccess
209+
} catch (e: Exception) {
210+
Log.e(TAG, "删除文件快照异常: ${e.message}", e)
211+
_resticProgress.value = ResticProgressState()
212+
false
213+
}
214+
}
215+
163216
suspend fun restoreFromCloudFileSnapshots(group: ResticFileBackupGroup): Boolean {
164217
Log.d(TAG, "=== restoreFromCloudFileSnapshots 开始 ===")
165218
Log.d(TAG, "恢复组: ${group.mediaName}, 时间戳: ${group.timestamp}, 备份数量: ${group.backups.size}")

0 commit comments

Comments
 (0)