From fb1a85ae463e06da03d1b17628b0a35460a52d50 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Apr 2026 10:36:45 +0200 Subject: [PATCH 1/4] =?UTF-8?q?try=20=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 8 +- iOSClient/AppDelegate.swift | 136 +++++++++++++++++++--------- 2 files changed, 98 insertions(+), 46 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 298fdfaf96..1413528bae 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5790,7 +5790,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -5818,7 +5818,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 33.0.6; + MARKETING_VERSION = 33.0.7; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; @@ -5858,7 +5858,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -5884,7 +5884,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 33.0.6; + MARKETING_VERSION = 33.0.7; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index f237c62852..ba145ded9a 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -242,50 +242,65 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } + // Executes the background synchronization flow for Auto Upload. + // + // The function: + // - discovers new Auto Upload items, + // - fetches pending metadata, + // - creates missing folders when required, + // - checks server-side existence, + // - expands seeds into concrete metadata items, + // - queues uploads sequentially. + // + // The flow is cooperative with BGTask expiration and stops as soon as expiration is detected. + // + // - Parameter task: Optional background task used to observe expiration. func backgroundSync(task: BGTask? = nil) async { - defer { - // Update badge safely at the end of the background sync - Task { @MainActor in - do { - let count = await NCManageDatabase.shared.getMetadatasInWaitingCountAsync() - try await UNUserNotificationCenter.current().setBadgeCount(count) - } catch { } - } - } + let expirationState = BackgroundSyncExpirationState() - // BGTask expiration flag - var expired = false task?.expirationHandler = { - expired = true + Task { + await expirationState.markExpired() + } } - // Discover new items for Auto Upload + // Discover new items for Auto Upload. let numAutoUpload = await NCAutoUpload.shared.initAutoUpload() nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Auto upload found \(numAutoUpload) new items") - guard !expired else { return } - // Fetch METADATAS + guard !(await expirationState.isExpired()) else { + return + } + + // Fetch pending metadata. let metadatas = await NCManageDatabase.shared.getMetadataProcess() - guard !metadatas.isEmpty, !expired else { + guard !metadatas.isEmpty, !(await expirationState.isExpired()) else { return } - // Create all pending Auto Upload folders (fail-fast) + // Create all pending Auto Upload folders (fail-fast). let pendingCreateFolders = metadatas.lazy.filter { $0.status == self.global.metadataStatusWaitCreateFolder && $0.sessionSelector == self.global.selectorUploadAutoUpload } - // Get accounts -> Capabilities + // Resolve capabilities once per account. let accounts = Array(Set(pendingCreateFolders.map { $0.account })) var capabilitiesByAccount: [String: NKCapabilities.Capabilities] = [:] + for account in accounts { + guard !(await expirationState.isExpired()) else { + return + } + let capabilities = await NKCapabilities.shared.getCapabilities(for: account) capabilitiesByAccount[account] = capabilities } for metadata in pendingCreateFolders { - guard !expired else { return } + guard !(await expirationState.isExpired()) else { + return + } // If server supports auto MKCOL (Nextcloud >= 33), skip manual folder creation. if let capabilities = capabilitiesByAccount[metadata.account] { @@ -294,24 +309,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD continue } } - // Create folder + let err = await NCNetworking.shared.createFolderForAutoUpload( serverUrlFileName: metadata.serverUrlFileName, account: metadata.account ) - // Fail-fast: abort the whole sync on first failure + + // Fail-fast: abort the whole sync on first failure. if err != .success { - nkLog(tag: self.global.logTagBgSync, emoji: .error, message: "Create folder '\(metadata.serverUrlFileName)' failed: \(err.errorCode) – aborting sync") + nkLog( + tag: self.global.logTagBgSync, + emoji: .error, + message: "Create folder '\(metadata.serverUrlFileName)' failed: \(err.errorCode) – aborting sync" + ) return } } - // Capacity computation + // Compute available capacity. let downloading = metadatas.lazy.filter { $0.status == self.global.metadataStatusDownloading }.count let uploading = metadatas.lazy.filter { $0.status == self.global.metadataStatusUploading }.count let availableProcess = max(0, NCBrandOptions.shared.numMaximumProcess - (downloading + uploading)) - // Start Auto Uploads + // Select Auto Upload candidates. let metadatasToUpload = Array( metadatas.lazy.filter { $0.status == self.global.metadataStatusWaitUpload && @@ -324,39 +344,71 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let cameraRoll = NCCameraRoll() for metadata in metadatasToUpload { - guard !expired else { return } + guard !(await expirationState.isExpired()) else { + return + } + + // Check whether the file already exists remotely. + let existsResult = await NCNetworking.shared.fileExists( + serverUrlFileName: metadata.serverUrlFileName, + account: metadata.account + ) - // File exists? skip it - let existsResult = await NCNetworking.shared.fileExists(serverUrlFileName: metadata.serverUrlFileName, account: metadata.account) if existsResult == .success { - // File exists → delete from local metadata and skip + // File exists remotely: remove local metadata and continue. await NCManageDatabase.shared.deleteMetadataAsync(id: metadata.ocId) continue - } else if existsResult.errorCode == 404 { - // 404 Not Found → directory does not exist - // Proceed - } else { - // Any other error (423 locked, 401 auth, 403 forbidden, 5xx, etc.) + } else if existsResult.errorCode != 404 { + // Ignore transient or server-side errors and continue with the next item. continue } - // Expand seed into concrete metadatas (e.g., Live Photo pair) - let extracted = await cameraRoll.extractCameraRoll(from: metadata) - guard !expired else { return } + // Expand the seed into concrete metadata entries (for example, Live Photo pairs). + let extractedMetadatas = await cameraRoll.extractCameraRoll(from: metadata) + + guard !(await expirationState.isExpired()) else { + return + } + + for extractedMetadata in extractedMetadatas { + guard !(await expirationState.isExpired()) else { + return + } + + let err = await NCNetworking.shared.uploadFileInBackground( + metadata: extractedMetadata.detachedCopy() + ) - for metadata in extracted { - // Sequential await keeps ordering and simplifies backpressure - let err = await NCNetworking.shared.uploadFileInBackground(metadata: metadata.detachedCopy()) if err == .success { - nkLog(tag: self.global.logTagBgSync, message: "In queued upload \(metadata.fileName) -> \(metadata.serverUrl)") + nkLog( + tag: self.global.logTagBgSync, + message: "In queued upload \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl)" + ) } else { - nkLog(tag: self.global.logTagBgSync, emoji: .error, message: "Upload failed \(metadata.fileName) -> \(metadata.serverUrl) [\(err.errorDescription)]") + nkLog( + tag: self.global.logTagBgSync, + emoji: .error, + message: "Upload failed \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl) [\(err.errorDescription)]" + ) } - guard !expired else { return } } } } + actor BackgroundSyncExpirationState { + private var expired = false + + // Marks the background sync as expired. + func markExpired() { + expired = true + } + + // Returns whether the background sync has expired. + func isExpired() -> Bool { + expired + } + } + // MARK: - Background Networking Session func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { From 77584d3485961e9fb9359fb93d59129fda02786d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Apr 2026 11:32:18 +0200 Subject: [PATCH 2/4] handleTask Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 12 + iOSClient/AppDelegate+AppProcessing.swift | 98 +++++++ iOSClient/AppDelegate+AppRefresh.swift | 64 +++++ ...AppDelegate+AutoUploadBackgroundSync.swift | 139 ++++++++++ iOSClient/AppDelegate.swift | 262 ------------------ .../NCBackgroundLocationUploadManager.swift | 2 +- 6 files changed, 314 insertions(+), 263 deletions(-) create mode 100644 iOSClient/AppDelegate+AppProcessing.swift create mode 100644 iOSClient/AppDelegate+AppRefresh.swift create mode 100644 iOSClient/AppDelegate+AutoUploadBackgroundSync.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 1413528bae..2644d821cf 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -230,6 +230,9 @@ F710FC80277B7D2700AA9FBF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F710FC7F277B7D2700AA9FBF /* RealmSwift */; }; F710FC84277B7D3500AA9FBF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F710FC83277B7D3500AA9FBF /* RealmSwift */; }; F710FC88277B7D3F00AA9FBF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F710FC87277B7D3F00AA9FBF /* RealmSwift */; }; + F7110AE02F9773230095AA5C /* AppDelegate+AppRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */; }; + F7110AE42F9774140095AA5C /* AppDelegate+AppProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */; }; + F7110AE62F977A680095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7110AE52F977A630095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift */; }; F711A4DC2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; }; F711A4DD2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; }; F711A4DE2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; }; @@ -1331,6 +1334,9 @@ F71070AA2F7E49E100AEE58A /* NCEndToEndSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndSetup.swift; sourceTree = ""; }; F710D1F42405770F00A6033D /* NCViewerPDF.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCViewerPDF.swift; sourceTree = ""; }; F710D2012405826100A6033D /* NCContextMenuViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCContextMenuViewer.swift; sourceTree = ""; }; + F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AppRefresh.swift"; sourceTree = ""; }; + F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AppProcessing.swift"; sourceTree = ""; }; + F7110AE52F977A630095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AutoUploadBackgroundSync.swift"; sourceTree = ""; }; F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCUtility+Date.swift"; sourceTree = ""; }; F7132C6B2D085AD200B42D6A /* NCTermOfServiceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTermOfServiceModel.swift; sourceTree = ""; }; F7132C6C2D085AD200B42D6A /* NCTermOfServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTermOfServiceView.swift; sourceTree = ""; }; @@ -3287,6 +3293,9 @@ children = ( AA517BB42D66149900F8D37C /* .tx */, F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, + F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */, + F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */, + F7110AE52F977A630095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift */, F7CAFE1A2F16AA8600DB35A5 /* main.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */, @@ -4508,6 +4517,8 @@ F70753F12542A9A200972D44 /* NCViewerMedia.swift in Sources */, F799DF822C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */, F76B649C2ADFFAED00014640 /* NCImageCache.swift in Sources */, + F7110AE42F9774140095AA5C /* AppDelegate+AppProcessing.swift in Sources */, + F7110AE62F977A680095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift in Sources */, F76341182EBE0BC60056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */, F34E1AD92ECC839100FA10C3 /* EmojiTextField.swift in Sources */, @@ -4531,6 +4542,7 @@ F765E9CD295C585800A09ED8 /* NCUploadScanDocument.swift in Sources */, F741C2242B6B9FD600E849BB /* NCMediaSelectTabBar.swift in Sources */, F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */, + F7110AE02F9773230095AA5C /* AppDelegate+AppRefresh.swift in Sources */, AA8D31662D411FA100FE2775 /* NCShareDateCell.swift in Sources */, F3F442EE2DDE292D00FD701F /* NCMetadataPermissions.swift in Sources */, F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */, diff --git a/iOSClient/AppDelegate+AppProcessing.swift b/iOSClient/AppDelegate+AppProcessing.swift new file mode 100644 index 0000000000..1c9df56052 --- /dev/null +++ b/iOSClient/AppDelegate+AppProcessing.swift @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit +import BackgroundTasks + +extension AppDelegate { + // Schedules the next processing task. + // + // The scheduler may delay execution depending on device conditions, + // battery state, thermal conditions, and system policy. + func scheduleAppProcessing() { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: global.processingTask) + + let request = BGProcessingTaskRequest(identifier: global.processingTask) + request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) + request.requiresNetworkConnectivity = false + request.requiresExternalPower = false + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Processing task failed to submit request: \(error)") + } + } + + // Handles the BGProcessingTask lifecycle for weekly cleanup or background synchronization. + // + // The function: + // - validates background Realm availability, + // - schedules the next processing task, + // - executes either weekly cleanup or background sync, + // - cooperates with BGTask expiration by cancelling the Swift task, + // - reports success only if the work completes without cancellation. + // + // - Parameter task: The system-provided background processing task. + func handleProcessingTask(_ task: BGProcessingTask) { + nkLog(tag: self.global.logTagTask, emoji: .start, message: "Start processing task") + + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Failed to open Realm in background") + task.setTaskCompleted(success: false) + return + } + + // Schedule next processing task. + scheduleAppProcessing() + + let processingTask = Task { () -> Bool in + // If possible, cleaning every week. + if NCPreferences().cleaningWeek() { + nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Start cleaning week") + + let tblAccounts = await NCManageDatabase.shared.getAllTableAccountAsync() + for tblAccount in tblAccounts { + guard !Task.isCancelled else { + return false + } + + await NCManageDatabase.shared.cleanTablesOcIds( + account: tblAccount.account, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase + ) + } + + guard !Task.isCancelled else { + return false + } + + await NCUtilityFileSystem().cleanUpAsync() + + guard !Task.isCancelled else { + return false + } + + NCPreferences().setDoneCleaningWeek() + nkLog(tag: self.global.logTagBgSync, emoji: .stop, message: "Stop cleaning week") + return true + } else { + await autoUploadBackgroundSync() + return !Task.isCancelled + } + } + + Task { + let success = await processingTask.value + task.setTaskCompleted(success: success) + } + + task.expirationHandler = { + nkLog(tag: self.global.logTagTask, emoji: .stop, message: "Processing task expired") + processingTask.cancel() + } + } +} diff --git a/iOSClient/AppDelegate+AppRefresh.swift b/iOSClient/AppDelegate+AppRefresh.swift new file mode 100644 index 0000000000..29f46d2a02 --- /dev/null +++ b/iOSClient/AppDelegate+AppRefresh.swift @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit +import BackgroundTasks + +extension AppDelegate { + // Schedules the next app refresh task. + // + // The scheduler may delay execution depending on device conditions, + // battery state, usage patterns, and system policy. + func scheduleAppRefresh() { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: global.refreshTask) + + let request = BGAppRefreshTaskRequest(identifier: global.refreshTask) + request.earliestBeginDate = Date(timeIntervalSinceNow: 60) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Refresh task failed to submit request: \(error)") + } + } + + // Handles the BGAppRefreshTask lifecycle for background synchronization. + // + // The function: + // - validates background Realm availability, + // - schedules the next refresh task, + // - starts the background synchronization flow, + // - cooperates with BGTask expiration by cancelling the Swift task, + // - reports success only if the work completes without cancellation. + // + // - Parameter task: The system-provided background refresh task. + func handleAppRefresh(_ task: BGAppRefreshTask) { + nkLog(tag: self.global.logTagTask, emoji: .start, message: "Start refresh task") + + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Failed to open Realm in background") + task.setTaskCompleted(success: false) + return + } + + // Schedule next refresh. + scheduleAppRefresh() + + let refreshTask = Task { () -> Bool in + await self.autoUploadBackgroundSync() + return !Task.isCancelled + } + + Task { + let success = await refreshTask.value + task.setTaskCompleted(success: success) + } + + task.expirationHandler = { + nkLog(tag: self.global.logTagTask, emoji: .stop, message: "Refresh task expired") + refreshTask.cancel() + } + } +} diff --git a/iOSClient/AppDelegate+AutoUploadBackgroundSync.swift b/iOSClient/AppDelegate+AutoUploadBackgroundSync.swift new file mode 100644 index 0000000000..0aeb0ec3a0 --- /dev/null +++ b/iOSClient/AppDelegate+AutoUploadBackgroundSync.swift @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit +import BackgroundTasks + +extension AppDelegate { + // Executes the background synchronization flow for Auto Upload. + // + // The function: + // - discovers new Auto Upload items, + // - fetches pending metadata, + // - creates missing folders when required, + // - checks remote existence, + // - expands seeds into concrete metadata items, + // - queues uploads sequentially. + // + // The flow cooperates with Swift task cancellation triggered by BGTask expiration. + func autoUploadBackgroundSync() async { + guard !Task.isCancelled else { return } + + // Discover new items for Auto Upload. + let numAutoUpload = await NCAutoUpload.shared.initAutoUpload() + nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Auto upload found \(numAutoUpload) new items") + + guard !Task.isCancelled else { return } + + // Fetch pending metadata. + let metadatas = await NCManageDatabase.shared.getMetadataProcess() + guard !metadatas.isEmpty, !Task.isCancelled else { + return + } + + // Create all pending Auto Upload folders (fail-fast). + let pendingCreateFolders = metadatas.lazy.filter { + $0.status == self.global.metadataStatusWaitCreateFolder && + $0.sessionSelector == self.global.selectorUploadAutoUpload + } + + // Resolve capabilities once per account. + let accounts = Array(Set(pendingCreateFolders.map { $0.account })) + var capabilitiesByAccount: [String: NKCapabilities.Capabilities] = [:] + + for account in accounts { + guard !Task.isCancelled else { return } + + let capabilities = await NKCapabilities.shared.getCapabilities(for: account) + capabilitiesByAccount[account] = capabilities + } + + for metadata in pendingCreateFolders { + guard !Task.isCancelled else { return } + + // If server supports auto MKCOL (Nextcloud >= 33), skip manual folder creation. + if let capabilities = capabilitiesByAccount[metadata.account] { + let autoMkcol = capabilities.serverVersionMajor >= NCGlobal.shared.nextcloudVersion33 + if autoMkcol { + continue + } + } + + let err = await NCNetworking.shared.createFolderForAutoUpload( + serverUrlFileName: metadata.serverUrlFileName, + account: metadata.account + ) + + if err != .success { + nkLog( + tag: self.global.logTagBgSync, + emoji: .error, + message: "Create folder '\(metadata.serverUrlFileName)' failed: \(err.errorCode) – aborting sync" + ) + return + } + } + + // Compute available capacity. + let downloading = metadatas.lazy.filter { $0.status == self.global.metadataStatusDownloading }.count + let uploading = metadatas.lazy.filter { $0.status == self.global.metadataStatusUploading }.count + let availableProcess = max(0, NCBrandOptions.shared.numMaximumProcess - (downloading + uploading)) + + // Select Auto Upload candidates. + let metadatasToUpload = Array( + metadatas.lazy.filter { + $0.status == self.global.metadataStatusWaitUpload && + $0.sessionSelector == self.global.selectorUploadAutoUpload && + $0.chunk == 0 + } + .prefix(availableProcess) + ) + + let cameraRoll = NCCameraRoll() + + for metadata in metadatasToUpload { + guard !Task.isCancelled else { return } + + // Check whether the file already exists remotely. + let existsResult = await NCNetworking.shared.fileExists( + serverUrlFileName: metadata.serverUrlFileName, + account: metadata.account + ) + + if existsResult == .success { + await NCManageDatabase.shared.deleteMetadataAsync(id: metadata.ocId) + continue + } else if existsResult.errorCode != 404 { + continue + } + + // Expand the seed into concrete metadata entries (for example, Live Photo pairs). + let extractedMetadatas = await cameraRoll.extractCameraRoll(from: metadata) + + guard !Task.isCancelled else { return } + + for extractedMetadata in extractedMetadatas { + guard !Task.isCancelled else { return } + + let err = await NCNetworking.shared.uploadFileInBackground( + metadata: extractedMetadata.detachedCopy() + ) + + if err == .success { + nkLog( + tag: self.global.logTagBgSync, + message: "In queued upload \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl)" + ) + } else { + nkLog( + tag: self.global.logTagBgSync, + emoji: .error, + message: "Upload failed \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl) [\(err.errorDescription)]" + ) + } + } + } + } +} diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index ba145ded9a..4251685876 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -95,8 +95,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD review.showStoreReview() #endif - // BACKGROUND TASK - // BGTaskScheduler.shared.register(forTaskWithIdentifier: global.refreshTask, using: backgroundQueue) { task in guard let appRefreshTask = task as? BGAppRefreshTask else { task.setTaskCompleted(success: false) @@ -149,266 +147,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - // MARK: - Background Task - - /* - @discussion Schedule a refresh task request to ask that the system launch your app briefly so that you can download data and keep your app's contents up-to-date. The system will fulfill this request intelligently based on system conditions and app usage. - */ - func scheduleAppRefresh() { - let request = BGAppRefreshTaskRequest(identifier: global.refreshTask) - - request.earliestBeginDate = Date(timeIntervalSinceNow: 60) // Refresh after 60 seconds. - - do { - try BGTaskScheduler.shared.submit(request) - } catch { - nkLog(tag: self.global.logTagTask, emoji: .error, message: "Refresh task failed to submit request: \(error)") - } - } - - /* - @discussion Schedule a processing task request to ask that the system launch your app when conditions are favorable for battery life to handle deferrable, longer-running processing, such as syncing, database maintenance, or similar tasks. The system will attempt to fulfill this request to the best of its ability within the next two days as long as the user has used your app within the past week. - */ - func scheduleAppProcessing() { - let request = BGProcessingTaskRequest(identifier: global.processingTask) - - request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // Refresh after 5 minutes. - request.requiresNetworkConnectivity = false - request.requiresExternalPower = false - - do { - try BGTaskScheduler.shared.submit(request) - } catch { - nkLog(tag: self.global.logTagTask, emoji: .error, message: "Processing task failed to submit request: \(error)") - } - } - - func handleAppRefresh(_ task: BGAppRefreshTask) { - nkLog(tag: self.global.logTagTask, emoji: .start, message: "Start refresh task") - guard NCManageDatabase.shared.openRealmBackground() else { - nkLog(tag: self.global.logTagTask, emoji: .error, message: "Failed to open Realm in background") - task.setTaskCompleted(success: false) - return - } - - // Schedule next refresh - scheduleAppRefresh() - - Task { - defer { - task.setTaskCompleted(success: true) - } - - await backgroundSync(task: task) - } - } - - func handleProcessingTask(_ task: BGProcessingTask) { - nkLog(tag: self.global.logTagTask, emoji: .start, message: "Start processing task") - guard NCManageDatabase.shared.openRealmBackground() else { - nkLog(tag: self.global.logTagTask, emoji: .error, message: "Failed to open Realm in background") - task.setTaskCompleted(success: false) - return - } - var expired = false - task.expirationHandler = { - expired = true - } - - // Schedule next processing task - scheduleAppProcessing() - - Task { - defer { - task.setTaskCompleted(success: true) - } - - // If possible, cleaning every week - if NCPreferences().cleaningWeek() { - // BGTask expiration flag - nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Start cleaning week") - let tblAccounts = await NCManageDatabase.shared.getAllTableAccountAsync() - for tblAccount in tblAccounts { - await NCManageDatabase.shared.cleanTablesOcIds(account: tblAccount.account, userId: tblAccount.userId, urlBase: tblAccount.urlBase) - guard !expired else { return } - } - await NCUtilityFileSystem().cleanUpAsync() - - NCPreferences().setDoneCleaningWeek() - nkLog(tag: self.global.logTagBgSync, emoji: .stop, message: "Stop cleaning week") - } else { - await backgroundSync(task: task) - } - } - } - - // Executes the background synchronization flow for Auto Upload. - // - // The function: - // - discovers new Auto Upload items, - // - fetches pending metadata, - // - creates missing folders when required, - // - checks server-side existence, - // - expands seeds into concrete metadata items, - // - queues uploads sequentially. - // - // The flow is cooperative with BGTask expiration and stops as soon as expiration is detected. - // - // - Parameter task: Optional background task used to observe expiration. - func backgroundSync(task: BGTask? = nil) async { - let expirationState = BackgroundSyncExpirationState() - - task?.expirationHandler = { - Task { - await expirationState.markExpired() - } - } - - // Discover new items for Auto Upload. - let numAutoUpload = await NCAutoUpload.shared.initAutoUpload() - nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Auto upload found \(numAutoUpload) new items") - - guard !(await expirationState.isExpired()) else { - return - } - - // Fetch pending metadata. - let metadatas = await NCManageDatabase.shared.getMetadataProcess() - guard !metadatas.isEmpty, !(await expirationState.isExpired()) else { - return - } - - // Create all pending Auto Upload folders (fail-fast). - let pendingCreateFolders = metadatas.lazy.filter { - $0.status == self.global.metadataStatusWaitCreateFolder && - $0.sessionSelector == self.global.selectorUploadAutoUpload - } - - // Resolve capabilities once per account. - let accounts = Array(Set(pendingCreateFolders.map { $0.account })) - var capabilitiesByAccount: [String: NKCapabilities.Capabilities] = [:] - - for account in accounts { - guard !(await expirationState.isExpired()) else { - return - } - - let capabilities = await NKCapabilities.shared.getCapabilities(for: account) - capabilitiesByAccount[account] = capabilities - } - - for metadata in pendingCreateFolders { - guard !(await expirationState.isExpired()) else { - return - } - - // If server supports auto MKCOL (Nextcloud >= 33), skip manual folder creation. - if let capabilities = capabilitiesByAccount[metadata.account] { - let autoMkcol = capabilities.serverVersionMajor >= NCGlobal.shared.nextcloudVersion33 - if autoMkcol { - continue - } - } - - let err = await NCNetworking.shared.createFolderForAutoUpload( - serverUrlFileName: metadata.serverUrlFileName, - account: metadata.account - ) - - // Fail-fast: abort the whole sync on first failure. - if err != .success { - nkLog( - tag: self.global.logTagBgSync, - emoji: .error, - message: "Create folder '\(metadata.serverUrlFileName)' failed: \(err.errorCode) – aborting sync" - ) - return - } - } - - // Compute available capacity. - let downloading = metadatas.lazy.filter { $0.status == self.global.metadataStatusDownloading }.count - let uploading = metadatas.lazy.filter { $0.status == self.global.metadataStatusUploading }.count - let availableProcess = max(0, NCBrandOptions.shared.numMaximumProcess - (downloading + uploading)) - - // Select Auto Upload candidates. - let metadatasToUpload = Array( - metadatas.lazy.filter { - $0.status == self.global.metadataStatusWaitUpload && - $0.sessionSelector == self.global.selectorUploadAutoUpload && - $0.chunk == 0 - } - .prefix(availableProcess) - ) - - let cameraRoll = NCCameraRoll() - - for metadata in metadatasToUpload { - guard !(await expirationState.isExpired()) else { - return - } - - // Check whether the file already exists remotely. - let existsResult = await NCNetworking.shared.fileExists( - serverUrlFileName: metadata.serverUrlFileName, - account: metadata.account - ) - - if existsResult == .success { - // File exists remotely: remove local metadata and continue. - await NCManageDatabase.shared.deleteMetadataAsync(id: metadata.ocId) - continue - } else if existsResult.errorCode != 404 { - // Ignore transient or server-side errors and continue with the next item. - continue - } - - // Expand the seed into concrete metadata entries (for example, Live Photo pairs). - let extractedMetadatas = await cameraRoll.extractCameraRoll(from: metadata) - - guard !(await expirationState.isExpired()) else { - return - } - - for extractedMetadata in extractedMetadatas { - guard !(await expirationState.isExpired()) else { - return - } - - let err = await NCNetworking.shared.uploadFileInBackground( - metadata: extractedMetadata.detachedCopy() - ) - - if err == .success { - nkLog( - tag: self.global.logTagBgSync, - message: "In queued upload \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl)" - ) - } else { - nkLog( - tag: self.global.logTagBgSync, - emoji: .error, - message: "Upload failed \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl) [\(err.errorDescription)]" - ) - } - } - } - } - - actor BackgroundSyncExpirationState { - private var expired = false - - // Marks the background sync as expired. - func markExpired() { - expired = true - } - - // Returns whether the background sync has expired. - func isExpired() -> Bool { - expired - } - } - // MARK: - Background Networking Session func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 3bea5f4204..5e67ead23b 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -118,7 +118,7 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") Task.detached { - await appDelegate.backgroundSync() + await appDelegate.autoUploadBackgroundSync() } } From 3c688a6b1d4683b5c9c4d943e7a4c5a00ffb5613 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Apr 2026 11:34:26 +0200 Subject: [PATCH 3/4] clean Signed-off-by: Marino Faggiana --- iOSClient/AppDelegate+AppProcessing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/AppDelegate+AppProcessing.swift b/iOSClient/AppDelegate+AppProcessing.swift index 1c9df56052..1a1c8f0133 100644 --- a/iOSClient/AppDelegate+AppProcessing.swift +++ b/iOSClient/AppDelegate+AppProcessing.swift @@ -80,7 +80,7 @@ extension AppDelegate { nkLog(tag: self.global.logTagBgSync, emoji: .stop, message: "Stop cleaning week") return true } else { - await autoUploadBackgroundSync() + await self.autoUploadBackgroundSync() return !Task.isCancelled } } From 8d15fe842614757905109aca021ac93882835314 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Apr 2026 11:52:14 +0200 Subject: [PATCH 4/4] cleaning Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 - iOSClient/AppDelegate+AppProcessing.swift | 2 +- iOSClient/AppDelegate+AppRefresh.swift | 2 +- ...AppDelegate+AutoUploadBackgroundSync.swift | 139 ------------------ .../NCBackgroundLocationUploadManager.swift | 2 +- iOSClient/Networking/NCAutoUpload.swift | 132 +++++++++++++++++ 6 files changed, 135 insertions(+), 146 deletions(-) delete mode 100644 iOSClient/AppDelegate+AutoUploadBackgroundSync.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 2644d821cf..8c403d47c1 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -232,7 +232,6 @@ F710FC88277B7D3F00AA9FBF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F710FC87277B7D3F00AA9FBF /* RealmSwift */; }; F7110AE02F9773230095AA5C /* AppDelegate+AppRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */; }; F7110AE42F9774140095AA5C /* AppDelegate+AppProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */; }; - F7110AE62F977A680095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7110AE52F977A630095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift */; }; F711A4DC2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; }; F711A4DD2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; }; F711A4DE2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; }; @@ -1336,7 +1335,6 @@ F710D2012405826100A6033D /* NCContextMenuViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCContextMenuViewer.swift; sourceTree = ""; }; F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AppRefresh.swift"; sourceTree = ""; }; F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AppProcessing.swift"; sourceTree = ""; }; - F7110AE52F977A630095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AutoUploadBackgroundSync.swift"; sourceTree = ""; }; F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCUtility+Date.swift"; sourceTree = ""; }; F7132C6B2D085AD200B42D6A /* NCTermOfServiceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTermOfServiceModel.swift; sourceTree = ""; }; F7132C6C2D085AD200B42D6A /* NCTermOfServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTermOfServiceView.swift; sourceTree = ""; }; @@ -3295,7 +3293,6 @@ F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */, F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */, - F7110AE52F977A630095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift */, F7CAFE1A2F16AA8600DB35A5 /* main.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */, @@ -4518,7 +4515,6 @@ F799DF822C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */, F76B649C2ADFFAED00014640 /* NCImageCache.swift in Sources */, F7110AE42F9774140095AA5C /* AppDelegate+AppProcessing.swift in Sources */, - F7110AE62F977A680095AA5C /* AppDelegate+AutoUploadBackgroundSync.swift in Sources */, F76341182EBE0BC60056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */, F34E1AD92ECC839100FA10C3 /* EmojiTextField.swift in Sources */, diff --git a/iOSClient/AppDelegate+AppProcessing.swift b/iOSClient/AppDelegate+AppProcessing.swift index 1a1c8f0133..467bc1fd64 100644 --- a/iOSClient/AppDelegate+AppProcessing.swift +++ b/iOSClient/AppDelegate+AppProcessing.swift @@ -80,7 +80,7 @@ extension AppDelegate { nkLog(tag: self.global.logTagBgSync, emoji: .stop, message: "Stop cleaning week") return true } else { - await self.autoUploadBackgroundSync() + await NCAutoUpload.shared.autoUploadBackgroundSync() return !Task.isCancelled } } diff --git a/iOSClient/AppDelegate+AppRefresh.swift b/iOSClient/AppDelegate+AppRefresh.swift index 29f46d2a02..c7b0d8b8bd 100644 --- a/iOSClient/AppDelegate+AppRefresh.swift +++ b/iOSClient/AppDelegate+AppRefresh.swift @@ -47,7 +47,7 @@ extension AppDelegate { scheduleAppRefresh() let refreshTask = Task { () -> Bool in - await self.autoUploadBackgroundSync() + await NCAutoUpload.shared.autoUploadBackgroundSync() return !Task.isCancelled } diff --git a/iOSClient/AppDelegate+AutoUploadBackgroundSync.swift b/iOSClient/AppDelegate+AutoUploadBackgroundSync.swift deleted file mode 100644 index 0aeb0ec3a0..0000000000 --- a/iOSClient/AppDelegate+AutoUploadBackgroundSync.swift +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2026 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import UIKit -import NextcloudKit -import BackgroundTasks - -extension AppDelegate { - // Executes the background synchronization flow for Auto Upload. - // - // The function: - // - discovers new Auto Upload items, - // - fetches pending metadata, - // - creates missing folders when required, - // - checks remote existence, - // - expands seeds into concrete metadata items, - // - queues uploads sequentially. - // - // The flow cooperates with Swift task cancellation triggered by BGTask expiration. - func autoUploadBackgroundSync() async { - guard !Task.isCancelled else { return } - - // Discover new items for Auto Upload. - let numAutoUpload = await NCAutoUpload.shared.initAutoUpload() - nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Auto upload found \(numAutoUpload) new items") - - guard !Task.isCancelled else { return } - - // Fetch pending metadata. - let metadatas = await NCManageDatabase.shared.getMetadataProcess() - guard !metadatas.isEmpty, !Task.isCancelled else { - return - } - - // Create all pending Auto Upload folders (fail-fast). - let pendingCreateFolders = metadatas.lazy.filter { - $0.status == self.global.metadataStatusWaitCreateFolder && - $0.sessionSelector == self.global.selectorUploadAutoUpload - } - - // Resolve capabilities once per account. - let accounts = Array(Set(pendingCreateFolders.map { $0.account })) - var capabilitiesByAccount: [String: NKCapabilities.Capabilities] = [:] - - for account in accounts { - guard !Task.isCancelled else { return } - - let capabilities = await NKCapabilities.shared.getCapabilities(for: account) - capabilitiesByAccount[account] = capabilities - } - - for metadata in pendingCreateFolders { - guard !Task.isCancelled else { return } - - // If server supports auto MKCOL (Nextcloud >= 33), skip manual folder creation. - if let capabilities = capabilitiesByAccount[metadata.account] { - let autoMkcol = capabilities.serverVersionMajor >= NCGlobal.shared.nextcloudVersion33 - if autoMkcol { - continue - } - } - - let err = await NCNetworking.shared.createFolderForAutoUpload( - serverUrlFileName: metadata.serverUrlFileName, - account: metadata.account - ) - - if err != .success { - nkLog( - tag: self.global.logTagBgSync, - emoji: .error, - message: "Create folder '\(metadata.serverUrlFileName)' failed: \(err.errorCode) – aborting sync" - ) - return - } - } - - // Compute available capacity. - let downloading = metadatas.lazy.filter { $0.status == self.global.metadataStatusDownloading }.count - let uploading = metadatas.lazy.filter { $0.status == self.global.metadataStatusUploading }.count - let availableProcess = max(0, NCBrandOptions.shared.numMaximumProcess - (downloading + uploading)) - - // Select Auto Upload candidates. - let metadatasToUpload = Array( - metadatas.lazy.filter { - $0.status == self.global.metadataStatusWaitUpload && - $0.sessionSelector == self.global.selectorUploadAutoUpload && - $0.chunk == 0 - } - .prefix(availableProcess) - ) - - let cameraRoll = NCCameraRoll() - - for metadata in metadatasToUpload { - guard !Task.isCancelled else { return } - - // Check whether the file already exists remotely. - let existsResult = await NCNetworking.shared.fileExists( - serverUrlFileName: metadata.serverUrlFileName, - account: metadata.account - ) - - if existsResult == .success { - await NCManageDatabase.shared.deleteMetadataAsync(id: metadata.ocId) - continue - } else if existsResult.errorCode != 404 { - continue - } - - // Expand the seed into concrete metadata entries (for example, Live Photo pairs). - let extractedMetadatas = await cameraRoll.extractCameraRoll(from: metadata) - - guard !Task.isCancelled else { return } - - for extractedMetadata in extractedMetadatas { - guard !Task.isCancelled else { return } - - let err = await NCNetworking.shared.uploadFileInBackground( - metadata: extractedMetadata.detachedCopy() - ) - - if err == .success { - nkLog( - tag: self.global.logTagBgSync, - message: "In queued upload \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl)" - ) - } else { - nkLog( - tag: self.global.logTagBgSync, - emoji: .error, - message: "Upload failed \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl) [\(err.errorDescription)]" - ) - } - } - } - } -} diff --git a/iOSClient/NCBackgroundLocationUploadManager.swift b/iOSClient/NCBackgroundLocationUploadManager.swift index 5e67ead23b..154fda662d 100644 --- a/iOSClient/NCBackgroundLocationUploadManager.swift +++ b/iOSClient/NCBackgroundLocationUploadManager.swift @@ -118,7 +118,7 @@ class NCBackgroundLocationUploadManager: NSObject, CLLocationManagerDelegate { nkLog(tag: self.global.logTagLocation, emoji: .start, message: "Triggered by location change: \(location?.coordinate.latitude ?? 0), \(location?.coordinate.longitude ?? 0)") Task.detached { - await appDelegate.autoUploadBackgroundSync() + await NCAutoUpload.shared.autoUploadBackgroundSync() } } diff --git a/iOSClient/Networking/NCAutoUpload.swift b/iOSClient/Networking/NCAutoUpload.swift index 80adf58676..c99d1085e9 100644 --- a/iOSClient/Networking/NCAutoUpload.swift +++ b/iOSClient/Networking/NCAutoUpload.swift @@ -255,4 +255,136 @@ class NCAutoUpload: NSObject { return(Array(newAssets), fileNames) } + + // MARK: - + + // Executes the background synchronization flow for Auto Upload. + // + // The function: + // - discovers new Auto Upload items, + // - fetches pending metadata, + // - creates missing folders when required, + // - checks remote existence, + // - expands seeds into concrete metadata items, + // - queues uploads sequentially. + // + // The flow cooperates with Swift task cancellation triggered by BGTask expiration. + func autoUploadBackgroundSync() async { + guard !Task.isCancelled else { return } + + // Discover new items for Auto Upload. + let numAutoUpload = await initAutoUpload() + nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Auto upload found \(numAutoUpload) new items") + + guard !Task.isCancelled else { return } + + // Fetch pending metadata. + let metadatas = await NCManageDatabase.shared.getMetadataProcess() + guard !metadatas.isEmpty, !Task.isCancelled else { + return + } + + // Create all pending Auto Upload folders (fail-fast). + let pendingCreateFolders = metadatas.lazy.filter { + $0.status == self.global.metadataStatusWaitCreateFolder && + $0.sessionSelector == self.global.selectorUploadAutoUpload + } + + // Resolve capabilities once per account. + let accounts = Array(Set(pendingCreateFolders.map { $0.account })) + var capabilitiesByAccount: [String: NKCapabilities.Capabilities] = [:] + + for account in accounts { + guard !Task.isCancelled else { return } + + let capabilities = await NKCapabilities.shared.getCapabilities(for: account) + capabilitiesByAccount[account] = capabilities + } + + for metadata in pendingCreateFolders { + guard !Task.isCancelled else { return } + + // If server supports auto MKCOL (Nextcloud >= 33), skip manual folder creation. + if let capabilities = capabilitiesByAccount[metadata.account] { + let autoMkcol = capabilities.serverVersionMajor >= NCGlobal.shared.nextcloudVersion33 + if autoMkcol { + continue + } + } + + let err = await NCNetworking.shared.createFolderForAutoUpload( + serverUrlFileName: metadata.serverUrlFileName, + account: metadata.account + ) + + if err != .success { + nkLog( + tag: self.global.logTagBgSync, + emoji: .error, + message: "Create folder '\(metadata.serverUrlFileName)' failed: \(err.errorCode) – aborting sync" + ) + return + } + } + + // Compute available capacity. + let downloading = metadatas.lazy.filter { $0.status == self.global.metadataStatusDownloading }.count + let uploading = metadatas.lazy.filter { $0.status == self.global.metadataStatusUploading }.count + let availableProcess = max(0, NCBrandOptions.shared.numMaximumProcess - (downloading + uploading)) + + // Select Auto Upload candidates. + let metadatasToUpload = Array( + metadatas.lazy.filter { + $0.status == self.global.metadataStatusWaitUpload && + $0.sessionSelector == self.global.selectorUploadAutoUpload && + $0.chunk == 0 + } + .prefix(availableProcess) + ) + + let cameraRoll = NCCameraRoll() + + for metadata in metadatasToUpload { + guard !Task.isCancelled else { return } + + // Check whether the file already exists remotely. + let existsResult = await NCNetworking.shared.fileExists( + serverUrlFileName: metadata.serverUrlFileName, + account: metadata.account + ) + + if existsResult == .success { + await NCManageDatabase.shared.deleteMetadataAsync(id: metadata.ocId) + continue + } else if existsResult.errorCode != 404 { + continue + } + + // Expand the seed into concrete metadata entries (for example, Live Photo pairs). + let extractedMetadatas = await cameraRoll.extractCameraRoll(from: metadata) + + guard !Task.isCancelled else { return } + + for extractedMetadata in extractedMetadatas { + guard !Task.isCancelled else { return } + + let err = await NCNetworking.shared.uploadFileInBackground( + metadata: extractedMetadata.detachedCopy() + ) + + if err == .success { + nkLog( + tag: self.global.logTagBgSync, + message: "In queued upload \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl)" + ) + } else { + nkLog( + tag: self.global.logTagBgSync, + emoji: .error, + message: "Upload failed \(extractedMetadata.fileName) -> \(extractedMetadata.serverUrl) [\(err.errorDescription)]" + ) + } + } + } + } }