144 lines
5.4 KiB
Markdown
144 lines
5.4 KiB
Markdown
# Feature 3: Backup
|
|
|
|
## Context
|
|
This is the core functionality — automatically backing up WoW's WTF and Interface folders on a daily schedule. The backup runs in the background, respects configuration, and notifies the user of results.
|
|
|
|
## Dependencies
|
|
- **Depends on**: Feature 0 (ConfigManager, logging, platform), Feature 2 (config must be complete)
|
|
- **Depended on by**: Feature 4 (Status — displays backup state), Feature 5 (Restore — reads backup history)
|
|
|
|
## Key Decisions
|
|
- **Compression**: ZIP format using `java.util.zip` (no extra deps)
|
|
- **Process detection**: `ProcessHandle.allProcesses()` to check if WoW is running
|
|
- **Scheduling**: Coroutine-based scheduler, checks every minute if it's time to run
|
|
- **Backup naming**: Timestamped folders like `2024-01-15_03-00-00/` (or `.zip`)
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: WoW process detection
|
|
**Files to create:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/WoWProcessDetector.kt`
|
|
|
|
```kotlin
|
|
object WoWProcessDetector {
|
|
fun isWoWRunning(): Boolean {
|
|
return ProcessHandle.allProcesses()
|
|
.filter { it.isAlive }
|
|
.anyMatch { handle ->
|
|
val name = handle.info().command().orElse("").lowercase()
|
|
name.contains("world of warcraft") || name.contains("wow.exe") || name.contains("wow-64")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 2: File copy engine
|
|
**Files to create:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupEngine.kt`
|
|
|
|
Responsibilities:
|
|
- Copy selected folders (WTF, Interface) from WoW install to backup destination
|
|
- Support two modes: plain copy and ZIP compression
|
|
- Report progress: total files count, files copied so far, current file name
|
|
- Cancellable via coroutine cancellation
|
|
- Return a `BackupResult` (success/failure with details)
|
|
|
|
Progress model:
|
|
```kotlin
|
|
data class BackupProgress(
|
|
val totalFiles: Int,
|
|
val completedFiles: Int,
|
|
val currentFile: String,
|
|
)
|
|
|
|
sealed class BackupResult {
|
|
data class Success(val backupPath: File, val sizeBytes: Long, val durationMs: Long) : BackupResult()
|
|
data class Failure(val reason: String, val exception: Throwable? = null) : BackupResult()
|
|
}
|
|
```
|
|
|
|
For plain copy:
|
|
- Walk source directories, replicate structure in `<backupDir>/<timestamp>/`
|
|
|
|
For ZIP:
|
|
- Walk source directories, write to `<backupDir>/<timestamp>.zip` using `ZipOutputStream`
|
|
|
|
### Step 3: Backup history management
|
|
**Files to create:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupHistory.kt`
|
|
|
|
Responsibilities:
|
|
- List existing backups in the backup directory (sorted by date, newest first)
|
|
- Parse timestamps from folder/zip names
|
|
- Prune old backups exceeding `backupHistoryCount`
|
|
- Return list of `BackupEntry` for the Status and Restore screens
|
|
|
|
```kotlin
|
|
data class BackupEntry(
|
|
val path: File,
|
|
val timestamp: LocalDateTime,
|
|
val isCompressed: Boolean,
|
|
val sizeBytes: Long,
|
|
)
|
|
```
|
|
|
|
### Step 4: Backup scheduler
|
|
**Files to create:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupScheduler.kt`
|
|
|
|
Coroutine-based scheduler that:
|
|
- Runs in a `CoroutineScope` tied to the application lifecycle
|
|
- Checks every 60 seconds if the current time matches `backupTimeOfDay` (within a 1-minute window)
|
|
- Tracks whether today's backup has already run (to avoid duplicate runs)
|
|
- Before starting backup, checks:
|
|
1. Config is complete (`ConfigManager.isConfigured`)
|
|
2. WoW is not running (unless `forceBackupWhileRunning` is true)
|
|
- Delegates to `BackupEngine` for the actual work
|
|
- After backup, calls `BackupHistory.pruneOldBackups()`
|
|
- Exposes state as `StateFlow<SchedulerState>`:
|
|
|
|
```kotlin
|
|
data class SchedulerState(
|
|
val lastBackupTime: LocalDateTime? = null,
|
|
val lastBackupResult: BackupResult? = null,
|
|
val nextBackupTime: LocalDateTime? = null,
|
|
val isRunning: Boolean = false,
|
|
val currentProgress: BackupProgress? = null,
|
|
val isWoWRunning: Boolean = false,
|
|
)
|
|
```
|
|
|
|
### Step 5: Notifications integration
|
|
**Files to create:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupNotifier.kt`
|
|
|
|
Uses `TrayState.sendNotification()` (from Compose Desktop's Tray) to send native notifications:
|
|
- Backup started: "Backup in progress..."
|
|
- Backup completed: "Backup complete (X files, Y MB)"
|
|
- Backup failed: "Backup failed: <reason>"
|
|
- Backup skipped (WoW running): "Backup skipped — WoW is running"
|
|
|
|
Only sends if `notificationsEnabled` is true in config.
|
|
|
|
### Step 6: Wire scheduler into app lifecycle
|
|
**Files to modify:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt`
|
|
|
|
Start `BackupScheduler` when app launches. Stop when app exits. Pass `TrayState` to `BackupNotifier`.
|
|
|
|
### Step 7: Manual backup trigger
|
|
Add a "Backup Now" item to the tray context menu that triggers an immediate backup (bypassing the schedule, still respecting the WoW-running check).
|
|
|
|
**Files to modify:**
|
|
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt` — add menu item
|
|
|
|
## Verification
|
|
1. Set backup time to 1 minute from now, verify backup runs automatically
|
|
2. Verify WTF and Interface folders are copied correctly to backup destination
|
|
3. With compression enabled, verify a valid `.zip` is created
|
|
4. With WoW running and force=false, verify backup is skipped with notification
|
|
5. With WoW running and force=true, verify backup proceeds
|
|
6. Verify old backups are pruned when exceeding history count
|
|
7. "Backup Now" from tray menu triggers immediate backup
|
|
8. Notifications appear for start/complete/fail (when enabled)
|
|
9. Check logs for detailed backup operation entries
|