From a8f02b1f332d5f2098a5fe31b6a2d55eb9a8eb11 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Tue, 30 Jun 2026 01:40:17 +0530 Subject: [PATCH 1/2] cli ignores unknown keys when parsing github manifest. This solves the unknown error key that gets thrown --- src/main/kotlin/app/morphe/cli/command/CliHttpClient.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/app/morphe/cli/command/CliHttpClient.kt b/src/main/kotlin/app/morphe/cli/command/CliHttpClient.kt index 80cf781..65a8ddc 100644 --- a/src/main/kotlin/app/morphe/cli/command/CliHttpClient.kt +++ b/src/main/kotlin/app/morphe/cli/command/CliHttpClient.kt @@ -9,6 +9,7 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json /** * Lazy initialized HttpClient for CLI commands. One client per process is fine for short-lived @@ -19,7 +20,9 @@ import io.ktor.serialization.kotlinx.json.json object CliHttpClient { val instance: HttpClient by lazy { HttpClient(CIO) { - install(ContentNegotiation) { json() } + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } } } } From 62195d6fc9d8539c6065c928c9581e30acb19495 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:09:24 +0530 Subject: [PATCH 2/2] feat: unify CLI/GUI patch cache + add clear-cache + make purge opt-out - purge is now out-out instead of opt-out. Flag renamed to `disable-purge` - cli and gui now share a shared folder for patch files. - new command under utility: clear-cache. Used in cli to `clear-cache`. `--info` runs this in verbose mode. - Removed `-t`/`--temporary-files-path` from list-patches, list-versions, and options-create. It no longer affects anything there now since downloads go to the shared cache folder. It only stays on `patch`. --- .../cli/command/ListCompatibleVersions.kt | 10 --- .../morphe/cli/command/ListPatchesCommand.kt | 10 --- .../app/morphe/cli/command/OptionsCommand.kt | 10 --- .../app/morphe/cli/command/PatchCommand.kt | 16 ++--- .../morphe/cli/command/PatchFileResolver.kt | 43 ++++-------- .../cli/command/utility/ClearCacheCommand.kt | 64 +++++++++++++++++ .../cli/command/utility/UtilityCommand.kt | 2 +- .../kotlin/app/morphe/engine/CacheManager.kt | 68 +++++++++++++++++++ .../app/morphe/engine/patches/PatchCache.kt | 37 ++++++++++ .../gui/data/repository/PatchRepository.kt | 19 ++---- .../morphe/gui/ui/components/ToolsDialog.kt | 29 ++------ 11 files changed, 206 insertions(+), 102 deletions(-) create mode 100644 src/main/kotlin/app/morphe/cli/command/utility/ClearCacheCommand.kt create mode 100644 src/main/kotlin/app/morphe/engine/CacheManager.kt create mode 100644 src/main/kotlin/app/morphe/engine/patches/PatchCache.kt diff --git a/src/main/kotlin/app/morphe/cli/command/ListCompatibleVersions.kt b/src/main/kotlin/app/morphe/cli/command/ListCompatibleVersions.kt index a1978f5..7fa88ce 100644 --- a/src/main/kotlin/app/morphe/cli/command/ListCompatibleVersions.kt +++ b/src/main/kotlin/app/morphe/cli/command/ListCompatibleVersions.kt @@ -6,7 +6,6 @@ package app.morphe.cli.command import app.morphe.engine.CompatibleVersionsMap -import app.morphe.engine.MorpheData import app.morphe.engine.VersionMap import app.morphe.engine.mostCommonCompatibleVersions import app.morphe.patcher.patch.loadPatchesFromJar @@ -68,12 +67,6 @@ internal class ListCompatibleVersions : Runnable { ) private var includeExperimental: Boolean = false - @Option( - names = ["-t", "--temporary-files-path"], - description = ["Path to store temporary files."], - ) - private var temporaryFilesPath: File? = null - @Spec private lateinit var spec: CommandSpec @@ -96,13 +89,10 @@ internal class ListCompatibleVersions : Runnable { appendLine(versions.buildVersionsString().prependIndent("\t")) } - val temporaryFilesPath = temporaryFilesPath ?: MorpheData.tmpDir - try { patchesFiles = PatchFileResolver.resolve( patchesFiles, prerelease, - temporaryFilesPath, CliHttpClient.instance ) } catch (e: IllegalArgumentException) { diff --git a/src/main/kotlin/app/morphe/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/morphe/cli/command/ListPatchesCommand.kt index 71ca16f..7eeca9a 100644 --- a/src/main/kotlin/app/morphe/cli/command/ListPatchesCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/ListPatchesCommand.kt @@ -8,7 +8,6 @@ package app.morphe.cli.command -import app.morphe.engine.MorpheData import app.morphe.engine.compatibleVersionsForDisplay import app.morphe.engine.isCompatibleWith import app.morphe.patcher.patch.Patch @@ -63,12 +62,6 @@ internal object ListPatchesCommand : Runnable { ) private var withDescriptions: Boolean = true - @Option( - names = ["-t", "--temporary-files-path"], - description = ["Path to store temporary files."], - ) - private var temporaryFilesPath: File? = null - @Option( names = ["-p", "--with-packages"], description = ["List the packages the patches are compatible with."], @@ -190,13 +183,10 @@ internal object ListPatchesCommand : Runnable { ) - val temporaryFilesPath = temporaryFilesPath ?: MorpheData.tmpDir - try { patchesFiles = PatchFileResolver.resolve( patchesFiles, prerelease, - temporaryFilesPath, CliHttpClient.instance ) } catch (e: IllegalArgumentException) { diff --git a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt index 9eccea8..1e02773 100644 --- a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt @@ -6,7 +6,6 @@ package app.morphe.cli.command import app.morphe.cli.command.model.PatchBundle -import app.morphe.engine.MorpheData import app.morphe.engine.patches.LoadedBundle import app.morphe.engine.patches.PatchBundleLoader import app.morphe.cli.command.model.findMatchingBundle @@ -63,12 +62,6 @@ internal object OptionsCommand : Callable { ) private var prerelease: Boolean = false - @Option( - names = ["-t", "--temporary-files-path"], - description = ["Path to store temporary files."], - ) - private var temporaryFilesPath: File? = null - @Option( names = ["-f", "--filter-package-name"], description = ["Filter patches by compatible package name."], @@ -78,15 +71,12 @@ internal object OptionsCommand : Callable { private val json = Json { prettyPrint = true } override fun call(): Int { - val temporaryFilesPath = temporaryFilesPath ?: MorpheData.tmpDir - try { // Since we could have many URLs, we resolve each of them separately patchesFiles = patchesFiles.map { file -> val resolved = PatchFileResolver.resolve( setOf(file), prerelease, - temporaryFilesPath, CliHttpClient.instance ) resolved.single() diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 7ffbb8a..5b9033c 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -237,12 +237,13 @@ internal object PatchCommand : Callable { private var aaptBinaryPath: File? = null @CommandLine.Option( - names = ["--purge"], - description = ["Delete THIS run's scratch files after patching. " + - "Does not affect cached patches, other sessions, or config."], + names = ["--disable-purge"], + description = ["Keep THIS run's scratch files instead of deleting them after patching. " + + "By default the scratch files are purged once patching finishes; this keeps them " + + "(e.g. for debugging a failed patch). Does not affect cached patches, other sessions, or config."], showDefaultValue = ALWAYS, ) - private var purge: Boolean = false + private var disablePurge: Boolean = false @CommandLine.Parameters( description = ["APK file to patch."], @@ -511,7 +512,6 @@ internal object PatchCommand : Callable { val resolved = PatchFileResolver.resolve( setOf(bundle.patchesFile), prerelease, - temporaryFilesPath, CliHttpClient.instance ) bundle.patchesFile = resolved.single() @@ -524,7 +524,7 @@ internal object PatchCommand : Callable { } // Per-session scratch dir. Hoisted out of the patching `try` block so - // the `finally` block can reference it for --purge scope (Phase 6). + // the `finally` block can reference it for the auto-purge (unless --disable-purge). // Naming matches the GUI's FileUtils.createPatchingTempDir() so the // tmp/ folder shows consistent siblings across CLI + GUI sessions. val patcherTemporaryFilesPath = @@ -617,7 +617,7 @@ internal object PatchCommand : Callable { // endregion // (patcherTemporaryFilesPath is declared above the outer try - // block so it's visible to --purge in the finally clause.) + // block so it's visible to the auto-purge in the finally clause.) // We need to check for apkm (like reddit), xapk and apks formats here @@ -976,7 +976,7 @@ internal object PatchCommand : Callable { } } - if (purge) { + if (!disablePurge) { // Scope: only THIS session's tmp subfolder. Cached patches, // logs, config, and other in-flight sessions (CLI or GUI) are // never touched. diff --git a/src/main/kotlin/app/morphe/cli/command/PatchFileResolver.kt b/src/main/kotlin/app/morphe/cli/command/PatchFileResolver.kt index 8266372..5af03a6 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchFileResolver.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchFileResolver.kt @@ -5,6 +5,7 @@ package app.morphe.cli.command +import app.morphe.engine.patches.PatchCache import app.morphe.engine.patches.RemotePatchSourceFactory import app.morphe.engine.patches.findPatchAsset import io.ktor.client.HttpClient @@ -21,12 +22,12 @@ object PatchFileResolver { * Returns a new Set with URLs replaced by downloaded/cached .mpp files. * * Provider detection (GitHub vs GitLab) + URL parsing + API talk lives in the engine. - * This function only owns the CLI's disk cache layout and the "which release do we pick" decision. + * This function owns the "which release do we pick" decision. The on-disk cache + * layout is shared with the GUI via [PatchCache] (one cache for both surfaces). */ fun resolve( patchFiles: Set, prerelease: Boolean, - cacheDir: File, httpClient: HttpClient ): Set { val urlEntry = patchFiles.firstOrNull { @@ -72,37 +73,21 @@ object PatchFileResolver { val asset = targetRelease.findPatchAsset() ?: throw IllegalArgumentException("No .mpp file found in release ${targetRelease.tagName}") - // Disk-cache check (same layout as before: {cacheDir}/download/{owner}-{repo}/). - val versionNumber = targetRelease.tagName.removePrefix("v") - val repoCacheDir = - cacheDir.resolve("download").resolve(parsed.repoPath.replace("/", "-")) + // Shared on-disk cache (via PatchCache): the same directory and tag-prefixed filename + // the GUI uses, so a .mpp downloaded by either side is reused by the other. + // Layout: morphe-data/patches/-/__.mpp + val repoCacheDir = PatchCache.sourceDir(parsed.repoPath) + val targetFile = File(repoCacheDir, PatchCache.cachedFileName(targetRelease, asset)) - val cachedFile = repoCacheDir.listFiles()?.find { - it.name.endsWith(".mpp") && it.name.contains(versionNumber) - } - - val resolvedFile = if (cachedFile != null) { - val rel = cachedFile.relativeTo(cacheDir.parentFile).path - logger.info("Using cached patch file at $rel") - - cachedFile + val resolvedFile = if (targetFile.exists() && targetFile.length() > 0L) { + logger.info("Using cached patch file at ${targetFile.absolutePath}") + targetFile } else { - // Different version cached -> wipe it before downloading (matches the existing behavior) - repoCacheDir.listFiles() - ?.filter { it.name.endsWith(".mpp") } - ?.forEach{ it.delete() } - repoCacheDir.mkdirs() - - val targetFile = File(repoCacheDir, asset.name) - logger.info("Downloading patches from ${parsed.repoPath} $versionNumber...") - + logger.info("Downloading patches from ${parsed.repoPath} ${targetRelease.tagName}...") source.downloadAsset(asset, targetFile).getOrThrow() - - val rel = targetFile.relativeTo(cacheDir.parentFile).path - logger.info("Patches mpp saved to $rel. This file will be used on your next run as long as it is not deleted!") - + logger.info("Patches mpp saved to ${targetFile.absolutePath}. This file will be used on your next run as long as it is not deleted!") targetFile - } + } patchFiles - urlEntry + resolvedFile } catch (e: Exception) { throw IllegalArgumentException("Failed to download patches from URL: ${e.message}") diff --git a/src/main/kotlin/app/morphe/cli/command/utility/ClearCacheCommand.kt b/src/main/kotlin/app/morphe/cli/command/utility/ClearCacheCommand.kt new file mode 100644 index 0000000..67c0fdf --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/utility/ClearCacheCommand.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-desktop + */ + +package app.morphe.cli.command.utility + +import app.morphe.engine.CacheManager +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import java.util.concurrent.Callable +import java.util.logging.Logger + +@Command( + name = "clear-cache", + description = ["Delete cached patch files, logs, and temporary files."], +) +internal object ClearCacheCommand : Callable { + private val logger = Logger.getLogger(this::class.java.name) + + private const val EXIT_CODE_SUCCESS = 0 + private const val EXIT_CODE_ERROR = 1 + + @Option( + names = ["--info"], + description = ["Show a per-category breakdown of what was cleared and how much space was freed."], + ) + private var info: Boolean = false + + override fun call(): Int { + val result = CacheManager.clearCaches() + + if (info) { + logger.info( + buildString { + appendLine("Cache cleared:") + result.perDirectory.forEach { dir -> + appendLine( + " ${dir.label.padEnd(8)} ${humanReadable(dir.bytesFreed).padStart(9)}" + + " (${dir.filesDeleted} file${if (dir.filesDeleted == 1) "" else "s"})" + ) + } + append(" ${"Total".padEnd(8)} ${humanReadable(result.bytesFreed).padStart(9)}") + } + ) + } else { + logger.info("Cache cleared.") + } + + // Deletion failures always shown, even without --info. + return if (result.success) { + EXIT_CODE_SUCCESS + } else { + logger.warning("${result.failedFiles} file(s) could not be deleted (may be locked)") + EXIT_CODE_ERROR + } + } + + private fun humanReadable(bytes: Long): String = when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) + else -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) + } +} diff --git a/src/main/kotlin/app/morphe/cli/command/utility/UtilityCommand.kt b/src/main/kotlin/app/morphe/cli/command/utility/UtilityCommand.kt index 1ef5520..1194e4a 100644 --- a/src/main/kotlin/app/morphe/cli/command/utility/UtilityCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/utility/UtilityCommand.kt @@ -10,6 +10,6 @@ import picocli.CommandLine @CommandLine.Command( name = "utility", description = ["Commands for utility purposes."], - subcommands = [InstallCommand::class, UninstallCommand::class], + subcommands = [InstallCommand::class, UninstallCommand::class, ClearCacheCommand::class], ) internal object UtilityCommand diff --git a/src/main/kotlin/app/morphe/engine/CacheManager.kt b/src/main/kotlin/app/morphe/engine/CacheManager.kt new file mode 100644 index 0000000..5015d60 --- /dev/null +++ b/src/main/kotlin/app/morphe/engine/CacheManager.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-desktop + */ + +package app.morphe.engine + +import java.io.File + +/** + * Clears Morphe's on-disk caches. Shared by the GUI's "Clear Cache" action and + * the CLI's `utility clear-cache` subcommand so both wipe the same set of + * directories: downloaded patches, logs, and temp scratch. + */ +object CacheManager { + + /** What was cleared from a single directory. */ + data class DirResult( + val label: String, + val bytesFreed: Long, + val filesDeleted: Int, + val failedFiles: Int, + ) + + /** Outcome of a [clearCaches] run, with a per-directory breakdown. */ + data class ClearResult( + val perDirectory: List, + ) { + val bytesFreed: Long get() = perDirectory.sumOf { it.bytesFreed } + val failedFiles: Int get() = perDirectory.sumOf { it.failedFiles } + val success: Boolean get() = failedFiles == 0 + } + + /** + * Deletes the contents of the patches cache, logs, and temp scratch + * directories (keeping the top-level directories themselves). + * + * Best-effort: a file that can't be deleted (e.g. a log still held open by + * the running process) is counted in [DirResult.failedFiles] rather than + * aborting the rest. + */ + fun clearCaches(): ClearResult = ClearResult( + listOf( + clearContents("Patches", MorpheData.patchesDir), + clearContents("Logs", MorpheData.logsDir), + clearContents("Temp", MorpheData.tmpDir), + ), + ) + + private fun clearContents(label: String, dir: File): DirResult { + var bytesFreed = 0L + var filesDeleted = 0 + var failedFiles = 0 + + dir.listFiles()?.forEach { entry -> + // Measure before deleting. + val files = entry.walkTopDown().filter { it.isFile }.toList() + val size = files.sumOf { it.length() } + if (entry.deleteRecursively()) { + bytesFreed += size + filesDeleted += files.size + } else { + failedFiles++ + } + } + return DirResult(label, bytesFreed, filesDeleted, failedFiles) + } +} diff --git a/src/main/kotlin/app/morphe/engine/patches/PatchCache.kt b/src/main/kotlin/app/morphe/engine/patches/PatchCache.kt new file mode 100644 index 0000000..6d05f9d --- /dev/null +++ b/src/main/kotlin/app/morphe/engine/patches/PatchCache.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-desktop + */ + +package app.morphe.engine.patches + +import app.morphe.engine.MorpheData +import app.morphe.engine.model.Release +import app.morphe.engine.model.ReleaseAsset +import java.io.File + +/** + * Shared on-disk cache layout for downloaded `.mpp` patch files. + * + * Both the GUI ([app.morphe.gui.data.repository.PatchRepository]) and the CLI + * ([app.morphe.cli.command.PatchFileResolver]) resolve their cache paths through here, + * so a patch file downloaded by one side is reused by the other instead of each keeping its own copy in a different place. + * + * Layout: `/-/__.mpp` + */ +object PatchCache { + + /** Per-source cache directory, e.g. `morphe-data/patches/MorpheApp-morphe-patches/`. */ + fun sourceDir(repoPath: String): File = + File(MorpheData.patchesDir, repoPath.replace("/", "-")).also { it.mkdirs() } + + /** + * Per-release cache filename, prefixed with the release tag. + * + * Many sources name their `.mpp` asset the same string across versions + * (e.g. `morphe-patches.mpp`); prefixing with the tag keeps versions from + * overwriting each other in the cache. + */ + fun cachedFileName(release: Release, asset: ReleaseAsset): String = + "${release.tagName}__${asset.name}" +} diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt index fcfc1fa..15c8ce2 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt @@ -7,9 +7,9 @@ package app.morphe.gui.data.repository import app.morphe.engine.model.Release import app.morphe.engine.model.ReleaseAsset +import app.morphe.engine.patches.PatchCache import app.morphe.engine.patches.RemotePatchSource import app.morphe.engine.patches.findPatchAsset -import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -55,7 +55,7 @@ class PatchRepository( * to eyeball when grepping the cache directory than a single dash. */ fun cachedFileName(release: Release, asset: ReleaseAsset): String = - "${release.tagName}__${asset.name}" + PatchCache.cachedFileName(release, asset) } // In-memory cache so multiple callers don't re-fetch from the remote API @@ -124,8 +124,7 @@ class PatchRepository( Exception("No .mpp patch files found in release ${release.tagName}") ) - val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) - patchesDir.mkdirs() + val patchesDir = PatchCache.sourceDir(repoPath) val targetFile = File(patchesDir, cachedFileName(release, asset)) // Cache hit rules: @@ -155,7 +154,7 @@ class PatchRepository( /** Get cached patch file for a specific version. */ fun getCachedPatches(version: String): File? { - val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) + val patchesDir = PatchCache.sourceDir(repoPath) return patchesDir.listFiles()?.find { it.name.contains(version) && isPatchFileName(it.name) } @@ -166,23 +165,19 @@ class PatchRepository( /** List all cached patch versions. */ fun listCachedPatches(): List { - val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) + val patchesDir = PatchCache.sourceDir(repoPath) return patchesDir.listFiles()?.filter { isPatchFileName(it.name) } ?: emptyList() } /** Get the per-source cache directory for this repository. */ - fun getCacheDir(): File { - val dir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) - dir.mkdirs() - return dir - } + fun getCacheDir(): File = PatchCache.sourceDir(repoPath) /** Delete cached patches (both in-memory release list and on-disk files). */ fun clearCache(): Boolean { cachedReleases = null cacheTimestamp = 0L return try { - val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) + val patchesDir = PatchCache.sourceDir(repoPath) var failedCount = 0 patchesDir.listFiles()?.forEach { file -> try { diff --git a/src/main/kotlin/app/morphe/gui/ui/components/ToolsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/ToolsDialog.kt index 422d8ab..a1a9dd1 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/ToolsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/ToolsDialog.kt @@ -24,6 +24,7 @@ import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.engine.CacheManager import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import java.awt.Desktop @@ -260,27 +261,11 @@ private fun calculateCacheSize(): String { } private fun clearAllCache(): Boolean { - return try { - var failedCount = 0 - FileUtils.getPatchesDir().listFiles()?.forEach { file -> - try { if (!file.deleteRecursively()) throw Exception("Could not delete") } - catch (e: Exception) { failedCount++; Logger.error("Failed to delete ${file.name}: ${e.message}") } - } - FileUtils.getLogsDir().listFiles()?.forEach { file -> - try { if (!file.deleteRecursively()) throw Exception("Could not delete") } - catch (e: Exception) { failedCount++; Logger.error("Failed to delete log ${file.name}: ${e.message}") } - } - - FileUtils.cleanupAllTempDirs() - if (failedCount > 0) { - Logger.error("Cache clear incomplete: $failedCount file(s) could not be deleted (may be locked)") - false - } else { - Logger.info("Cache cleared successfully") - true - } - } catch (e: Exception) { - Logger.error("Failed to clear cache", e) - false + val result = CacheManager.clearCaches() + if (result.success) { + Logger.info("Cache cleared successfully (${result.bytesFreed} bytes freed)") + } else { + Logger.error("Cache clear incomplete: ${result.failedFiles} file(s) could not be deleted (may be locked)") } + return result.success }