diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupHistory.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupHistory.kt index e140a2d..f03c20d 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupHistory.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupHistory.kt @@ -17,12 +17,29 @@ class BackupEntry( val sizeBytes: Long by lazy { sizeComputer() } } +data class BackupMetrics(val fileCount: Int, val sizeBytes: Long) + object BackupHistory { // Used internally for parsing — java.time formatter since kotlinx.datetime // doesn't have custom format patterns built in private val JAVA_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss") + /** + * Counts backups without parsing timestamps or sorting — just matches the naming pattern. + */ + fun countBackups(backupDir: File): Int { + if (!backupDir.exists() || !backupDir.isDirectory) return 0 + return backupDir.listFiles()?.count { file -> + val name = file.nameWithoutExtension + val isZip = file.extension.equals("zip", ignoreCase = true) && file.isFile + val isDir = file.isDirectory + if (!isZip && !isDir) return@count false + try { java.time.LocalDateTime.parse(name, JAVA_TIMESTAMP_FORMAT); true } + catch (_: DateTimeParseException) { false } + } ?: 0 + } + /** * Lists all existing backups in the backup directory, sorted newest first. */ @@ -37,13 +54,18 @@ object BackupHistory { } /** - * Returns the file count for a backup entry without computing its full size. + * Computes file count and size in a single pass over the backup tree. */ - fun fileCount(entry: BackupEntry): Int { + fun computeMetrics(entry: BackupEntry): BackupMetrics { return if (entry.isCompressed) { - java.util.zip.ZipFile(entry.path).use { it.size() } + java.util.zip.ZipFile(entry.path).use { zip -> + BackupMetrics(zip.size(), entry.path.length()) + } } else { - entry.path.walkTopDown().count { it.isFile } + var count = 0 + var size = 0L + entry.path.walkTopDown().filter { it.isFile }.forEach { count++; size += it.length() } + BackupMetrics(count, size) } } diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupScheduler.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupScheduler.kt index f8f7e85..552f1f5 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupScheduler.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupScheduler.kt @@ -46,6 +46,19 @@ object BackupScheduler { if (schedulerJob?.isActive == true) return logger.info { "Backup scheduler started" } + // Eagerly set timestamps so StatusScreen has data immediately. + // Only set lastBackupTime and nextBackupTime here (cheap). + // The full lastBackupResult (fileCount, sizeBytes) is filled by the async path + // since fileCount() walks the directory tree and would block the main thread. + val config = ConfigManager.config.value + if (config.isConfigured && config.backupPath != null) { + val backups = BackupHistory.listBackups(File(config.backupPath)) + if (backups.isNotEmpty()) { + _state.update { it.copy(lastBackupTime = backups.first().timestamp) } + } + updateNextBackupTime(TimeZone.currentSystemDefault()) + } + schedulerJob = scope.launch { // Collect progress from BackupEngine launch { @@ -65,23 +78,23 @@ object BackupScheduler { } // On first config emission, restore last backup info from disk - if (_state.value.lastBackupTime == null && config.backupPath != null) { + if (_state.value.lastBackupResult == null && config.backupPath != null) { val backups = BackupHistory.listBackups(File(config.backupPath)) if (backups.isNotEmpty()) { val latest = backups.first() - val fileCount = BackupHistory.fileCount(latest) + val metrics = BackupHistory.computeMetrics(latest) _state.update { it.copy( lastBackupTime = latest.timestamp, lastBackupResult = BackupResult.Success( backupPath = latest.path, - fileCount = fileCount, - sizeBytes = latest.sizeBytes, + fileCount = metrics.fileCount, + sizeBytes = metrics.sizeBytes, durationMs = 0, ), ) } - logger.info { "Restored last backup info from disk: ${latest.path.name} ($fileCount files)" } + logger.info { "Restored last backup info from disk: ${latest.path.name} (${metrics.fileCount} files)" } } } diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusViewModel.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusViewModel.kt index 08a3fee..ff36ca0 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusViewModel.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusViewModel.kt @@ -43,7 +43,7 @@ class StatusViewModel : ViewModel() { BackupScheduler.state.map { it.lastBackupResult }.distinctUntilChanged(), ) { backupPath, _ -> if (backupPath != null) { - BackupHistory.listBackups(File(backupPath)).size + BackupHistory.countBackups(File(backupPath)) } else { 0 }