fix: 外部存储路径 EPERM 时通过 root shell 回落写入

新增 mkdirsForBackup / writeFileForBackup 辅助函数:
- 优先尝试 Java File API(内部存储直写)
- 失败后回退到 root shell mkdir -p / cp(绕过 FUSE UID 检查)
- 临时文件写入 /data/local/tmp 后用 root cp 拷贝到目标路径
- 替换 backupApps / backupSsaid / backupPermissions 中所有 writeText 调用
This commit is contained in:
sakuradairong
2026-06-08 14:37:07 +08:00
parent 1fdba019d7
commit 64ded465e6
2 changed files with 59 additions and 18 deletions

View File

@@ -72,7 +72,7 @@ object BackupOperation {
// Create backup structure
val backupRoot = File(outputDir, "Backup_${config.compressionMethod}_$userId")
if (!backupRoot.mkdirs() && !backupRoot.isDirectory) {
if (!mkdirsForBackup(backupRoot)) {
LogUtil.e(TAG, "backupApps: cannot create output dir ${backupRoot.absolutePath}")
return@withContext BackupResult(0, 0, 0, outputDir.absolutePath, 0)
}
@@ -80,19 +80,15 @@ object BackupOperation {
// Write app list — includes ALL packages in [apps] (selected + legacy from snapshot)
val appListFile = File(backupRoot, "appList.txt")
try {
appListFile.writeText(apps.joinToString("\n") { it.packageName.value })
} catch (e: Exception) {
LogUtil.e(TAG, "backupApps: failed to write appList.txt — ${e.message}")
if (!writeFileForBackup(appListFile, apps.joinToString("\n") { it.packageName.value })) {
LogUtil.e(TAG, "backupApps: failed to write appList.txt")
return@withContext BackupResult(0, 0, 0, outputDir.absolutePath, 0)
}
// Write metadata JSON — fresh metadata for selected apps, legacy for historical apps
val metaFile = File(backupRoot, "app_details.json")
try {
metaFile.writeText(buildAppDetailsJson(apps, legacyApps))
} catch (e: Exception) {
LogUtil.e(TAG, "backupApps: failed to write app_details.json — ${e.message}")
if (!writeFileForBackup(metaFile, buildAppDetailsJson(apps, legacyApps))) {
LogUtil.e(TAG, "backupApps: failed to write app_details.json")
return@withContext BackupResult(0, 0, 0, outputDir.absolutePath, 0)
}
@@ -354,11 +350,11 @@ object BackupOperation {
?.substringBefore("\"")
?.takeIf { it.isNotBlank() }
if (value != null) {
try {
File(appDir, "ssaid.txt").writeText(value)
val ssaidFile = File(appDir, "ssaid.txt")
if (!writeFileForBackup(ssaidFile, value)) {
Log.w(TAG, "backupSsaid: failed to write ssaid.txt for $packageName")
} else {
Log.d(TAG, "backupSsaid: backed up SSAID for $packageName = $value")
} catch (e: Exception) {
Log.w(TAG, "backupSsaid: failed to write ssaid.txt for $packageName", e)
}
}
}
@@ -366,10 +362,9 @@ object BackupOperation {
private suspend fun backupPermissions(packageName: String, appDir: File) {
val result = RootShell.exec("dumpsys package '${packageName.shellEscape()}' | grep -E 'granted=(true|false)'")
if (result.output.isNotBlank()) {
try {
File(appDir, "permissions.txt").writeText(result.output)
} catch (e: Exception) {
Log.w(TAG, "backupPermissions: failed to write permissions.txt for $packageName", e)
val permFile = File(appDir, "permissions.txt")
if (!writeFileForBackup(permFile, result.output)) {
Log.w(TAG, "backupPermissions: failed to write permissions.txt for $packageName")
}
}
}
@@ -406,4 +401,50 @@ object BackupOperation {
}
return root.toString(2)
}
}
/**
* Create backup output directory, falling back to root shell [mkdir -p]
* when Java [File.mkdirs] fails (e.g. EPERM on FUSE external storage).
*/
private suspend fun mkdirsForBackup(dir: File): Boolean {
if (dir.isDirectory) return true
if (dir.mkdirs()) return true
// Fallback: root shell bypasses FUSE/sdcardfs restrictions
val result = RootShell.exec("mkdir -p '${dir.absolutePath.shellEscape()}'")
return result.isSuccess && dir.isDirectory
}
/**
* Write text content to a backup file, falling back to root shell
* when [File.writeText] fails (e.g. EPERM on FUSE external storage).
*
* Fallback strategy: write to world-writable tmp → root [cp] to target,
* bypassing FUSE UID checks that block the app's Java process.
*/
private suspend fun writeFileForBackup(file: File, text: String): Boolean {
// Try direct File API first (works for internal storage)
try {
mkdirsForBackup(file.parentFile ?: return false)
file.writeText(text)
return true
} catch (_: Exception) {
// Fall through to root-shell fallback
}
// Fallback: write to /data/local/tmp (world-writable) then root cp to target
try {
val tmpFile = File("/data/local/tmp/backup_write_${file.name}")
tmpFile.parentFile?.mkdirs()
tmpFile.writeText(text)
val cpResult = RootShell.exec(
"cp '${tmpFile.absolutePath}' '${file.absolutePath.shellEscape()}'"
)
tmpFile.delete()
if (!cpResult.isSuccess) return false
RootShell.exec("chmod 644 '${file.absolutePath.shellEscape()}'")
return true
} catch (e: Exception) {
Log.w(TAG, "writeFileForBackup: all methods failed for ${file.absolutePath}", e)
return false
}
}
}