diff --git a/Brand/Database.swift b/Brand/Database.swift index 3a37271bf0..2a1727e2f8 100644 --- a/Brand/Database.swift +++ b/Brand/Database.swift @@ -8,4 +8,4 @@ import Foundation // let databaseName = "nextcloud.realm" let tableAccountBackup = "tableAccountBackup.json" -let databaseSchemaVersion: UInt64 = 422 +let databaseSchemaVersion: UInt64 = 423 diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index ef41a66750..ebe0141221 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -6740,7 +6740,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -6808,7 +6808,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = NKUJUXUJ3B; diff --git a/Tests/NextcloudUnitTests/NCAutoUploadDateTests.swift b/Tests/NextcloudUnitTests/NCAutoUploadDateTests.swift index 7aa5484f64..84c5c5fd0f 100644 --- a/Tests/NextcloudUnitTests/NCAutoUploadDateTests.swift +++ b/Tests/NextcloudUnitTests/NCAutoUploadDateTests.swift @@ -38,4 +38,31 @@ struct NCAutoUploadDateTests { let decoded = try JSONDecoder().decode(tableAccountCodable.self, from: oldData) #expect(tableAccount(codableObject: decoded).autoUploadSinceDate == account.autoUploadSinceDate) } + + @Test("Whole-library selection survives backup with a nonempty progress date") + func wholeLibrarySelectionWithProgress() throws { + let account = tableAccount() + account.autoUploadAllPhotos = true + account.autoUploadSinceDate = Date(timeIntervalSince1970: 100) + let data = try JSONEncoder().encode(account.tableAccountToCodable()) + let decoded = try JSONDecoder().decode(tableAccountCodable.self, from: data) + let restored = tableAccount(codableObject: decoded) + #expect(restored.autoUploadAllPhotos) + #expect(restored.autoUploadSinceDate == account.autoUploadSinceDate) + } + + @Test("Older backups infer the library selection from their existing date") + func previousBackupSelectionCompatibility() throws { + let dates: [Date?] = [nil, Date(timeIntervalSince1970: 100)] + for date in dates { + let account = tableAccount() + account.autoUploadSinceDate = date + let data = try JSONEncoder().encode(account.tableAccountToCodable()) + var json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) + json.removeValue(forKey: "autoUploadAllPhotos") + let oldData = try JSONSerialization.data(withJSONObject: json) + let decoded = try JSONDecoder().decode(tableAccountCodable.self, from: oldData) + #expect(tableAccount(codableObject: decoded).autoUploadAllPhotos == (date == nil)) + } + } } diff --git a/iOSClient/Data/NCManageDatabase+Account.swift b/iOSClient/Data/NCManageDatabase+Account.swift index f97958adfc..8422094b3c 100644 --- a/iOSClient/Data/NCManageDatabase+Account.swift +++ b/iOSClient/Data/NCManageDatabase+Account.swift @@ -25,6 +25,8 @@ class tableAccount: Object { @objc dynamic var autoUploadWWAnVideo: Bool = false /// Incremental restart date; nil scans the whole library. Advances only past confirmed uploads. @objc dynamic var autoUploadSinceDate: Date? + /// Preserves the whole-library selection independently of incremental progress. + @objc dynamic var autoUploadAllPhotos = false /// Ignores completed uploads from previous sessions while preserving the local history. @objc dynamic var autoUploadForceReupload = false @objc dynamic var backend = "" @@ -78,6 +80,7 @@ class tableAccount: Object { autoUploadWWAnPhoto: self.autoUploadWWAnPhoto, autoUploadWWAnVideo: self.autoUploadWWAnVideo, autoUploadSinceDate: self.autoUploadSinceDate, + autoUploadAllPhotos: self.autoUploadAllPhotos, autoUploadForceReupload: self.autoUploadForceReupload, user: self.user, userId: self.userId, @@ -100,6 +103,7 @@ class tableAccount: Object { self.autoUploadWWAnPhoto = codableObject.autoUploadWWAnPhoto self.autoUploadWWAnVideo = codableObject.autoUploadWWAnVideo self.autoUploadSinceDate = codableObject.autoUploadSinceDate + self.autoUploadAllPhotos = codableObject.autoUploadAllPhotos ?? (codableObject.autoUploadSinceDate == nil) self.autoUploadForceReupload = codableObject.autoUploadForceReupload ?? false self.user = codableObject.user @@ -123,6 +127,8 @@ struct tableAccountCodable: Codable { var autoUploadWWAnPhoto: Bool var autoUploadWWAnVideo: Bool var autoUploadSinceDate: Date? + // Optional so older backups infer the selection from their restart date. + var autoUploadAllPhotos: Bool? // Optional so backups written before this setting remain readable. var autoUploadForceReupload: Bool? @@ -397,6 +403,19 @@ extension NCManageDatabase { guard let current = realm.objects(tableAccount.self).filter("account == %@", account).first, !current.autoUploadStart else { return } current.autoUploadSinceDate = date + current.autoUploadAllPhotos = date == nil + } + } + + /// Selecting the whole library preserves the date until the next Start. + func setAutoUploadAllPhotosAsync(_ allPhotos: Bool, account: String) async { + await core.performRealmWriteAsync { realm in + guard let current = realm.objects(tableAccount.self).filter("account == %@", account).first, + !current.autoUploadStart else { return } + current.autoUploadAllPhotos = allPhotos + if !allPhotos, current.autoUploadSinceDate == nil { + current.autoUploadSinceDate = Date.now + } } } @@ -414,6 +433,7 @@ extension NCManageDatabase { guard let current = accounts.filter("account == %@", account).first else { return } if enabled, !accounts.filter("autoUploadStart == true AND account != %@", account).isEmpty { return } if current.autoUploadStart != enabled { + if enabled, current.autoUploadAllPhotos { current.autoUploadSinceDate = nil } current.autoUploadSessionIdentifier = UUID().uuidString current.autoUploadStart = enabled } diff --git a/iOSClient/Data/NCManageDatabaseCore.swift b/iOSClient/Data/NCManageDatabaseCore.swift index 5cbb0b6f2d..26231c252d 100644 --- a/iOSClient/Data/NCManageDatabaseCore.swift +++ b/iOSClient/Data/NCManageDatabaseCore.swift @@ -72,6 +72,14 @@ final class NCManageDatabaseCore { } } + if oldSchemaVersion < 423 { + migration.enumerateObjects(ofType: tableAccount.className()) { _, newObject in + guard let newObject else { return } + let sinceDate = newObject.value("autoUploadSinceDate", as: Date.self) + newObject.setValueSafely(sinceDate == nil, for: "autoUploadAllPhotos") + } + } + // // AUTOMATIC / DEFENSIVE MIGRATIONS // diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift index d166f4d4e2..aa5bbb4128 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift @@ -36,7 +36,8 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { /// The incremental restart date, editable while Auto Upload is stopped. @Published var autoUploadSinceDate: Date? @Published var autoUploadForceReupload = false - var autoUploadTimespan: AutoUploadTimespan { autoUploadSinceDate == nil ? .allPhotos : .fromDate } + @Published var autoUploadAllPhotos = false + var autoUploadTimespan: AutoUploadTimespan { autoUploadAllPhotos ? .allPhotos : .fromDate } /// Whether Photos permissions have been granted or not. @Published var photosPermissionsGranted = true /// Whether `Always` location authorization has been granted, enabling background location-based auto upload. @@ -96,6 +97,7 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { autoUploadCreateSubfolder = tableAccount.autoUploadCreateSubfolder autoUploadSubfolderGranularity = Granularity(rawValue: tableAccount.autoUploadSubfolderGranularity) ?? .monthly autoUploadSinceDate = tableAccount.autoUploadSinceDate + autoUploadAllPhotos = tableAccount.autoUploadAllPhotos autoUploadForceReupload = tableAccount.autoUploadForceReupload } @@ -154,10 +156,22 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } } - /// Starts from the whole library or the current incremental restart date. + /// Changes the selection without discarding progress before Start is pressed. @MainActor func handleAutoUploadTimespan(_ timespan: AutoUploadTimespan) { - handleAutoUploadSinceDate(timespan == .allPhotos ? nil : autoUploadSinceDate ?? Date.now) + guard !isChangingAutoUpload, !autoUploadStart else { return } + isChangingAutoUpload = true + let accountIdentifier = session.account + autoUploadAllPhotos = timespan == .allPhotos + let allPhotos = autoUploadAllPhotos + Task { + defer { isChangingAutoUpload = false } + await database.setAutoUploadAllPhotosAsync(allPhotos, account: accountIdentifier) + if let account = await database.getTableAccountAsync(predicate: NSPredicate(format: "account == %@", accountIdentifier)) { + autoUploadSinceDate = account.autoUploadSinceDate + autoUploadAllPhotos = account.autoUploadAllPhotos + } + } } /// Saves a new incremental restart date only while Auto Upload is stopped. @@ -167,6 +181,7 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { isChangingAutoUpload = true let accountIdentifier = session.account autoUploadSinceDate = date + autoUploadAllPhotos = date == nil Task { defer { isChangingAutoUpload = false } await database.setAutoUploadSinceDateAsync(date, account: accountIdentifier) @@ -245,6 +260,7 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { return } + autoUploadSinceDate = updatedAccount.autoUploadSinceDate // Stop remains available during the legacy initial scan. isChangingAutoUpload = false _ = await NCAutoUpload.shared.startManualAutoUploadForAlbums( diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 7d6ab00183..a64f692b90 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -539,7 +539,7 @@ struct NCAutoUploadView: View { .accessibilityAddTraits(model.autoUploadTimespan == timespan ? .isSelected : []) } - if model.autoUploadSinceDate != nil { + if model.autoUploadTimespan == .fromDate { VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 12) { Text("_autoupload_start_date_") @@ -582,7 +582,7 @@ struct NCAutoUploadView: View { return NSLocalizedString("_autoupload_force_reupload_footer_", comment: "") } let destination = model.returnPath() - if model.autoUploadSinceDate != nil { + if model.autoUploadTimespan == .fromDate { return String(format: NSLocalizedString("_autoupload_date_range_footer_", comment: ""), destination) } return String(format: NSLocalizedString("_autoupload_whole_library_description_", comment: ""), destination)