feat(backup): parallel uploads via connection pool + TaskGroup

Replace the sequential for-loop with withTaskGroup running N concurrent
uploads. Each slot gets its own SMB/SFTP connection so uploads never
block each other. Default concurrency is 3 (configurable via
ConnectionStore.maxConcurrentUploads, capped at 6).

- ConnectionStore: add maxConcurrentUploads (default 3, persisted)
- BackupEngine: build pool of N connections at start, seed TaskGroup
  with one task per slot, recycle slots as tasks complete
- Manifest checkpoints, pause, cancel, retry logic all preserved
- SlotResult extracted to BackupEngine extension for Sendable conformance
- Fix: remove undefined .trackScrollForTabBar() from BackupView

Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-28 20:24:20 +03:00
parent 920888efc1
commit 8fb425344d
2 changed files with 243 additions and 185 deletions

View File

@@ -74,6 +74,11 @@ final class ConnectionStore: ObservableObject {
didSet { UserDefaults.standard.set(notifyOnErrors, forKey: "notifyOnErrors") }
}
// Parallel upload concurrency (1 = sequential, 3 = default, max 6)
@Published var maxConcurrentUploads: Int {
didSet { UserDefaults.standard.set(maxConcurrentUploads, forKey: "maxConcurrentUploads") }
}
// Auto-backup via LAN trigger (trusted Wi-Fi join)
@Published var autoBackupEnabled: Bool {
didSet { UserDefaults.standard.set(autoBackupEnabled, forKey: "autoBackupEnabled") }
@@ -113,6 +118,7 @@ final class ConnectionStore: ObservableObject {
notifyOnStart = UserDefaults.standard.object(forKey: "notifyOnStart") as? Bool ?? false
notifyOnComplete = UserDefaults.standard.object(forKey: "notifyOnComplete") as? Bool ?? true
notifyOnErrors = UserDefaults.standard.object(forKey: "notifyOnErrors") as? Bool ?? true
maxConcurrentUploads = UserDefaults.standard.object(forKey: "maxConcurrentUploads") as? Int ?? 3
autoBackupEnabled = UserDefaults.standard.object(forKey: "autoBackupEnabled") as? Bool ?? true
autoBackupOnOpen = UserDefaults.standard.object(forKey: "autoBackupOnOpen") as? Bool ?? false
allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false