From 60dd52afd1a34c5ead4452c3ccaacc424b2a61b3 Mon Sep 17 00:00:00 2001 From: 5ec1cff <56485584+5ec1cff@users.noreply.github.com> Date: Sun, 14 Apr 2024 00:45:06 +0800 Subject: [PATCH] ksud: backup stock image and use it when restore (#1619) --- .../me/weishu/kernelsu/ui/screen/Flash.kt | 60 +++++- .../me/weishu/kernelsu/ui/screen/Install.kt | 10 +- .../me/weishu/kernelsu/ui/screen/Settings.kt | 12 +- .../java/me/weishu/kernelsu/ui/util/KsuCli.kt | 38 +++- .../src/main/res/values-zh-rCN/strings.xml | 4 + manager/app/src/main/res/values/strings.xml | 4 + userspace/ksud/src/boot_patch.rs | 175 +++++++++++++----- userspace/ksud/src/defs.rs | 3 + 8 files changed, 243 insertions(+), 63 deletions(-) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt index aac12c36..b5028969 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt @@ -12,9 +12,21 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key @@ -37,9 +49,17 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.installBoot import me.weishu.kernelsu.ui.util.installModule import me.weishu.kernelsu.ui.util.reboot +import me.weishu.kernelsu.ui.util.restoreBoot import java.io.File import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale + +enum class FlashingStatus { + FLASHING, + SUCCESS, + FAILED +} /** * @author weishu @@ -57,19 +77,24 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() val scrollState = rememberScrollState() + var flashing by rememberSaveable { + mutableStateOf(FlashingStatus.FLASHING) + } LaunchedEffect(Unit) { if (text.isNotEmpty()) { return@LaunchedEffect } withContext(Dispatchers.IO) { - flashIt(flashIt, onFinish = { showReboot -> + flashIt(flashIt, onFinish = { showReboot, code -> + if (code != 0) { + text += "Error: exit code = $code.\nPlease save and check the log.\n" + } if (showReboot) { - for (i in 0..2) { - text += "\n" - } + text += "\n\n\n" showFloatAction = true } + flashing = if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED }, onStdout = { text += "$it\n" logContent.append(it).append("\n") @@ -82,6 +107,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { Scaffold( topBar = { TopBar( + flashing, onBack = { navigator.popBackStack() }, @@ -146,10 +172,12 @@ sealed class FlashIt : Parcelable { FlashIt() data class FlashModule(val uri: Uri) : FlashIt() + + data object FlashRestore : FlashIt() } fun flashIt( - flashIt: FlashIt, onFinish: (Boolean) -> Unit, + flashIt: FlashIt, onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ) { @@ -164,14 +192,26 @@ fun flashIt( ) is FlashIt.FlashModule -> installModule(flashIt.uri, onFinish, onStdout, onStderr) + + FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr) } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { +private fun TopBar(status: FlashingStatus, onBack: () -> Unit = {}, onSave: () -> Unit = {}) { TopAppBar( - title = { Text(stringResource(R.string.install)) }, + title = { + Text( + stringResource( + when (status) { + FlashingStatus.FLASHING -> R.string.flashing + FlashingStatus.SUCCESS -> R.string.flash_success + FlashingStatus.FAILED -> R.string.flash_failed + } + ) + ) + }, navigationIcon = { IconButton( onClick = onBack diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt index afd06034..5da75e71 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt @@ -123,11 +123,19 @@ fun InstallScreen(navigator: DestinationsNavigator) { installMethod = method } - Row( + Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { + (lkmSelection as? LkmSelection.LkmUri)?.let { + Text( + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) + ) + } Button(modifier = Modifier.fillMaxWidth(), enabled = installMethod != null, onClick = { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt index 8cbd839f..9986a880 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt @@ -63,6 +63,7 @@ import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rememberCustomDialog import me.weishu.kernelsu.ui.component.rememberLoadingDialog import me.weishu.kernelsu.ui.screen.destinations.AppProfileTemplateScreenDestination +import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination import me.weishu.kernelsu.ui.util.getBugreportFile import me.weishu.kernelsu.ui.util.shrinkModules @@ -214,7 +215,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode if (lkmMode) { - UninstallItem { + UninstallItem(navigator) { loadingDialog.withLoading(it) } } @@ -237,7 +238,10 @@ fun SettingScreen(navigator: DestinationsNavigator) { } @Composable -fun UninstallItem(withLoading: suspend (suspend () -> Unit) -> Unit) { +fun UninstallItem( + navigator: DestinationsNavigator, + withLoading: suspend (suspend () -> Unit) -> Unit +) { val context = LocalContext.current val scope = rememberCoroutineScope() val uninstallConfirmDialog = rememberConfirmDialog() @@ -255,7 +259,9 @@ fun UninstallItem(withLoading: suspend (suspend () -> Unit) -> Unit) { when (uninstallType) { UninstallType.TEMPORARY -> showTodo() UninstallType.PERMANENT -> showTodo() - UninstallType.RESTORE_STOCK_IMAGE -> showTodo() + UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate( + FlashScreenDestination(FlashIt.FlashRestore) + ) UninstallType.NONE -> Unit } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt index d90e8068..bdb907d2 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt @@ -106,7 +106,10 @@ fun uninstallModule(id: String): Boolean { } fun installModule( - uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit + uri: Uri, + onFinish: (Boolean, Int) -> Unit, + onStdout: (String) -> Unit, + onStderr: (String) -> Unit ): Boolean { val resolver = ksuApp.contentResolver with(resolver.openInputStream(uri)) { @@ -137,11 +140,38 @@ fun installModule( file.delete() - onFinish(result.isSuccess) + onFinish(result.isSuccess, result.code) return result.isSuccess } } +fun restoreBoot( + onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit +): Boolean { + val shell = createRootShell() + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") + + val stdoutCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStdout(s ?: "") + } + } + + val stderrCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStderr(s ?: "") + } + } + + val result = + shell.newJob().add("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot") + .to(stdoutCallback, stderrCallback) + .exec() + + onFinish(result.isSuccess, result.code) + return result.isSuccess +} + suspend fun shrinkModules(): Boolean = withContext(Dispatchers.IO) { execKsud("module shrink", true) } @@ -157,7 +187,7 @@ fun installBoot( bootUri: Uri?, lkm: LkmSelection, ota: Boolean, - onFinish: (Boolean) -> Unit, + onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit, ): Boolean { @@ -238,7 +268,7 @@ fun installBoot( lkmFile?.delete() // if boot uri is empty, it is direct install, when success, we should show reboot button - onFinish(bootUri == null && result.isSuccess) + onFinish(bootUri == null && result.isSuccess, result.code) return result.isSuccess } diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 3e1a8814..6c4df229 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -124,4 +124,8 @@ 临时卸载 KernelSU,下次重启后恢复 完全并永久移除 KernelSU 和所有模块 恢复原厂镜像,一般在 OTA 前使用;如需卸载请使用“永久卸载” + 刷写中 + 刷写完成 + 刷写失败 + 选择的 LKM :%s \ No newline at end of file diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 91299724..7202b4e2 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -126,4 +126,8 @@ Temporarily uninstall KernelSU, restore to original state after next reboot. Uninstalling KernelSU(Root and all modules) completely and permanently. Restore the stock factory image (if a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Permanent Uninstall\". + Flashing + Flash success + Flash failed + Selected lkm: %s \ No newline at end of file diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index ac4feee8..651ad6d4 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -1,17 +1,18 @@ #[cfg(unix)] use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::process::Stdio; use anyhow::anyhow; use anyhow::bail; use anyhow::ensure; use anyhow::Context; use anyhow::Result; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; -use std::process::Stdio; use which::which; +use crate::defs::{KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX}; use crate::{assets, utils}; #[cfg(target_os = "android")] @@ -152,8 +153,7 @@ pub fn restore( magiskboot_path: Option, flash: bool, ) -> Result<()> { - let workding_dir = - tempdir::TempDir::new("KernelSU").with_context(|| "create temp dir failed")?; + let workding_dir = tempdir::TempDir::new("KernelSU").context("create temp dir failed")?; let magiskboot = find_magiskboot(magiskboot_path, workding_dir.path())?; let (bootimage, bootdevice) = find_boot_image(&image, false, false, workding_dir.path())?; @@ -171,29 +171,57 @@ pub fn restore( let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workding_dir.path())?; ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU"); - // remove kernelsu.ko - do_cpio_cmd(&magiskboot, workding_dir.path(), "rm kernelsu.ko")?; + let mut new_boot = None; + let mut from_backup = false; - // if init.real is exist, restore it - let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init.real").is_ok(); - if status { - do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init.real init")?; + #[cfg(target_os = "android")] + if do_cpio_cmd(&magiskboot, workding_dir.path(), "exists orig.ksu").is_ok() { + do_cpio_cmd( + &magiskboot, + workding_dir.path(), + "extract orig.ksu orig.ksu", + )?; + let sha = std::fs::read(workding_dir.path().join("orig.ksu"))?; + let sha = String::from_utf8(sha)?; + let sha = sha.trim(); + let backup_path = format!("{KSU_BACKUP_DIR}/{KSU_BACKUP_FILE_PREFIX}{sha}"); + if Path::new(&backup_path).is_file() { + new_boot = Some(PathBuf::from(backup_path)); + from_backup = true; + } else { + println!("Warning: no backup {KSU_BACKUP_DIR}/{KSU_BACKUP_FILE_PREFIX}{sha} found!"); + } } else { - let ramdisk = workding_dir.path().join("ramdisk.cpio"); - std::fs::remove_file(ramdisk)?; + println!("Warning: no backup found!"); } - println!("- Repacking boot image"); - let status = Command::new(&magiskboot) - .current_dir(workding_dir.path()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .arg("repack") - .arg(bootimage.display().to_string()) - .status()?; - ensure!(status.success(), "magiskboot repack failed"); + if new_boot.is_none() { + // remove kernelsu.ko + do_cpio_cmd(&magiskboot, workding_dir.path(), "rm kernelsu.ko")?; + + // if init.real exists, restore it + let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init.real").is_ok(); + if status { + do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init.real init")?; + } else { + let ramdisk = workding_dir.path().join("ramdisk.cpio"); + std::fs::remove_file(ramdisk)?; + } + + println!("- Repacking boot image"); + let status = Command::new(&magiskboot) + .current_dir(workding_dir.path()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("repack") + .arg(bootimage.display().to_string()) + .status()?; + ensure!(status.success(), "magiskboot repack failed"); + new_boot = Some(workding_dir.path().join("new-boot.img")); + } + + let new_boot = new_boot.unwrap(); - let new_boot = workding_dir.path().join("new-boot.img"); if image.is_some() { // if image is specified, write to output file let output_dir = std::env::current_dir()?; @@ -203,16 +231,19 @@ pub fn restore( now.format("%Y%m%d_%H%M%S") )); - if std::fs::rename(&new_boot, &output_image).is_err() { - std::fs::copy(&new_boot, &output_image) - .with_context(|| "copy out new boot failed".to_string())?; + if from_backup || std::fs::rename(&new_boot, &output_image).is_err() { + std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?; } println!("- Output file is written to"); println!("- {}", output_image.display().to_string().trim_matches('"')); } if flash { - println!("- Flashing new boot image"); - flash_boot(bootdevice, new_boot)?; + if from_backup { + println!("- Flashing new boot image from {}", new_boot.display()); + } else { + println!("- Flashing new boot image"); + } + flash_boot(&bootdevice, new_boot)?; } println!("- Done!"); Ok(()) @@ -265,12 +296,13 @@ fn do_patch( ); } - let workding_dir = - tempdir::TempDir::new("KernelSU").with_context(|| "create temp dir failed")?; + let workding_dir = tempdir::TempDir::new("KernelSU").context("create temp dir failed")?; let (bootimage, bootdevice) = find_boot_image(&image, ota, is_replace_kernel, workding_dir.path())?; + let bootimage = bootimage.display().to_string(); + // try extract magiskboot/bootctl let _ = assets::ensure_binaries(false); @@ -279,20 +311,20 @@ fn do_patch( if let Some(kernel) = kernel { std::fs::copy(kernel, workding_dir.path().join("kernel")) - .with_context(|| "copy kernel from failed".to_string())?; + .context("copy kernel from failed")?; } println!("- Preparing assets"); let kmod_file = workding_dir.path().join("kernelsu.ko"); if let Some(kmod) = kmod { - std::fs::copy(kmod, kmod_file).with_context(|| "copy kernel module failed".to_string())?; + std::fs::copy(kmod, kmod_file).context("copy kernel module failed")?; } else { // If kmod is not specified, extract from assets let kmi = if let Some(kmi) = kmi { kmi } else { - get_current_kmi().with_context(|| "Unknown KMI, please choose LKM manually")? + get_current_kmi().context("Unknown KMI, please choose LKM manually")? }; println!("- KMI: {kmi}"); let name = format!("{kmi}_kernelsu.ko"); @@ -302,9 +334,9 @@ fn do_patch( let init_file = workding_dir.path().join("init"); if let Some(init) = init { - std::fs::copy(init, init_file).with_context(|| "copy init failed".to_string())?; + std::fs::copy(init, init_file).context("copy init failed")?; } else { - assets::copy_assets_to_file("ksuinit", init_file).with_context(|| "copy ksuinit failed")?; + assets::copy_assets_to_file("ksuinit", init_file).context("copy ksuinit failed")?; } // magiskboot unpack boot.img @@ -318,7 +350,7 @@ fn do_patch( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("unpack") - .arg(bootimage.display().to_string()) + .arg(&bootimage) .status()?; ensure!(status.success(), "magiskboot unpack failed"); @@ -331,12 +363,45 @@ fn do_patch( println!("- Adding KernelSU LKM"); let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workding_dir.path())?; + + #[cfg(target_os = "android")] + let mut backup = None; if !is_kernelsu_patched { // kernelsu.ko is not exist, backup init if necessary let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init"); if status.is_ok() { do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init init.real")?; } + println!("- Backup stock boot image"); + // magiskboot cpio ramdisk.cpio 'add 0755 orig.ksu' + let output = Command::new(&magiskboot) + .current_dir(workding_dir.path()) + .arg("sha1") + .arg(&bootimage) + .output()?; + ensure!( + output.status.success(), + "Cannot calculate sha1 of original boot!" + ); + + #[cfg(target_os = "android")] + { + let output = String::from_utf8(output.stdout)?; + let output = output.trim(); + let output = format!("{KSU_BACKUP_FILE_PREFIX}{output}"); + let target = format!("{KSU_BACKUP_DIR}/{output}"); + std::fs::copy(&bootimage, &target).with_context(|| format!("backup to {target}"))?; + std::fs::write(workding_dir.path().join("orig.ksu"), output.as_bytes()) + .context("write sha1")?; + do_cpio_cmd( + &magiskboot, + workding_dir.path(), + "add 0755 orig.ksu orig.ksu", + )?; + println!("- Stock image has been backup to"); + println!("- {target}"); + backup = Some(output); + } } do_cpio_cmd(&magiskboot, workding_dir.path(), "add 0755 init init")?; @@ -353,7 +418,7 @@ fn do_patch( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") - .arg(bootimage.display().to_string()) + .arg(&bootimage) .status()?; ensure!(status.success(), "magiskboot repack failed"); let new_boot = workding_dir.path().join("new-boot.img"); @@ -368,8 +433,7 @@ fn do_patch( )); if std::fs::rename(&new_boot, &output_image).is_err() { - std::fs::copy(&new_boot, &output_image) - .with_context(|| "copy out new boot failed".to_string())?; + std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?; } println!("- Output file is written to"); println!("- {}", output_image.display().to_string().trim_matches('"')); @@ -377,27 +441,48 @@ fn do_patch( if flash { println!("- Flashing new boot image"); - flash_boot(bootdevice, new_boot)?; + flash_boot(&bootdevice, new_boot)?; if ota { post_ota()?; } } + #[cfg(target_os = "android")] + if let Some(backup) = backup { + println!("- Clean up backup"); + if let Ok(dir) = std::fs::read_dir("/data") { + for entry in dir.flatten() { + let path = entry.path(); + if path.is_file() { + if let Some(name) = path.file_name() { + let name = name.to_string_lossy().to_string(); + if name != backup + && name.starts_with(KSU_BACKUP_FILE_PREFIX) + && std::fs::remove_file(path).is_ok() + { + println!("- removed {name}"); + } + } + } + } + } + } + println!("- Done!"); Ok(()) } -fn flash_boot(bootdevice: Option, new_boot: PathBuf) -> Result<()> { +fn flash_boot(bootdevice: &Option, new_boot: PathBuf) -> Result<()> { let Some(bootdevice) = bootdevice else { bail!("boot device not found") }; let status = Command::new("blockdev") .arg("--setrw") - .arg(&bootdevice) + .arg(bootdevice) .status()?; ensure!(status.success(), "set boot device rw failed"); - dd(new_boot, &bootdevice).with_context(|| "flash boot failed")?; + dd(new_boot, bootdevice).context("flash boot failed")?; Ok(()) } @@ -413,7 +498,7 @@ fn find_magiskboot(magiskboot_path: Option, workding_dir: &Path) -> Res } else { let magiskboot_path = workding_dir.join("magiskboot"); assets::copy_assets_to_file("magiskboot", &magiskboot_path) - .with_context(|| "copy magiskboot failed")?; + .context("copy magiskboot failed")?; magiskboot_path }; ensure!(magiskboot.exists(), "{magiskboot:?} is not exist"); diff --git a/userspace/ksud/src/defs.rs b/userspace/ksud/src/defs.rs index 9508f562..d459dbd4 100644 --- a/userspace/ksud/src/defs.rs +++ b/userspace/ksud/src/defs.rs @@ -38,3 +38,6 @@ pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE")); pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME")); + +pub const KSU_BACKUP_DIR: &str = "/data"; +pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_";