diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 298fdfaf96..8c403d47c1 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -230,6 +230,8 @@ 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 */; }; 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 +1333,8 @@ 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 = ""; }; 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 +3291,8 @@ children = ( AA517BB42D66149900F8D37C /* .tx */, F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, + F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */, + F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */, F7CAFE1A2F16AA8600DB35A5 /* main.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */, @@ -4508,6 +4514,7 @@ F70753F12542A9A200972D44 /* NCViewerMedia.swift in Sources */, F799DF822C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */, F76B649C2ADFFAED00014640 /* NCImageCache.swift in Sources */, + F7110AE42F9774140095AA5C /* AppDelegate+AppProcessing.swift in Sources */, F76341182EBE0BC60056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */, F34E1AD92ECC839100FA10C3 /* EmojiTextField.swift in Sources */, @@ -4531,6 +4538,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 */, @@ -5790,7 +5798,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 +5826,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 +5866,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 +5892,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+AppProcessing.swift b/iOSClient/AppDelegate+AppProcessing.swift new file mode 100644 index 0000000000..467bc1fd64 --- /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 NCAutoUpload.shared.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..c7b0d8b8bd --- /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 NCAutoUpload.shared.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.swift b/iOSClient/AppDelegate.swift index f237c62852..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,214 +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) - } - } - } - - 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 { } - } - } - - // BGTask expiration flag - var expired = false - task?.expirationHandler = { - expired = true - } - - // 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 - let metadatas = await NCManageDatabase.shared.getMetadataProcess() - guard !metadatas.isEmpty, !expired 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 - } - - // Get accounts -> Capabilities - let accounts = Array(Set(pendingCreateFolders.map { $0.account })) - var capabilitiesByAccount: [String: NKCapabilities.Capabilities] = [:] - for account in accounts { - let capabilities = await NKCapabilities.shared.getCapabilities(for: account) - capabilitiesByAccount[account] = capabilities - } - - for metadata in pendingCreateFolders { - guard !expired 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 - } - } - // Create folder - 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 - } - } - - // Capacity computation - 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 - 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 !expired else { return } - - // 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 - 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.) - continue - } - - // Expand seed into concrete metadatas (e.g., Live Photo pair) - let extracted = await cameraRoll.extractCameraRoll(from: metadata) - guard !expired else { return } - - 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)") - } else { - nkLog(tag: self.global.logTagBgSync, emoji: .error, message: "Upload failed \(metadata.fileName) -> \(metadata.serverUrl) [\(err.errorDescription)]") - } - guard !expired else { return } - } - } - } - // 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..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.backgroundSync() + 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)]" + ) + } + } + } + } }