diff --git a/BackgroundUploadExtension/BackgroundUploadExtension+Accounts.swift b/BackgroundUploadExtension/BackgroundUploadExtension+Accounts.swift new file mode 100644 index 0000000000..fea05bbfe4 --- /dev/null +++ b/BackgroundUploadExtension/BackgroundUploadExtension+Accounts.swift @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Photos +import NextcloudKit + +extension BackgroundUploadExtension { + func setupAccount() async -> tableAccount? { + guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { + logDebug("Background upload account setup skipped: Photos authorization is not granted") + return nil + } + + guard !NCPreferences().formatCompatibility else { + logDebug("Background upload account setup skipped: compatibility format is enabled") + return nil + } + + guard let account = await database.getTableAccountAsync(predicate: NSPredicate(format: "autoUploadStart == true")) else { + logDebug("Background upload account setup skipped: no Auto Upload account") + return nil + } + + NextcloudKit.shared.appendSession( + account: account.account, + urlBase: account.urlBase, + user: account.user, + userId: account.userId, + password: NCPreferences().getPassword(account: account.account), + userAgent: userAgent, + httpMaximumConnectionsPerHost: NCBrandOptions.shared.httpMaximumConnectionsPerHost, + httpMaximumConnectionsPerHostInDownload: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload, + httpMaximumConnectionsPerHostInUpload: NCBrandOptions.shared.httpMaximumConnectionsPerHostInUpload, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup + ) + + guard let capabilities = await database.getCapabilities(account: account.account) else { + logError("Background upload account setup failed: capabilities not found for \(account.account)") + return nil + } + + guard NCBrandOptions.shared.isServerVersion( + capabilities, + greaterOrEqualTo: .v33 + ) else { + logInfo("Background upload extension stopped because account \(account.account) uses a server lower than version 33", persist: true) + return nil + } + + return account + } +} diff --git a/BackgroundUploadExtension/BackgroundUploadExtension+Destination.swift b/BackgroundUploadExtension/BackgroundUploadExtension+Destination.swift new file mode 100644 index 0000000000..9291aeab75 --- /dev/null +++ b/BackgroundUploadExtension/BackgroundUploadExtension+Destination.swift @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Photos +import NextcloudKit + +extension BackgroundUploadExtension { + func buildDestination(metadata: tableMetadata, asset: PHAsset) -> URLRequest? { + guard let url = metadata.serverUrlFileName.encodedToUrl as? URL else { + logError("Invalid destination URL: \(metadata.serverUrlFileName)") + return nil + } + + guard let nkSession = nkComm.nksessions.session(forAccount: metadata.account) else { + logError("Session not found for account: \(metadata.account)") + return nil + } + + let wifiOnly = metadata.session == nkComm.identifierSessionUploadBackgroundWWan + let loginString = "\(nkSession.user):\(nkSession.password)" + var request = URLRequest(url: url) + + guard let loginData = loginString.data(using: .utf8) else { + logError("Unable to encode credentials for account: \(metadata.account)") + return nil + } + + request.httpMethod = "PUT" + request.allowsCellularAccess = !wifiOnly + request.allowsExpensiveNetworkAccess = true + request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue("Basic \(loginData.base64EncodedString())", forHTTPHeaderField: "Authorization") + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + request.setValue("1", forHTTPHeaderField: "X-NC-WebDAV-Auto-Mkcol") + + if let creationDate = asset.creationDate, + creationDate.timeIntervalSince1970 > 0 { + request.setValue("\(creationDate.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-CTime") + } + + if let modificationDate = asset.modificationDate, + modificationDate.timeIntervalSince1970 > 0 { + request.setValue("\(modificationDate.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-MTime") + } + + logDebug("Destination created for \(metadata.fileName) -> \(metadata.serverUrlFileName)") + + return request + } +} diff --git a/BackgroundUploadExtension/BackgroundUploadExtension+Discovery.swift b/BackgroundUploadExtension/BackgroundUploadExtension+Discovery.swift new file mode 100644 index 0000000000..f226404111 --- /dev/null +++ b/BackgroundUploadExtension/BackgroundUploadExtension+Discovery.swift @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Photos +import NextcloudKit + +extension BackgroundUploadExtension { + func createPendingMetadatas(account: tableAccount, limit: Int) async -> Bool { + guard limit > 0, + account.autoUploadImage || account.autoUploadVideo else { + return false + } + + let autoUploadServerUrlBase = await database.getAccountAutoUploadServerUrlBaseAsync( + account: account.account, + urlBase: account.urlBase, + userId: account.userId + ) + + var skipFileNames = await database.fetchSkipFileNamesAsync(account: account.account, autoUploadServerUrlBase: autoUploadServerUrlBase) + + var skipAssetLocalIdentifiers = await database.fetchSkipAssetLocalIdentifiersAsync( + account: account.account, + autoUploadServerUrlBase: autoUploadServerUrlBase + ) + + let fetchOptions = PHFetchOptions() + var mediaPredicates: [NSPredicate] = [] + + if account.autoUploadImage { + mediaPredicates.append(NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue)) + } + + if account.autoUploadVideo { + mediaPredicates.append(NSPredicate(format: "mediaType == %d", PHAssetMediaType.video.rawValue)) + } + + var predicates: [NSPredicate] = [NSCompoundPredicate(orPredicateWithSubpredicates: mediaPredicates)] + + if let sinceDate = account.autoUploadSinceDate { + predicates.append(NSPredicate(format: "creationDate >= %@", sinceDate as NSDate)) + } else if let lastDate = await database.fetchLastAutoUploadedDateAsync( + account: account.account, + autoUploadServerUrlBase: autoUploadServerUrlBase + ) { + predicates.append(NSPredicate(format: "creationDate >= %@", lastDate as NSDate)) + } + + fetchOptions.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] + + var assetsByIdentifier: [String: PHAsset] = [:] + + for collection in autoUploadCollections(for: account) { + let assets = PHAsset.fetchAssets(in: collection, options: fetchOptions) + + assets.enumerateObjects { asset, _, _ in + assetsByIdentifier[asset.localIdentifier] = asset + } + } + + let assets = assetsByIdentifier.values.sorted { + ($0.creationDate ?? .distantPast) < + ($1.creationDate ?? .distantPast) + } + + var remaining = limit + var madeProgress = false + var lastQueuedDate: Date? + + for asset in assets { + guard remaining > 0 else { + break + } + + guard !skipAssetLocalIdentifiers.contains(asset.localIdentifier) else { + continue + } + + guard let resource = primaryUploadResource(for: asset), + let originalFileName = resource.filename, + !originalFileName.isEmpty else { + logError("Upload resource not found for asset \(asset.localIdentifier)") + continue + } + + let creationDate = asset.creationDate ?? Date() + let fileName = utilityFileSystem.createFileName(originalFileName, fileDate: creationDate, fileType: asset.mediaType) + + guard !skipFileNames.contains(fileName) else { + continue + } + + guard await createPendingMetadata(asset: asset, resource: resource, fileName: fileName, account: account) != nil else { + continue + } + + skipFileNames.insert(fileName) + skipAssetLocalIdentifiers.insert(asset.localIdentifier) + lastQueuedDate = creationDate + remaining -= 1 + madeProgress = true + } + + if let lastQueuedDate { + await database.updateAccountPropertyAsync(\.autoUploadSinceDate, value: lastQueuedDate, account: account.account) + } + + return madeProgress + } + + private func createPendingMetadata(asset: PHAsset, resource: PHAssetResource, fileName: String, account: tableAccount) async -> tableMetadata? { + let session = NCSession.Session( + account: account.account, + urlBase: account.urlBase, + user: account.user, + userId: account.userId + ) + + let autoUploadServerUrlBase = await database.getAccountAutoUploadServerUrlBaseAsync( + account: account.account, + urlBase: account.urlBase, + userId: account.userId + ) + + let serverUrl: String + let wifiOnly = asset.mediaType == .image ? account.autoUploadWWAnPhoto : account.autoUploadWWAnVideo + + if account.autoUploadCreateSubfolder { + serverUrl = utilityFileSystem.createGranularityPath(asset: asset, serverUrlBase: autoUploadServerUrlBase, granularity: account.autoUploadSubfolderGranularity) + } else { + serverUrl = autoUploadServerUrlBase + } + + let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync( + fileName: fileName, + ocId: UUID().uuidString, + serverUrl: serverUrl, + session: session, + sceneIdentifier: nil + ) + + metadata.assetLocalIdentifier = asset.localIdentifier + metadata.autoUploadServerUrlBase = autoUploadServerUrlBase + metadata.nativeFormat = true + metadata.contentType = resource.contentType.preferredMIMEType ?? "application/octet-stream" + metadata.typeIdentifier = resource.contentType.identifier + metadata.size = Int64(resource.dataSize ?? 0) + metadata.width = asset.pixelWidth + metadata.height = asset.pixelHeight + + if let creationDate = asset.creationDate { + metadata.creationDate = creationDate as NSDate + } + + if let modificationDate = asset.modificationDate { + metadata.date = modificationDate as NSDate + } + + metadata.session = wifiOnly ? nkComm.identifierSessionUploadBackgroundWWan : nkComm.identifierSessionUploadBackground + metadata.sessionSelector = global.selectorUploadAutoUpload + metadata.sessionDate = Date() + metadata.status = global.metadataStatusWaitUpload + metadata.backgroundUploadJobIdentifier = "pending" + + await database.addMetadataAsync(metadata) + + logDebug("Created pending metadata for \(fileName), account: \(account.account), asset: \(asset.localIdentifier)") + + return metadata + } + + private func primaryUploadResource(for asset: PHAsset) -> PHAssetResource? { + let resources = PHAssetResource.assetResources(for: asset) + + switch asset.mediaType { + case .image: + return resources.first { + $0.type == .fullSizePhoto + } ?? resources.first { + $0.type == .photo + } + + case .video: + return resources.first { + $0.type == .fullSizeVideo + } ?? resources.first { + $0.type == .video + } + + default: + return nil + } + } + + private func autoUploadCollections(for account: tableAccount) -> [PHAssetCollection] { + let albumIds = NCPreferences().getAutoUploadAlbumIds(account: account.account) + + if !albumIds.isEmpty { + let result = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: Array(albumIds), options: nil) + var collections: [PHAssetCollection] = [] + + result.enumerateObjects { collection, _, _ in + collections.append(collection) + } + + if !collections.isEmpty { + return collections + } + } + + let result = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil) + + guard let cameraRoll = result.firstObject else { + return [] + } + + return [cameraRoll] + } +} diff --git a/BackgroundUploadExtension/BackgroundUploadExtension+Jobs.swift b/BackgroundUploadExtension/BackgroundUploadExtension+Jobs.swift new file mode 100644 index 0000000000..aa5c501c81 --- /dev/null +++ b/BackgroundUploadExtension/BackgroundUploadExtension+Jobs.swift @@ -0,0 +1,426 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Photos +import NextcloudKit + +extension BackgroundUploadExtension { + func createUploadJobs(account: tableAccount) async throws -> Bool { + let availableJobs = availableUploadJobSlots() + + guard availableJobs > 0 else { + logDebug("No available background upload job slots") + return false + } + + let predicate = NSPredicate( + format: """ + status == %d AND \ + backgroundUploadJobIdentifier == %@ AND \ + account == %@ + """, + global.metadataStatusWaitUpload, + "pending", + account.account + ) + + guard let metadatas = await database.getMetadatasAsync( + predicate: predicate, + sortedByKeyPath: "sessionDate", + ascending: true, + limit: availableJobs + ), + !metadatas.isEmpty else { + return false + } + + let library = PHPhotoLibrary.shared() + var madeProgress = false + + for metadata in metadatas { + let assets = PHAsset.fetchAssets(withLocalIdentifiers: [metadata.assetLocalIdentifier], options: nil) + + guard let asset = assets.firstObject else { + logError("Asset not found: \(metadata.assetLocalIdentifier), file: \(metadata.fileName)") + continue + } + + guard let resource = uploadResource(for: asset, metadata: metadata) else { + logError("Upload resource not found for asset \(metadata.assetLocalIdentifier)") + continue + } + + guard let destination = buildDestination(metadata: metadata, asset: asset) else { + continue + } + + var jobIdentifier: String? + + try library.performChangesAndWait { + let request = PHAssetResourceUploadJobChangeRequest.creationRequestForJob(destination: destination, resource: resource) + jobIdentifier = request.placeholderForCreatedAssetResourceUploadJob?.localIdentifier + } + + guard let jobIdentifier, !jobIdentifier.isEmpty else { + logError("Created job has no local identifier") + continue + } + + metadata.backgroundUploadJobIdentifier = jobIdentifier + metadata.status = global.metadataStatusUploading + metadata.sessionDate = Date() + metadata.sessionError = "" + metadata.errorCode = 0 + + await database.replaceMetadataAsync(ocId: metadata.ocId, metadata: metadata) + + madeProgress = true + + logInfo("Created background upload job \(jobIdentifier), file: \(metadata.fileName), resource: \(resource.filename ?? "")") + } + + return madeProgress + } + + func cancelRequestedUploadJobs() async throws -> Bool { + let library = PHPhotoLibrary.shared() + var madeProgress = false + let cancellableJobs = PHAssetResourceUploadJob.fetchJobs(action: .process, options: nil) + + for index in 0.. Bool { + let jobs = PHAssetResourceUploadJob.fetchJobs(action: .retry, options: nil) + + guard jobs.count > 0 else { + return false + } + + let library = PHPhotoLibrary.shared() + var madeProgress = false + + for index in 0.."), code: \(error?.code ?? 0), description: \(error?.localizedDescription ?? ""), headers: \(job.responseHeaderFields ?? [:])") + + let authenticationRequired = job.responseHeaderFields?["www-authenticate"] != nil || (error?.domain == NSURLErrorDomain && error?.code == URLError.userAuthenticationRequired.rawValue) + + if authenticationRequired { + await updateMetadataForUploadFailure(metadata: metadata, job: job) + + guard try acknowledge(job: job, library: library) else { + logError("Unable to acknowledge authentication-failed job \(jobIdentifier)") + continue + } + + metadata.backgroundUploadJobIdentifier = "" + metadata.backgroundUploadNextRetryDate = nil + await database.replaceMetadataAsync(ocId: metadata.ocId, metadata: metadata) + + madeProgress = true + logError("Stopped background upload after authentication failure for \(metadata.fileName), job: \(jobIdentifier)") + continue + } + + let assets = PHAsset.fetchAssets(withLocalIdentifiers: [metadata.assetLocalIdentifier], options: nil) + + guard let asset = assets.firstObject else { + logError("Retry asset not found for job \(jobIdentifier), asset: \(metadata.assetLocalIdentifier)") + continue + } + + guard let destination = buildDestination(metadata: metadata, asset: asset) else { + logError("Unable to rebuild destination for job \(jobIdentifier)") + continue + } + + var retryRequested = false + + try library.performChangesAndWait { + guard let request = PHAssetResourceUploadJobChangeRequest(for: job) else { + return + } + + request.retry(destination: destination) + retryRequested = true + } + + guard retryRequested else { + logError("Unable to create retry request for job \(jobIdentifier)") + continue + } + + if metadata.backgroundUploadRetryCount < Int.max { + metadata.backgroundUploadRetryCount += 1 + } + + metadata.backgroundUploadNextRetryDate = nil + metadata.sessionDate = Date() + metadata.sessionError = "" + metadata.errorCode = 0 + metadata.status = global.metadataStatusUploading + + await database.replaceMetadataAsync(ocId: metadata.ocId, metadata: metadata) + + madeProgress = true + + logInfo("Retry requested for \(metadata.fileName), job: \(jobIdentifier)") + } + + return madeProgress + } + + func acknowledgeUploadJobs() async throws -> Bool { + let jobs = PHAssetResourceUploadJob.fetchJobs(action: .acknowledge, options: nil) + + guard jobs.count > 0 else { + return false + } + + let library = PHPhotoLibrary.shared() + var madeProgress = false + + for index in 0.. PHAssetResource? { + let resources = PHAssetResource.assetResources(for: asset) + + if let resource = resources.first(where: { + $0.filename?.caseInsensitiveCompare(metadata.fileName) == .orderedSame + }) { + return resource + } + + switch asset.mediaType { + case .image: + return resources.first(where: { + $0.type == .fullSizePhoto + }) ?? resources.first(where: { + $0.type == .photo + }) + + case .video: + return resources.first(where: { + $0.type == .fullSizeVideo + }) ?? resources.first(where: { + $0.type == .video + }) + + default: + return nil + } + } + + private func acknowledge(job: PHAssetResourceUploadJob, library: PHPhotoLibrary) throws -> Bool { + var acknowledged = false + + try library.performChangesAndWait { + guard let request = PHAssetResourceUploadJobChangeRequest(for: job) else { + return + } + + request.acknowledge() + acknowledged = true + } + + return acknowledged + } + + private func cancel(job: PHAssetResourceUploadJob, library: PHPhotoLibrary) throws -> Bool { + var cancelled = false + + try library.performChangesAndWait { + guard let request = PHAssetResourceUploadJobChangeRequest(for: job) else { + return + } + + request.cancel() + cancelled = true + } + + return cancelled + } + + func availableUploadJobSlots() -> Int { + let actions: [PHAssetResourceUploadJob.Action] = [.process, .retry, .acknowledge] + var jobIdentifiers = Set() + + for action in actions { + let jobs = PHAssetResourceUploadJob.fetchJobs(action: action, options: nil) + + for index in 0.. Bool { + PHAssetResourceUploadJob.fetchJobs(action: .process, options: nil).count > 0 || + PHAssetResourceUploadJob.fetchJobs(action: .retry, options: nil).count > 0 || + PHAssetResourceUploadJob.fetchJobs(action: .acknowledge, options: nil).count > 0 + } +} diff --git a/BackgroundUploadExtension/BackgroundUploadExtension+Results.swift b/BackgroundUploadExtension/BackgroundUploadExtension+Results.swift new file mode 100644 index 0000000000..c60f7fa771 --- /dev/null +++ b/BackgroundUploadExtension/BackgroundUploadExtension+Results.swift @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Photos +import NextcloudKit + +extension BackgroundUploadExtension { + func updateMetadataForUploadFailure(metadata: tableMetadata, job: PHAssetResourceUploadJob) async { + let error = job.error.map { $0 as NSError } + let authenticationRequired = job.responseHeaderFields?["www-authenticate"] != nil + + metadata.sessionTaskIdentifier = 0 + metadata.sessionDate = Date() + metadata.sessionError = authenticationRequired ? "Authentication required" : error?.localizedDescription ?? "Background upload failed" + metadata.errorCode = authenticationRequired ? NSURLErrorUserAuthenticationRequired : error?.code ?? NSURLErrorUnknown + metadata.status = global.metadataStatusUploadError + + await database.replaceMetadataAsync(ocId: metadata.ocId, metadata: metadata) + + logError("Background upload failed for \(metadata.fileName), job: \(job.localIdentifier), error: \(metadata.errorCode) \(metadata.sessionError)") + } + + func processUploadSuccess(metadata: tableMetadata, job: PHAssetResourceUploadJob) async -> Bool { + let headers = job.responseHeaderFields ?? [:] + + guard let ocId = headers["oc-fileid"], !ocId.isEmpty else { + metadata.session = "" + metadata.sessionTaskIdentifier = 0 + metadata.sessionDate = Date() + metadata.sessionError = "Upload response missing oc-fileid" + metadata.errorCode = NSURLErrorBadServerResponse + metadata.status = global.metadataStatusUploadError + + await database.replaceMetadataAsync(ocId: metadata.ocId, metadata: metadata) + + logError("Successful job without oc-fileid: \(job.localIdentifier)") + + return false + } + + let etag = nkComm.normalizedETag( + headers["oc-etag"] ?? headers["etag"] + ) + + let date = headers["date"]?.parsedDate( + using: "EEE, dd MMM y HH:mm:ss zzz" + ) + + let ownerId = headers["x-nc-ownerid"] + let permissions = headers["x-nc-permissions"] + + metadata.uploadDate = (date as? NSDate) ?? NSDate() + metadata.etag = etag ?? "" + metadata.ocId = ocId + + if let fileId = NCUtility().ocIdToFileId(ocId: ocId) { + metadata.fileId = fileId + } + + if let ownerId, !ownerId.isEmpty { + metadata.ownerId = ownerId + if let ownerDisplayName = await NCManageDatabase.shared.getOwnerDisplayName(account: metadata.account, ownerId: ownerId) { + metadata.ownerDisplayName = ownerDisplayName + } + } + + if let permissions, !permissions.isEmpty { + metadata.permissions = permissions + } + + metadata.chunk = 0 + metadata.sceneIdentifier = nil + metadata.session = "" + metadata.sessionError = "" + metadata.sessionDate = nil + metadata.sessionTaskIdentifier = 0 + metadata.status = NCGlobal.shared.metadataStatusNormal + + if metadata.sessionSelector == global.selectorUploadAutoUpload, + let serverUrlBase = metadata.autoUploadServerUrlBase { + await database.addAutoUploadTransferAsync( + account: metadata.account, + serverUrlBase: serverUrlBase, + fileName: metadata.fileNameView, + assetLocalIdentifier: metadata.assetLocalIdentifier, + date: metadata.creationDate as Date + ) + } + + await database.replaceMetadataAsync(ocId: metadata.ocIdTransfer, metadata: metadata) + + logInfo("Completed background upload for \(metadata.fileName), job: \(job.localIdentifier), ocId: \(ocId)") + + return true + } +} diff --git a/BackgroundUploadExtension/BackgroundUploadExtension.swift b/BackgroundUploadExtension/BackgroundUploadExtension.swift new file mode 100644 index 0000000000..33399b91de --- /dev/null +++ b/BackgroundUploadExtension/BackgroundUploadExtension.swift @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import ExtensionFoundation +import Photos +import NextcloudKit +import OSLog + +@main +final class BackgroundUploadExtension: PHBackgroundResourceUploadJobExtension { + let global = NCGlobal.shared + let database = NCManageDatabase.shared + let utilityFileSystem = NCUtilityFileSystem() + let nkComm = NextcloudKit.shared.nkCommonInstance + let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "BackgroundUploadExtension", category: NCGlobal.shared.logTagBackgroundUpload) + + required init() { + database.openRealm() + + NextcloudKit.configureLogger(logLevel: NCBrandOptions.shared.disable_log ? .disabled : NCPreferences().log) + NextcloudKit.shared.setup(groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) + + logInfo("BackgroundUploadExtension initialized, bundle: \(Bundle.main.bundleIdentifier ?? "")") + } + + func processJobs() async -> PHBackgroundResourceUploadProcessingResult { + logDebug("processJobs begin") + + let account = await setupAccount() + + do { + var madeProgress = false + + if try await cancelRequestedUploadJobs() { + madeProgress = true + } + + if try await retryUploadJobs() { + madeProgress = true + } + + if try await acknowledgeUploadJobs() { + madeProgress = true + } + + if let account { + if try await createUploadJobs(account: account) { + madeProgress = true + } + + let availableJobs = availableUploadJobSlots() + + if availableJobs > 0, + await createPendingMetadatas(account: account, limit: availableJobs) { + madeProgress = true + + if try await createUploadJobs(account: account) { + madeProgress = true + } + } + } + + let hasActiveJobs = hasActiveUploadJobs() + let result: PHBackgroundResourceUploadProcessingResult = madeProgress || hasActiveJobs ? .processing : .completed + + logDebug("processJobs end, madeProgress: \(madeProgress), hasActiveJobs: \(hasActiveJobs)") + return result + } catch let error as NSError where error.domain == PHPhotosErrorDomain && error.code == PHPhotosError.limitExceeded.rawValue { + logInfo("Job limit reached") + return .processing + } catch { + logError("processJobs error: \(error)") + return .failure + } + } + + func willTerminate() async { + logDebug("BackgroundUploadExtension will terminate") + } + + func logDebug(_ message: String) { + logger.debug("\(message, privacy: .public)") + } + + func logInfo(_ message: String, persist: Bool = false) { + logger.info("\(message, privacy: .public)") + + if persist { + nkLog(tag: global.logTagBackgroundUpload, emoji: .info, message: message) + } + } + + func logError(_ message: String) { + logger.error("\(message, privacy: .public)") + nkLog(tag: global.logTagBackgroundUpload, emoji: .error, message: message) + } +} diff --git a/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift b/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift new file mode 100644 index 0000000000..ccd4b21a8e --- /dev/null +++ b/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Photos +import NextcloudKit + +@available(iOS 27, *) +final class NCBackgroundUploadExtensionManager { + static let shared = NCBackgroundUploadExtensionManager() + + private let database = NCManageDatabase.shared + private let global = NCGlobal.shared + + private init() {} + + func shouldUseExtension() async -> Bool { + guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { + return false + } + + guard !NCPreferences().formatCompatibility else { + return false + } + + guard let account = await database.getTableAccountAsync(predicate: NSPredicate(format: "autoUploadStart == true")) else { + return false + } + + let capabilities = await NKCapabilities.shared.getCapabilities(for: account.account) + + guard NCBrandOptions.shared.isServerVersion(capabilities, greaterOrEqualTo: .v33) else { + nkLog(tag: global.logTagBackgroundUpload, message: "Background upload extension unavailable for account \(account.account): server version is lower than 33") + return false + } + + return true + } + + func ensureEnabled() async -> Bool { + guard await shouldUseExtension() else { + return false + } + + let library = PHPhotoLibrary.shared() + let options = PHAssetResourceUploadJobOptions() + options.preventsExpensiveNetworkAccess = false + + do { + if library.uploadJobExtensionEnabled { + try library.setUploadJobExtensionOptions(options) + } else { + try library.enableUploadJobExtension(with: options) + } + + nkLog(tag: global.logTagBackgroundUpload, message: "Background upload extension enabled: \(library.uploadJobExtensionEnabled)") + return library.uploadJobExtensionEnabled + } catch { + nkLog(tag: global.logTagBackgroundUpload, message: "Background upload extension enable failed: \(error)") + return false + } + } + + func disableIfIdle() async -> Bool { + let account = await database.getTableAccountAsync(predicate: NSPredicate(format: "autoUploadStart == true")) + + guard account == nil else { + return false + } + + let predicate = NSPredicate(format: "sessionSelector == %@ AND backgroundUploadJobIdentifier != ''", global.selectorUploadAutoUpload) + let metadatas: [tableMetadata] = await database.getMetadatasAsync(predicate: predicate) + + guard metadatas.isEmpty else { + nkLog(tag: global.logTagBackgroundUpload, message: "Background upload extension disable deferred: \(metadatas.count) jobs still active") + return false + } + + let library = PHPhotoLibrary.shared() + + guard library.uploadJobExtensionEnabled else { + return true + } + + do { + try library.disableUploadJobExtension() + nkLog(tag: global.logTagBackgroundUpload, message: "Background upload extension disabled") + return !library.uploadJobExtensionEnabled + } catch { + nkLog(tag: global.logTagBackgroundUpload, message: "Background upload extension disable failed: \(error)") + return false + } + } +} diff --git a/BackgroundUploadExtension/NCManageDatabase.swift b/BackgroundUploadExtension/NCManageDatabase.swift new file mode 100644 index 0000000000..ab46908738 --- /dev/null +++ b/BackgroundUploadExtension/NCManageDatabase.swift @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit +import RealmSwift +import OSLog + +final class NCManageDatabase { + static let shared = NCManageDatabase() + + internal let core: NCManageDatabaseCore + internal let databaseURL: URL? + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "BackgroundUploadExtension", category: NCGlobal.shared.logTagBackgroundUpload) + + private init() { + self.core = NCManageDatabaseCore() + + if let dirGroup = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: NCBrandOptions.shared.capabilitiesGroup) { + self.databaseURL = dirGroup + .appendingPathComponent(NCGlobal.shared.appDatabaseNextcloud) + .appendingPathComponent(databaseName) + } else { + self.databaseURL = nil + } + } + + func openRealm() { + do { + let configuration = Realm.Configuration( + fileURL: databaseURL, + schemaVersion: databaseSchemaVersion, + objectTypes: [ + NCKeyValue.self, tableMetadata.self, tableLocalFile.self, tableMetadataTag.self, + tableDirectory.self, tableAccount.self, tableAutoUploadTransfer.self, tableCapabilities.self + ] + ) + Realm.Configuration.defaultConfiguration = configuration + + let realm = try Realm(configuration: configuration) + if let url = realm.configuration.fileURL { + logger.debug("Realm is located at: \(url.path, privacy: .public)") + } + } catch let error { + logger.error("Realm error: \(error.localizedDescription, privacy: .public)") + nkLog(tag: NCGlobal.shared.logTagBackgroundUpload, emoji: .error, message: "Realm error: \(error)") + isSuspendingDatabaseOperation = true + } + } +} diff --git a/Brand/BackgroundUploadExtension.entitlements b/Brand/BackgroundUploadExtension.entitlements new file mode 100644 index 0000000000..4ecc3f0d13 --- /dev/null +++ b/Brand/BackgroundUploadExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.it.twsweb.Crypto-Cloud + + keychain-access-groups + + $(AppIdentifierPrefix)it.twsweb.Crypto-Cloud + + + diff --git a/Brand/BackgroundUploadExtension.plist b/Brand/BackgroundUploadExtension.plist new file mode 100644 index 0000000000..851d3d8542 --- /dev/null +++ b/Brand/BackgroundUploadExtension.plist @@ -0,0 +1,13 @@ + + + + + BackgroundUploadURLBase + https://cloud.nextcloud.com + EXAppExtensionAttributes + + EXExtensionPointIdentifier + com.apple.photos.background-upload + + + diff --git a/Brand/Database.swift b/Brand/Database.swift index 6dc29a2020..b1f5415d61 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 = 414 +let databaseSchemaVersion: UInt64 = 416 diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index d6859ef987..84038dd04d 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -236,6 +236,10 @@ F70BFC7420E0FA7D00C67599 /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; F70BFC7520E0FA7D00C67599 /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; F70C8742301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70C8741301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */; }; + F70CAC7B30408EB200CCCB7F /* NCGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */; }; + F70CAC7D30408F2600CCCB7F /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = F70CAC7C30408F2600CCCB7F /* NextcloudKit */; }; + F70CAC7F30408F3100CCCB7F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = F70CAC7E30408F3100CCCB7F /* KeychainAccess */; }; + F70CAC8130408F4D00CCCB7F /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F70CAC8030408F4D00CCCB7F /* RealmSwift */; }; F70CAE3A1F8CF31A008125FD /* NCEndToEndEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F70CAE391F8CF31A008125FD /* NCEndToEndEncryption.m */; }; F70CEF5623E9C7E50007035B /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; F70D7C3725FFBF82002B9E34 /* NCCollectionViewCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70D7C3525FFBF81002B9E34 /* NCCollectionViewCommon.swift */; }; @@ -295,6 +299,7 @@ F71F6D0D2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */; }; F71FA7992F3508C600E86192 /* NCNetworking+WebDAV.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */; }; F721C50A2FB6F9AA00207DA9 /* NCCollectionViewCommon+TransitionSourceBlink.swift in Sources */ = {isa = PBXBuildFile; fileRef = F721C5092FB6F9AA00207DA9 /* NCCollectionViewCommon+TransitionSourceBlink.swift */; }; + F721C7FF3040346F00DD9D36 /* BackgroundUploadExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = F7FB188030401D0D00EB0AE6 /* BackgroundUploadExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F722133B2D40EF9D002F7438 /* NCFilesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F722133A2D40EF8C002F7438 /* NCFilesNavigationController.swift */; }; F7226EDC1EE4089300EBECB1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7226EDB1EE4089300EBECB1 /* Main.storyboard */; }; F722F0112CFF569500065FB5 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F722F0102CFF569500065FB5 /* MainInterface.storyboard */; }; @@ -689,6 +694,7 @@ F78ACD54219047D40088454D /* NCSectionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD53219047D40088454D /* NCSectionFooter.xib */; }; F78B87E72B62527100C65ADC /* NCMediaDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */; }; F78C6FDE296D677300C952C3 /* NCContextMenuMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78C6FDD296D677300C952C3 /* NCContextMenuMain.swift */; }; + F78DB2103040239E00E6FE24 /* BackgroundUploadExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78DB20C3040239E00E6FE24 /* BackgroundUploadExtension.swift */; }; F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; }; F78E2D6629AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; }; F78E2D6729AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; }; @@ -702,6 +708,35 @@ F790110E21415BF600D7B136 /* NCViewerRichdocuments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F790110D21415BF600D7B136 /* NCViewerRichdocuments.swift */; }; F79377052FBD86AF00DE56DE /* NCMediaViewerFloatingTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79377042FBD86AE00DE56DE /* NCMediaViewerFloatingTitleView.swift */; }; F793E59D28B761E7005E4B02 /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; + F793E79B30415A0A00F011FE /* NCManageDatabase+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */; }; + F793E79C30415A2800F011FE /* NCManageDatabaseCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76340F32EBDE9740056F538 /* NCManageDatabaseCore.swift */; }; + F793E79D30415A4E00F011FE /* NCManageDatabase+TableCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D61EA52EBF168E007F865B /* NCManageDatabase+TableCapabilities.swift */; }; + F793E79E30415A5A00F011FE /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; }; + F793E79F30415A6500F011FE /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; }; + F793E7A030415A8B00F011FE /* NCManageDatabase+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78A10BE29322E8A008499B8 /* NCManageDatabase+Directory.swift */; }; + F793E7A230415B4E00F011FE /* NCUtilityFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74AF3A3247FB6AE00AC767B /* NCUtilityFileSystem.swift */; }; + F793E7A430415B6800F011FE /* NCManageDatabase+LocalFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7864ACB2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift */; }; + F793E7A530415C8C00F011FE /* NCManageDatabase+CreateMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */; }; + F793E7A630415CB300F011FE /* NCManageDatabase+Metadata+Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B769A72B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift */; }; + F793E7A730415FCB00F011FE /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F793E7A93041601800F011FE /* ThreadSafeArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */; }; + F793E7AB3041611600F011FE /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7AA3041611600F011FE /* NCManageDatabase.swift */; }; + F793E7AC3041614D00F011FE /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; + F793E7AD3041615800F011FE /* NCPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882132C0DD1E7001CF441 /* NCPreferences.swift */; }; + F793E7AE3041617B00F011FE /* ThreadSafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */; }; + F793E7AF3041619800F011FE /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; + F793E7B0304161B100F011FE /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F793E7B1304161BD00F011FE /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; + F793E7B2304162FC00F011FE /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; + F793E7C1304170F200F011FE /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; + F793E7C430417D1300F011FE /* NCBackgroundUploadExtensionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */; }; + F793E7C63041801300F011FE /* BackgroundUploadExtension+Accounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C53041801300F011FE /* BackgroundUploadExtension+Accounts.swift */; }; + F793E7C83041802600F011FE /* BackgroundUploadExtension+Jobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C73041802400F011FE /* BackgroundUploadExtension+Jobs.swift */; }; + F793E7CA3041803C00F011FE /* BackgroundUploadExtension+Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C93041803B00F011FE /* BackgroundUploadExtension+Destination.swift */; }; + F793E7CC3041804C00F011FE /* BackgroundUploadExtension+Results.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7CB3041804B00F011FE /* BackgroundUploadExtension+Results.swift */; }; + F793E7CD30418A4E00F011FE /* NCManageDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */; }; + F793E7CF304190AD00F011FE /* BackgroundUploadExtension+Discovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7CE304190AC00F011FE /* BackgroundUploadExtension+Discovery.swift */; }; + F793E7DB304575AB00F011FE /* NCManageDatabase+Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F763D29C2A249C4500A3C901 /* NCManageDatabase+Capabilities.swift */; }; F7948DE72FBAE53000253D1C /* NCVideoAVPlayerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7948DE62FBAE52F00253D1C /* NCVideoAVPlayerPresenter.swift */; }; F7948DE92FBAEC5400253D1C /* NCVideoAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7948DE82FBAEC5300253D1C /* NCVideoAVPlayerViewController.swift */; }; F794E13D2BBBFF2E003693D7 /* NCMainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F794E13C2BBBFF2E003693D7 /* NCMainTabBarController.swift */; }; @@ -1104,6 +1139,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + F70CAC8330408F4D00CCCB7F /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; F7160A592BE92CF30034DCB3 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1194,6 +1239,16 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + F721C7FE3040345700DD9D36 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + F721C7FF3040346F00DD9D36 /* BackgroundUploadExtension.appex in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F76DA934277B75710082465B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1711,6 +1766,7 @@ F78D6F461F0B7CB9002F9619 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/Localizable.strings"; sourceTree = ""; }; F78D6F4D1F0B7CE4002F9619 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; F78D6F541F0B7D47002F9619 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + F78DB20C3040239E00E6FE24 /* BackgroundUploadExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundUploadExtension.swift; sourceTree = ""; }; F78E2D6429AF02DB0024D4F3 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; F78F74332163757000C2ADAD /* NCTrash.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCTrash.storyboard; sourceTree = ""; }; F78F74352163781100C2ADAD /* NCTrash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrash.swift; sourceTree = ""; }; @@ -1718,6 +1774,13 @@ F79131C628AFB86E00577277 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; F79131C728AFB86E00577277 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; F79377042FBD86AE00DE56DE /* NCMediaViewerFloatingTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaViewerFloatingTitleView.swift; sourceTree = ""; }; + F793E7AA3041611600F011FE /* NCManageDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCManageDatabase.swift; sourceTree = ""; }; + F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCBackgroundUploadExtensionManager.swift; sourceTree = ""; }; + F793E7C53041801300F011FE /* BackgroundUploadExtension+Accounts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundUploadExtension+Accounts.swift"; sourceTree = ""; }; + F793E7C73041802400F011FE /* BackgroundUploadExtension+Jobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundUploadExtension+Jobs.swift"; sourceTree = ""; }; + F793E7C93041803B00F011FE /* BackgroundUploadExtension+Destination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundUploadExtension+Destination.swift"; sourceTree = ""; }; + F793E7CB3041804B00F011FE /* BackgroundUploadExtension+Results.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundUploadExtension+Results.swift"; sourceTree = ""; }; + F793E7CE304190AC00F011FE /* BackgroundUploadExtension+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundUploadExtension+Discovery.swift"; sourceTree = ""; }; F7948DE62FBAE52F00253D1C /* NCVideoAVPlayerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCVideoAVPlayerPresenter.swift; sourceTree = ""; }; F7948DE82FBAEC5300253D1C /* NCVideoAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCVideoAVPlayerViewController.swift; sourceTree = ""; }; F794E13C2BBBFF2E003693D7 /* NCMainTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMainTabBarController.swift; sourceTree = ""; }; @@ -1995,6 +2058,7 @@ F7FA7FFF2C0F4F3B0072FC60 /* NCUploadAssetsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCUploadAssetsView.swift; sourceTree = ""; }; F7FAAC212FB773CA00DCA45B /* NCVideoAVPlayerViewControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCVideoAVPlayerViewControls.swift; sourceTree = ""; }; F7FAFD3928BFA947000777FE /* NCContextMenuNotification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCContextMenuNotification.swift; sourceTree = ""; }; + F7FB188030401D0D00EB0AE6 /* BackgroundUploadExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = BackgroundUploadExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F7FDFF512E437E55000D7688 /* NCAccountRequest.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCAccountRequest.storyboard; sourceTree = ""; }; F7FDFF522E437E55000D7688 /* NCAccountRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccountRequest.swift; sourceTree = ""; }; F7FDFF532E437E55000D7688 /* NCShareAccounts.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCShareAccounts.storyboard; sourceTree = ""; }; @@ -2162,6 +2226,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7FB187D30401D0D00EB0AE6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F70CAC8130408F4D00CCCB7F /* RealmSwift in Frameworks */, + F70CAC7F30408F3100CCCB7F /* KeychainAccess in Frameworks */, + F70CAC7D30408F2600CCCB7F /* NextcloudKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -3042,6 +3116,21 @@ path = "Section Header Footer"; sourceTree = ""; }; + F78DB20E3040239E00E6FE24 /* BackgroundUploadExtension */ = { + isa = PBXGroup; + children = ( + F78DB20C3040239E00E6FE24 /* BackgroundUploadExtension.swift */, + F793E7C53041801300F011FE /* BackgroundUploadExtension+Accounts.swift */, + F793E7C93041803B00F011FE /* BackgroundUploadExtension+Destination.swift */, + F793E7CE304190AC00F011FE /* BackgroundUploadExtension+Discovery.swift */, + F793E7C73041802400F011FE /* BackgroundUploadExtension+Jobs.swift */, + F793E7CB3041804B00F011FE /* BackgroundUploadExtension+Results.swift */, + F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */, + F793E7AA3041611600F011FE /* NCManageDatabase.swift */, + ); + path = BackgroundUploadExtension; + sourceTree = ""; + }; F78F74322163753B00C2ADAD /* Trash */ = { isa = PBXGroup; children = ( @@ -3608,6 +3697,7 @@ F7346E1428B0EF5B006CE2D2 /* Widget */, F7C9739328F17131002C43E2 /* WidgetDashboardIntentHandler */, F7C55CC82FB5CE74004A974F /* Action Assistant */, + F78DB20E3040239E00E6FE24 /* BackgroundUploadExtension */, F7FC7D651DC1F98700BB2C6A /* Products */, F30A962A2A27A9C800D7BCFE /* Tests */, F771E3D020E2392D00AFB62D /* File Provider Extension.appex */, @@ -3621,6 +3711,7 @@ F7F1FBA62E27D13700C79E20 /* Frameworks */, F31165012F9674A1009A1E37 /* AppIcon.icon */, F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */, + F7FB188030401D0D00EB0AE6 /* BackgroundUploadExtension.appex */, ); sourceTree = ""; }; @@ -3963,6 +4054,7 @@ AFBFD01327551A54002244BC /* ShellScript */, F76DA934277B75710082465B /* Embed Frameworks */, F76995F02F99EF6C00291FA7 /* Crashlytics dSYM Upload */, + F721C7FE3040345700DD9D36 /* CopyFiles */, ); buildRules = ( ); @@ -4051,6 +4143,29 @@ productReference = F7C9739028F17131002C43E2 /* WidgetDashboardIntentHandler.appex */; productType = "com.apple.product-type.app-extension"; }; + F7FB187F30401D0D00EB0AE6 /* BackgroundUploadExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7FB188630401D0D00EB0AE6 /* Build configuration list for PBXNativeTarget "BackgroundUploadExtension" */; + buildPhases = ( + F7FB187C30401D0D00EB0AE6 /* Sources */, + F7FB187D30401D0D00EB0AE6 /* Frameworks */, + F7FB187E30401D0D00EB0AE6 /* Resources */, + F70CAC8330408F4D00CCCB7F /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BackgroundUploadExtension; + packageProductDependencies = ( + F70CAC7C30408F2600CCCB7F /* NextcloudKit */, + F70CAC7E30408F3100CCCB7F /* KeychainAccess */, + F70CAC8030408F4D00CCCB7F /* RealmSwift */, + ); + productName = BackgroundUploadExtension; + productReference = F7FB188030401D0D00EB0AE6 /* BackgroundUploadExtension.appex */; + productType = "com.apple.product-type.extensionkit-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -4058,7 +4173,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 2640; + LastSwiftUpdateCheck = 2700; LastUpgradeCheck = 2640; ORGANIZATIONNAME = "Marino Faggiana"; TargetAttributes = { @@ -4118,6 +4233,9 @@ F7C9738F28F17131002C43E2 = { CreatedOnToolsVersion = 14.0; }; + F7FB187F30401D0D00EB0AE6 = { + CreatedOnToolsVersion = 27.0; + }; }; }; buildConfigurationList = F7F67BA31A24D27800EE80DA /* Build configuration list for PBXProject "Nextcloud" */; @@ -4208,6 +4326,7 @@ F7346E0F28B0EF5B006CE2D2 /* Widget */, F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */, F71459B41D12E3B700CAFEEC /* Share */, + F7FB187F30401D0D00EB0AE6 /* BackgroundUploadExtension */, F7C55C792FB5AEF7004A974F /* Action Assistant */, F771E3CF20E2392D00AFB62D /* File Provider Extension */, F70716E22987F81400E72C1D /* File Provider Extension UI */, @@ -4403,6 +4522,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7FB187E30401D0D00EB0AE6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -5214,6 +5340,7 @@ F7725A60251F33BB00D125E0 /* NCFiles.swift in Sources */, F721C50A2FB6F9AA00207DA9 /* NCCollectionViewCommon+TransitionSourceBlink.swift in Sources */, F704B5E52430AA8000632F5F /* NCCreateFormUploadConflict.swift in Sources */, + F793E7C430417D1300F011FE /* NCBackgroundUploadExtensionManager.swift in Sources */, F7865FF12F39D32F00D09AE4 /* NCCollectionViewCommon+Search.swift in Sources */, F7327E352B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift in Sources */, F76687072B7D067400779E3F /* NCAudioRecorderViewController.swift in Sources */, @@ -5360,6 +5487,43 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7FB187C30401D0D00EB0AE6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F793E7AC3041614D00F011FE /* NCBrand.swift in Sources */, + F793E7C1304170F200F011FE /* NCUtility.swift in Sources */, + F793E7CC3041804C00F011FE /* BackgroundUploadExtension+Results.swift in Sources */, + F793E7AF3041619800F011FE /* UIColor+Extension.swift in Sources */, + F793E7CF304190AD00F011FE /* BackgroundUploadExtension+Discovery.swift in Sources */, + F793E7CA3041803C00F011FE /* BackgroundUploadExtension+Destination.swift in Sources */, + F793E7A630415CB300F011FE /* NCManageDatabase+Metadata+Session.swift in Sources */, + F793E7A430415B6800F011FE /* NCManageDatabase+LocalFile.swift in Sources */, + F793E79B30415A0A00F011FE /* NCManageDatabase+Metadata.swift in Sources */, + F793E7C63041801300F011FE /* BackgroundUploadExtension+Accounts.swift in Sources */, + F793E7B2304162FC00F011FE /* Optional+Extension.swift in Sources */, + F793E7AE3041617B00F011FE /* ThreadSafeDictionary.swift in Sources */, + F793E79D30415A4E00F011FE /* NCManageDatabase+TableCapabilities.swift in Sources */, + F793E7C83041802600F011FE /* BackgroundUploadExtension+Jobs.swift in Sources */, + F793E79E30415A5A00F011FE /* NCManageDatabase+Account.swift in Sources */, + F793E7AB3041611600F011FE /* NCManageDatabase.swift in Sources */, + F793E7AD3041615800F011FE /* NCPreferences.swift in Sources */, + F793E79C30415A2800F011FE /* NCManageDatabaseCore.swift in Sources */, + F793E7A230415B4E00F011FE /* NCUtilityFileSystem.swift in Sources */, + F793E7A730415FCB00F011FE /* NCSession.swift in Sources */, + F793E7A030415A8B00F011FE /* NCManageDatabase+Directory.swift in Sources */, + F793E7CD30418A4E00F011FE /* NCManageDatabase+AutoUpload.swift in Sources */, + F793E7DB304575AB00F011FE /* NCManageDatabase+Capabilities.swift in Sources */, + F793E7A530415C8C00F011FE /* NCManageDatabase+CreateMetadata.swift in Sources */, + F78DB2103040239E00E6FE24 /* BackgroundUploadExtension.swift in Sources */, + F793E7B0304161B100F011FE /* AwakeMode.swift in Sources */, + F793E79F30415A6500F011FE /* Database.swift in Sources */, + F793E7A93041601800F011FE /* ThreadSafeArray.swift in Sources */, + F793E7B1304161BD00F011FE /* NCEndToEndKeySet.swift in Sources */, + F70CAC7B30408EB200CCCB7F /* NCGlobal.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -6218,8 +6382,8 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", "$(inherited)", + EXTENSION, ); GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; @@ -6236,7 +6400,6 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; }; name = Debug; }; @@ -6258,6 +6421,10 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + EXTENSION, + ); GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; @@ -6273,7 +6440,6 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; VALIDATE_PRODUCT = YES; }; name = Release; @@ -6490,6 +6656,108 @@ }; name = Release; }; + F7FB188730401D0D00EB0AE6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand//BackgroundUploadExtension.entitlements"; + CODE_SIGN_STYLE = Automatic; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + EXTENSION, + EXTENSION_BACKGROUNDUPLOAD, + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = "$(BUILD_DIR)/../../SourcePackages/checkouts/realm-swift/include"; + INFOPLIST_FILE = "$(SRCROOT)/Brand//BackgroundUploadExtension.plist"; + INFOPLIST_KEY_CFBundleDisplayName = BackgroundUploadExtension; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = it.twsweb.Nextcloud.BackgroundUploadExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION EXTENSION_BACKGROUNDUPLOAD"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + F7FB188830401D0D00EB0AE6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand//BackgroundUploadExtension.entitlements"; + CODE_SIGN_STYLE = Automatic; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + EXTENSION, + EXTENSION_BACKGROUNDUPLOAD, + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = "$(BUILD_DIR)/../../SourcePackages/checkouts/realm-swift/include"; + INFOPLIST_FILE = "$(SRCROOT)/Brand//BackgroundUploadExtension.plist"; + INFOPLIST_KEY_CFBundleDisplayName = BackgroundUploadExtension; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = it.twsweb.Nextcloud.BackgroundUploadExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION EXTENSION_BACKGROUNDUPLOAD"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -6601,6 +6869,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F7FB188630401D0D00EB0AE6 /* Build configuration list for PBXNativeTarget "BackgroundUploadExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7FB188730401D0D00EB0AE6 /* Debug */, + F7FB188830401D0D00EB0AE6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -6945,6 +7222,21 @@ package = F70B86732642CE3B00ED5349 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseCrashlytics; }; + F70CAC7C30408F2600CCCB7F /* NextcloudKit */ = { + isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; + productName = NextcloudKit; + }; + F70CAC7E30408F3100CCCB7F /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = F760DE012AE66E860027D78A /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; + F70CAC8030408F4D00CCCB7F /* RealmSwift */ = { + isa = XCSwiftPackageProductDependency; + package = F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */; + productName = RealmSwift; + }; F710FC7F277B7D2700AA9FBF /* RealmSwift */ = { isa = XCSwiftPackageProductDependency; package = F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */; diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/BackgroundUploadExtension.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/BackgroundUploadExtension.xcscheme new file mode 100644 index 0000000000..6f83c512bc --- /dev/null +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/BackgroundUploadExtension.xcscheme @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme index e8cd71a81d..ad5d115f12 100755 --- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme @@ -1,7 +1,7 @@ + version = "2.0"> @@ -120,13 +120,14 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" + askForAppToLaunch = "Yes" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - allowLocationSimulation = "NO"> - + allowLocationSimulation = "NO" + launchAutomaticallySubstyle = "2"> + - + Void)? diff --git a/iOSClient/Data/NCManageDatabase+Account.swift b/iOSClient/Data/NCManageDatabase+Account.swift index d01025f637..07b7f69510 100644 --- a/iOSClient/Data/NCManageDatabase+Account.swift +++ b/iOSClient/Data/NCManageDatabase+Account.swift @@ -363,6 +363,24 @@ extension NCManageDatabase { } } } + + func setAutoUploadStartAsync(_ enabled: Bool, account: String) async { + await core.performRealmWriteAsync { realm in + let accounts = realm.objects(tableAccount.self) + + if enabled { + for result in accounts { + result.autoUploadStart = result.account == account + } + } else { + accounts + .filter("account == %@", account) + .first? + .autoUploadStart = false + } + } + } + // MARK: - Realm Read func getTableAccount(predicate: NSPredicate) -> tableAccount? { @@ -529,18 +547,26 @@ extension NCManageDatabase { return folderPhotos } - func getAccountAutoUploadSubfolderGranularity() -> Int { + func getAccountAutoUploadSubfolderGranularity(account: String? = nil) -> Int { core.performRealmRead { realm in - realm.objects(tableAccount.self) + if let account { + return realm.object(ofType: tableAccount.self, forPrimaryKey: account)?.autoUploadSubfolderGranularity + } + + return realm.objects(tableAccount.self) .filter("active == true") .first? .autoUploadSubfolderGranularity } ?? NCGlobal.shared.subfolderGranularityMonthly } - func getAccountAutoUploadSubfolderGranularityAsync() async -> Int { + func getAccountAutoUploadSubfolderGranularityAsync(account: String? = nil) async -> Int { await core.performRealmReadAsync { realm in - realm.objects(tableAccount.self) + if let account { + return realm.object(ofType: tableAccount.self, forPrimaryKey: account)?.autoUploadSubfolderGranularity + } + + return realm.objects(tableAccount.self) .filter("active == true") .first? .autoUploadSubfolderGranularity diff --git a/iOSClient/Data/NCManageDatabase+AutoUpload.swift b/iOSClient/Data/NCManageDatabase+AutoUpload.swift index 084b4a9fd2..31b7d747d9 100644 --- a/iOSClient/Data/NCManageDatabase+AutoUpload.swift +++ b/iOSClient/Data/NCManageDatabase+AutoUpload.swift @@ -91,6 +91,25 @@ extension NCManageDatabase { return result ?? [] } + func fetchSkipAssetLocalIdentifiersAsync(account: String, + autoUploadServerUrlBase: String) async -> Set { + let result: Set? = await core.performRealmReadAsync { realm in + let metadataIdentifiers = realm.objects(tableMetadata.self) + .filter("account == %@ AND autoUploadServerUrlBase == %@ AND assetLocalIdentifier != ''", + account, autoUploadServerUrlBase) + .map(\.assetLocalIdentifier) + + let transferIdentifiers = realm.objects(tableAutoUploadTransfer.self) + .filter("account == %@ AND serverUrlBase == %@ AND assetLocalIdentifier != ''", + account, autoUploadServerUrlBase) + .map(\.assetLocalIdentifier) + + return Set(metadataIdentifiers).union(transferIdentifiers) + } + + return result ?? [] + } + /// Asynchronously fetches the most recent auto-uploaded date for the given account and server base URL. /// - Parameters: /// - account: The account identifier. diff --git a/iOSClient/Data/NCManageDatabase+Capabilities.swift b/iOSClient/Data/NCManageDatabase+Capabilities.swift index d93ed07b00..a707ef059b 100644 --- a/iOSClient/Data/NCManageDatabase+Capabilities.swift +++ b/iOSClient/Data/NCManageDatabase+Capabilities.swift @@ -96,7 +96,9 @@ extension NCManageDatabase { } // use Networking +#if !EXTENSION_BACKGROUNDUPLOAD NCNetworking.shared.capabilities[account] = capabilities +#endif return capabilities } diff --git a/iOSClient/Data/NCManageDatabase+CreateMetadata.swift b/iOSClient/Data/NCManageDatabase+CreateMetadata.swift index 2e690992b2..7fb5032a49 100644 --- a/iOSClient/Data/NCManageDatabase+CreateMetadata.swift +++ b/iOSClient/Data/NCManageDatabase+CreateMetadata.swift @@ -22,7 +22,7 @@ final class NCManageDatabaseCreateMetadata { account: file.account) } -#if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD // E2EE find the fileName for fileNameView if e2eEncryptedDirectory || file.e2eEncrypted { if let tableE2eEncryption = await NCManageDatabase.shared.getE2eEncryptionAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameIdentifier == %@", file.account, file.serverUrl, file.fileName)) { @@ -47,7 +47,7 @@ final class NCManageDatabaseCreateMetadata { func convertFileToMetadata(_ file: NKFile, capabilities: NKCapabilities.Capabilities?, isDirectoryE2EE: Bool? = nil, completion: @escaping (tableMetadata) -> Void) { let metadata = self.createMetadata(file) -#if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD let e2eEncryptedDirectory: Bool = isDirectoryE2EE ?? NCUtilityFileSystem().isDirectoryE2EE( serverUrl: file.serverUrl, urlBase: file.urlBase, @@ -83,7 +83,7 @@ final class NCManageDatabaseCreateMetadata { var metadatas: [tableMetadata] = [] for file in files { -#if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD if let key = listServerUrl[file.serverUrl] { isDirectoryE2EE = key } else { @@ -105,7 +105,7 @@ final class NCManageDatabaseCreateMetadata { return (metadataFolder.detachedCopy(), metadatas) } -#if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD func convertFilesToMetadatas(_ files: [NKFile], capabilities: NKCapabilities.Capabilities?, serverUrlMetadataFolder: String? = nil, completion: @escaping (_ metadataFolder: tableMetadata?, _ metadatas: [tableMetadata]) -> Void) { var counter: Int = 0 var isDirectoryE2EE: Bool = false @@ -395,7 +395,7 @@ final class NCManageDatabaseCreateMetadata { return metadata } - #if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD private func createMetadatasFolder(assets: [PHAsset], useSubFolder: Bool, metadatasFolder: [tableMetadata], @@ -441,9 +441,9 @@ final class NCManageDatabaseCreateMetadata { // Create Auto Upload SubDirectory - Granularity if useSubFolder { - let autoUploadServerUrlBase = NCManageDatabase.shared.getAccountAutoUploadServerUrlBase(session: session) - let autoUploadSubfolderGranularity = NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularity() - let folders = Set(assets.map { utilityFileSystem.createGranularityPath(asset: $0) }).sorted() + let folders = Set(assets.map { + utilityFileSystem.createGranularityPath(asset: $0, granularity: autoUploadSubfolderGranularity) + }).sorted() for folder in folders { let componentsDate = folder.split(separator: "/") @@ -481,7 +481,7 @@ final class NCManageDatabaseCreateMetadata { let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND directory == true", session.account, autoUploadDirectory) let metadatasFolder = NCManageDatabase.shared.getMetadatas(predicate: predicate) let autoUploadServerUrlBase = NCManageDatabase.shared.getAccountAutoUploadServerUrlBase(session: session) - let autoUploadSubfolderGranularity = NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularity() + let autoUploadSubfolderGranularity = NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularity(account: session.account) let metadatas = self.createMetadatasFolder(assets: assets, useSubFolder: useSubFolder, metadatasFolder: metadatasFolder, @@ -499,7 +499,7 @@ final class NCManageDatabaseCreateMetadata { let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND directory == true", session.account, autoUploadDirectory) let metadatasFolder = await NCManageDatabase.shared.getMetadatasAsync(predicate: predicate) let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(session: session) - let autoUploadSubfolderGranularity = await NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularityAsync() + let autoUploadSubfolderGranularity = await NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularityAsync(account: session.account) let metadatas = self.createMetadatasFolder(assets: assets, useSubFolder: useSubFolder, metadatasFolder: metadatasFolder, @@ -509,5 +509,5 @@ final class NCManageDatabaseCreateMetadata { session: session) return metadatas } - #endif +#endif } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index be356b1183..44171a690a 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -128,6 +128,10 @@ class tableMetadata: Object { @objc dynamic var nativeFormat: Bool = false @objc dynamic var autoUploadServerUrlBase: String? @objc dynamic var typeIdentifier: String = "" + @objc dynamic var backgroundUploadJobIdentifier = "" + @objc dynamic var backgroundUploadRetryCount: Int = 0 + @objc dynamic var backgroundUploadNextRetryDate: Date? + @objc dynamic var backgroundUploadCancellationRequested = false // ========================= // UI / transient properties @@ -183,7 +187,7 @@ extension tableMetadata { !directory } -#if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD @objc var isDirectoryE2EE: Bool { return NCUtilityFileSystem().isDirectoryE2EE(serverUrl: serverUrl, urlBase: urlBase, userId: userId, account: account) } @@ -827,11 +831,27 @@ extension NCManageDatabase { } } - func clearMetadatasUploadAsync(account: String) async { + func requestBackgroundAutoUploadCancellationAsync(account: String) async { await core.performRealmWriteAsync { realm in - let results = realm.objects(tableMetadata.self) - .filter("account == %@ AND (status == %d OR status == %d)", account, NCGlobal.shared.metadataStatusWaitUpload, NCGlobal.shared.metadataStatusUploadError) - realm.delete(results) + let pendingMetadatas = realm.objects(tableMetadata.self).filter( + "account == %@ AND sessionSelector == %@ AND backgroundUploadJobIdentifier == %@", + account, + NCGlobal.shared.selectorUploadAutoUpload, + "pending" + ) + + realm.delete(pendingMetadatas) + + let jobMetadatas = realm.objects(tableMetadata.self).filter( + "account == %@ AND sessionSelector == %@ AND backgroundUploadJobIdentifier != %@ AND backgroundUploadJobIdentifier != ''", + account, + NCGlobal.shared.selectorUploadAutoUpload, + "pending" + ) + + for metadata in jobMetadatas { + metadata.backgroundUploadCancellationRequested = true + } } } @@ -966,6 +986,15 @@ extension NCManageDatabase { } } + func getMetadataAsync(backgroundUploadJobIdentifier: String) async -> tableMetadata? { + await core.performRealmReadAsync { realm in + realm.objects(tableMetadata.self) + .filter("backgroundUploadJobIdentifier == %@", backgroundUploadJobIdentifier) + .first? + .detachedCopy() + } + } + func getResultsMetadatasAsync(predicate: NSPredicate) async -> Results? { await core.performRealmReadAsync { realm in let results = realm.objects(tableMetadata.self) @@ -1354,7 +1383,7 @@ extension NCManageDatabase { } ?? [] } -#if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD /// Asynchronously retrieves and sorts `tableMetadata` objects matching a given predicate and layout. func getMetadatasAsync(predicate: NSPredicate, withLayout layoutForView: NCDBLayoutForView?, diff --git a/iOSClient/Data/NCMetadataUploadTranfersSuccess.swift b/iOSClient/Data/NCMetadataUploadTranfersSuccess.swift index 9033ac2be8..68cd4730ff 100644 --- a/iOSClient/Data/NCMetadataUploadTranfersSuccess.swift +++ b/iOSClient/Data/NCMetadataUploadTranfersSuccess.swift @@ -107,9 +107,6 @@ actor NCMetadataUploadTranfersSuccess { for metadata in metadatas { let results = await NCNetworking.shared.helperMetadataSuccess(metadata: metadata) - if let localFile = results.localFile { - metadatasLocalFiles.append(localFile) - } if let livePhoto = results.livePhoto { metadatasLivePhoto.append(livePhoto) } diff --git a/iOSClient/NCAppStateManager.swift b/iOSClient/NCAppStateManager.swift index 0db91a8079..77397a3d5d 100644 --- a/iOSClient/NCAppStateManager.swift +++ b/iOSClient/NCAppStateManager.swift @@ -37,6 +37,12 @@ final class NCAppStateManager { NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in nkLog(debug: "Application did become active") + + if #available(iOS 27, *) { + Task { + _ = await NCBackgroundUploadExtensionManager.shared.disableIfIdle() + } + } } NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { _ in diff --git a/iOSClient/NCBridgeSwift.h b/iOSClient/NCBridgeSwift.h index e3849b4d21..04a33ef436 100755 --- a/iOSClient/NCBridgeSwift.h +++ b/iOSClient/NCBridgeSwift.h @@ -29,3 +29,7 @@ #if defined(EXTENSION_WIDGETDASHBOARDINTENTHANDLER) #import "WidgetDashboardIntentHandler-Swift.h" #endif + +#if defined(EXTENSION_BACKGROUNDUPLOAD) +#import "BackgroundUploadExtension-Swift.h" +#endif diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 13e7b65e06..86ceafebc9 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -395,6 +395,7 @@ final class NCGlobal: Sendable { let logTagMediaBackfill = "MEDIA BACKFILL" let logTagMediaPlaceholder = "MEDIA PLACEHOLDER" let logTagMediaPreview = "MEDIA PREVIEW" + let logTagBackgroundUpload = "BACKGROUND UPLOAD" // USER DEFAULTS // diff --git a/iOSClient/Networking/NCAutoUpload.swift b/iOSClient/Networking/NCAutoUpload.swift index 5c8c359fe4..be4978ec09 100644 --- a/iOSClient/Networking/NCAutoUpload.swift +++ b/iOSClient/Networking/NCAutoUpload.swift @@ -15,26 +15,42 @@ class NCAutoUpload: NSObject { private let database = NCManageDatabase.shared private let global = NCGlobal.shared private let networking = NCNetworking.shared - private var endForAssetToUpload: Bool = false func initAutoUpload(controller: NCMainTabBarController? = nil) async -> Int { - guard self.networking.isOnline else { + if #available(iOS 27, *), + await NCBackgroundUploadExtensionManager.shared.ensureEnabled() { + nkLog(tag: global.logTagBackgroundUpload, message: "Auto upload delegated to Photos extension") return 0 } - var counter = 0 - - let tblAccounts = await NCManageDatabase.shared.getTableAccountsAsync(predicate: NSPredicate(format: "autoUploadStart == true")) - for tblAccount in tblAccounts { - let albumIds = NCPreferences().getAutoUploadAlbumIds(account: tblAccount.account) - let assetCollections = PHAssetCollection.allAlbums.filter({albumIds.contains($0.localIdentifier)}) - let result = await getCameraRollAssets(controller: nil, assetCollections: assetCollections, tblAccount: tableAccount(value: tblAccount)) - if let assets = result.assets, !assets.isEmpty, let fileNames = result.fileNames { - let item = await uploadAssets(controller: nil, tblAccount: tblAccount, assets: assets, fileNames: fileNames, filterExistingQueue: true) - counter += item - } + + guard networking.isOnline else { + return 0 + } + + guard let account = await database.getTableAccountAsync(predicate: NSPredicate(format: "autoUploadStart == true")) else { + return 0 + } + + let albumIds = NCPreferences().getAutoUploadAlbumIds(account: account.account) + let assetCollections = PHAssetCollection.allAlbums.filter { + albumIds.contains($0.localIdentifier) + } + + let result = await getCameraRollAssets(controller: controller, assetCollections: assetCollections, tblAccount: account) + + guard let assets = result.assets, + !assets.isEmpty, + let fileNames = result.fileNames else { + return 0 } - return counter + return await uploadAssets( + controller: controller, + tblAccount: account, + assets: assets, + fileNames: fileNames, + filterExistingQueue: true + ) } @MainActor @@ -54,6 +70,12 @@ class NCAutoUpload: NSObject { return } + if #available(iOS 27, *), + await NCBackgroundUploadExtensionManager.shared.ensureEnabled() { + nkLog(tag: global.logTagBackgroundUpload, message: "Manual auto upload delegated to Photos extension") + return + } + (banner, _) = await showBanner(windowScene: windowScene, title: "_info_", subtitle: "_creating_db_photo_progress_", @@ -116,7 +138,9 @@ class NCAutoUpload: NSObject { let mediaType = asset.mediaType let isLivePhoto = asset.mediaSubtypes.contains(.photoLive) && keychainLivePhoto - let serverUrl = tblAccount.autoUploadCreateSubfolder ? fileSystem.createGranularityPath(asset: asset, serverUrlBase: autoUploadServerUrlBase) : autoUploadServerUrlBase + let serverUrl = tblAccount.autoUploadCreateSubfolder + ? fileSystem.createGranularityPath(asset: asset, serverUrlBase: autoUploadServerUrlBase, granularity: tblAccount.autoUploadSubfolderGranularity) + : autoUploadServerUrlBase let onWWAN = (mediaType == .image && tblAccount.autoUploadWWAnPhoto) || (mediaType == .video && tblAccount.autoUploadWWAnVideo) let uploadSession = onWWAN ? self.networking.sessionUploadBackgroundWWan : self.networking.sessionUploadBackground @@ -289,6 +313,11 @@ class NCAutoUpload: NSObject { // // The flow cooperates with Swift task cancellation triggered by BGTask expiration. func autoUploadBackgroundSync() async { + if #available(iOS 27, *), + await NCBackgroundUploadExtensionManager.shared.ensureEnabled() { + return + } + guard !Task.isCancelled else { return } // Discover new items for Auto Upload. @@ -356,6 +385,7 @@ class NCAutoUpload: NSObject { metadatas.lazy.filter { $0.status == self.global.metadataStatusWaitUpload && $0.sessionSelector == self.global.selectorUploadAutoUpload && + $0.backgroundUploadJobIdentifier.isEmpty && $0.chunk == 0 } .prefix(availableProcess) diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift index 2dc6090ca5..fde6d5dd2d 100644 --- a/iOSClient/Networking/NCNetworking+Upload.swift +++ b/iOSClient/Networking/NCNetworking+Upload.swift @@ -331,9 +331,6 @@ extension NCNetworking { let results = await helperMetadataSuccess(metadata: metadata) await NCManageDatabase.shared.replaceMetadataAsync(ocId: metadata.ocIdTransfer, metadata: metadata) - if let localFile = results.localFile { - await NCManageDatabase.shared.addLocalFilesAsync(metadatas: [localFile]) - } if let tblAutoUpload = results.autoUpload { await NCManageDatabase.shared.addAutoUploadTransferAsync([tblAutoUpload]) } @@ -552,10 +549,7 @@ extension NCNetworking { // MARK: - Helper - func helperMetadataSuccess(metadata: tableMetadata) async -> (localFile: tableMetadata?, - livePhoto: tableMetadata?, - autoUpload: tableAutoUploadTransfer?) { - var localFile: tableMetadata? + func helperMetadataSuccess(metadata: tableMetadata) async -> (livePhoto: tableMetadata?, autoUpload: tableAutoUploadTransfer?) { var livePhoto: tableMetadata? var autoUpload: tableAutoUploadTransfer? @@ -582,6 +576,6 @@ extension NCNetworking { date: metadata.creationDate as Date) } - return (localFile: localFile, livePhoto: livePhoto, autoUpload: autoUpload) + return (livePhoto: livePhoto, autoUpload: autoUpload) } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 3227dbf642..2d7cfe36b6 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -416,26 +416,37 @@ actor NCNetworkingProcess { return } - // UPLOAD IN ERROR (check > 5 minute ago) + // UPLOAD IN ERROR (check > 5 minute ago) (NO backgroundUploadJobIdentifier) // - for metadata in metadatas where metadata.status == self.global.metadataStatusUploadError && (metadata.sessionDate ?? .distantFuture) < Date().addingTimeInterval(-300) { - await NCManageDatabase.shared.setMetadataSessionAsync(ocId: metadata.ocId, - session: self.networking.sessionUploadBackground, - sessionError: "", - status: global.metadataStatusWaitUpload) + for metadata in metadatas where + metadata.status == global.metadataStatusUploadError && + metadata.errorCode != NSURLErrorUserAuthenticationRequired && + metadata.backgroundUploadJobIdentifier.isEmpty && + (metadata.sessionDate ?? .distantFuture) < Date().addingTimeInterval(-300) { + + await NCManageDatabase.shared.setMetadataSessionAsync( + ocId: metadata.ocId, + session: networking.sessionUploadBackground, + sessionError: "", + status: global.metadataStatusWaitUpload + ) } - // UPLOAD + // UPLOAD (NO backgroundUploadJobIdentifier) // - let metadatasWaitUpload = Array(metadatas - .filter { - sessionForUpload.contains($0.session) && - $0.status == NCGlobal.shared.metadataStatusWaitUpload - } - .sorted { // Earlier dates first; nils go to the end - ($0.sessionDate ?? .distantFuture) < ($1.sessionDate ?? .distantFuture) - } - .prefix(availableProcess)) + let metadatasWaitUpload = Array( + metadatas + .filter { + $0.backgroundUploadJobIdentifier.isEmpty && + sessionForUpload.contains($0.session) && + $0.status == global.metadataStatusWaitUpload + } + .sorted { + ($0.sessionDate ?? .distantFuture) < + ($1.sessionDate ?? .distantFuture) + } + .prefix(availableProcess) + ) for metadata in metadatasWaitUpload { guard availableProcess > 0, timer != nil else { return } diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift index 9845f8962e..7e8777c169 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift @@ -147,30 +147,91 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } Task { await database.updateAccountPropertyAsync(\.autoUploadSinceDate, value: autoUploadSinceDate, account: session.account) + + if #available(iOS 27, *) { + _ = await NCBackgroundUploadExtensionManager.shared.ensureEnabled() + } } } /// Updates the auto-upload full content setting. func handleAutoUploadChange(newValue: Bool, assetCollections: [PHAssetCollection]) { + let accountIdentifier = session.account + Task { - if let tblAccount = await self.database.getTableAccountAsync(predicate: NSPredicate(format: "account == %@", session.account)), - tblAccount.autoUploadStart == newValue { + guard let account = await database.getTableAccountAsync( + predicate: NSPredicate(format: "account == %@", accountIdentifier) + ), + account.autoUploadStart != newValue else { return } - await database.updateAccountPropertyAsync(\.autoUploadStart, value: newValue, account: session.account) - if newValue { - _ = await NCAutoUpload.shared.startManualAutoUploadForAlbums(controller: self.controller, - model: self, - assetCollections: assetCollections, - account: session.account) + let previousAccounts = await database.getTableAccountsAsync( + predicate: NSPredicate( + format: "autoUploadStart == true AND account != %@", + accountIdentifier + ) + ) + + for previousAccount in previousAccounts { + await database.setAutoUploadStartAsync( + false, + account: previousAccount.account + ) + + await cancelAutoUploadTransfers( + account: previousAccount.account + ) + } + + await database.setAutoUploadStartAsync(true, account: accountIdentifier) + + _ = await NCAutoUpload.shared.startManualAutoUploadForAlbums( + controller: controller, + model: self, + assetCollections: assetCollections, + account: accountIdentifier + ) } else { - await database.clearMetadatasUploadAsync(account: session.account) + await database.setAutoUploadStartAsync(false, account: accountIdentifier) + await cancelAutoUploadTransfers(account: accountIdentifier) + + if #available(iOS 27, *) { + _ = await NCBackgroundUploadExtensionManager.shared.disableIfIdle() + } } } } + private func cancelAutoUploadTransfers(account: String) async { + await database.requestBackgroundAutoUploadCancellationAsync(account: account) + + let predicate = NSPredicate( + format: "account == %@ AND sessionSelector == %@ AND backgroundUploadJobIdentifier == '' AND status != %d", + account, + NCGlobal.shared.selectorUploadAutoUpload, + NCGlobal.shared.metadataStatusNormal + ) + + let metadatas: [tableMetadata] = await database.getMetadatasAsync( + predicate: predicate + ) + + for metadata in metadatas { + await NCNetworking.shared.cancelTask(metadata: metadata) + } + } + + func getOtherAutoUploadAccount() async -> tableAccount? { + await database.getTableAccountAsync( + predicate: NSPredicate( + format: "autoUploadStart == true AND account != %@", + session.account + ) + ) + } + /// Updates the auto-upload create subfolder setting. func handleAutoUploadCreateSubfolderChange(newValue: Bool) { Task { diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 3c3024697a..878986933a 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -6,11 +6,10 @@ import SwiftUI import UIKit -/// A view that allows the user to configure the `auto upload settings for Nextcloud` +/// A view that allows the user to configure the auto upload settings for Nextcloud. @MainActor struct NCAutoUploadView: View { @State private var reachedAnchor = false - @StateObject var model: NCAutoUploadModel @StateObject var albumModel: AlbumModel @State private var showUploadFolder = false @@ -20,6 +19,9 @@ struct NCAutoUploadView: View { @State private var showFocusedAutoUploadProgress = false @State private var openFocusedAutoUploadFinish = false @State private var startAutoUpload = false + @State private var showReplaceAutoUploadAccount = false + @State private var replaceAutoUploadAccountName = "" + @State private var autoUploadAccountReplacementConfirmed = false @Environment(NCAutoUploadCounter.self) private var autoUploadCounter var body: some View { @@ -57,26 +59,58 @@ struct NCAutoUploadView: View { model.checkPermission() } .alert(model.error, isPresented: $model.showErrorAlert) { - Button(NSLocalizedString("_ok_", comment: ""), role: .cancel) { } + Button(NSLocalizedString("_ok_", comment: ""), role: .cancel) {} + } + .confirmationDialog( + NSLocalizedString("_change_autoupload_account_title_", comment: ""), + isPresented: $showReplaceAutoUploadAccount, + titleVisibility: .visible + ) { + Button(NSLocalizedString("_continue_", comment: ""), role: .destructive) { + autoUploadAccountReplacementConfirmed = true + model.autoUploadStart = true + } + + Button(NSLocalizedString("_cancel_", comment: ""), role: .cancel) {} + } message: { + Text( + String( + format: NSLocalizedString("_change_autoupload_account_message_", comment: ""), + replaceAutoUploadAccountName + ) + ) } .sheet(isPresented: $showUploadFolder) { - SelectView(serverUrl: $model.serverUrl, includeDirectoryE2EEncryption: false, session: model.session, controller: model.controller) - .onDisappear { - model.setAutoUploadDirectory(serverUrl: model.serverUrl) - } + SelectView( + serverUrl: $model.serverUrl, + includeDirectoryE2EEncryption: false, + session: model.session, + controller: model.controller + ) + .onDisappear { + model.setAutoUploadDirectory(serverUrl: model.serverUrl) + } } .sheet(isPresented: $showSelectAlbums) { SelectAlbumView(model: albumModel) } .sheet(isPresented: $showUploadAllPhotosWarning) { - ConfirmAutoUploadSheet(model: model, isPresented: $showUploadAllPhotosWarning) + ConfirmAutoUploadSheet( + model: model, + isPresented: $showUploadAllPhotosWarning + ) .presentationDetents([.medium, .large]) } .sheet(isPresented: $showFocusedAutoUploadIntro, onDismiss: { - guard openFocusedAutoUploadFinish else { return } + guard openFocusedAutoUploadFinish else { + return + } openFocusedAutoUploadFinish = false - guard autoUploadCounter.hasItemsToUpload else { return } + + guard autoUploadCounter.hasItemsToUpload else { + return + } showFocusedAutoUploadProgress = true }) { @@ -87,11 +121,13 @@ struct NCAutoUploadView: View { .presentationDetents([.large]) } .fullScreenCover(isPresented: $showFocusedAutoUploadProgress) { - NCFocusedAutoUploadProgressView(isPresented: $showFocusedAutoUploadProgress, - account: model.session.account, - urlBase: model.session.urlBase, - userId: model.session.userId) - .environment(autoUploadCounter) + NCFocusedAutoUploadProgressView( + isPresented: $showFocusedAutoUploadProgress, + account: model.session.account, + urlBase: model.session.urlBase, + userId: model.session.userId + ) + .environment(autoUploadCounter) } .onChange(of: model.autoUploadStart) { _, newValue in if !newValue { @@ -99,6 +135,7 @@ struct NCAutoUploadView: View { showFocusedAutoUploadProgress = false openFocusedAutoUploadFinish = false } + updateAutoUploadCounterSubscription() } } @@ -140,154 +177,277 @@ struct NCAutoUploadView: View { Group { Section(content: { - Button(action: { + Button { showUploadFolder.toggle() - }, label: { + } label: { HStack { Image(systemName: "folder") .font(.icon()) .frame(width: 26) .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) .opacity(model.autoUploadStart ? 0.15 : 1) + Text(NSLocalizedString("_destination_", comment: "")) .font(.body) .opacity(model.autoUploadStart ? 0.5 : 1) .tint(.primary) + Text(model.returnPath()) .font(.body) .tint(.primary) .frame(maxWidth: .infinity, alignment: .trailing) .opacity(model.autoUploadStart ? 0.5 : 1) } - }) + } }) Section(content: { NavigationLink(destination: SelectAlbumView(model: albumModel)) { - Button(action: { + Button { showSelectAlbums.toggle() - }, label: { + } label: { HStack { Image(systemName: "person.2.crop.square.stack") .font(.icon()) .frame(width: 26) .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) .opacity(model.autoUploadStart ? 0.3 : 1) + Text(NSLocalizedString("_upload_from_", comment: "")) .font(.body) .tint(.primary) - Text(NSLocalizedString(model.createAlbumTitle(autoUploadAlbumIds: albumModel.autoUploadAlbumIds), comment: "")) - .font(.body) - .frame(maxWidth: .infinity, alignment: .trailing) - .tint(.primary) + + Text( + NSLocalizedString( + model.createAlbumTitle( + autoUploadAlbumIds: albumModel.autoUploadAlbumIds + ), + comment: "" + ) + ) + .font(.body) + .frame(maxWidth: .infinity, alignment: .trailing) + .tint(.primary) } - }) + } } - Toggle(NSLocalizedString("_back_up_new_photos_only_", comment: ""), isOn: Binding( - get: { - model.autoUploadSinceDate != nil - }, - set: { newValue in - model.handleAutoUploadOnlyNew(newValue: newValue) - } - )) + Toggle( + NSLocalizedString("_back_up_new_photos_only_", comment: ""), + isOn: Binding( + get: { + model.autoUploadSinceDate != nil + }, + set: { newValue in + model.handleAutoUploadOnlyNew(newValue: newValue) + } + ) + ) .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) .opacity(model.autoUploadStart ? 0.15 : 1) .accessibilityIdentifier("NewPhotosToggle") }, footer: { if let date = model.autoUploadSinceDate { - Text(String(format: NSLocalizedString("_new_photos_starting_", comment: ""), NCUtility().longDate(date))) - .font(.footnote) + Text( + String( + format: NSLocalizedString( + "_new_photos_starting_", + comment: "" + ), + NCUtility().longDate(date) + ) + ) + .font(.footnote) } }) - // Auto Upload Photo Section(content: { - Toggle(NSLocalizedString("_autoupload_photos_", comment: ""), isOn: $model.autoUploadImage) - .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.autoUploadImage) { _, newValue in - if !newValue { model.autoUploadVideo = true } - model.handleAutoUploadImageChange(newValue: newValue) + Toggle( + NSLocalizedString("_autoupload_photos_", comment: ""), + isOn: $model.autoUploadImage + ) + .font(.body) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) + .opacity(model.autoUploadStart ? 0.15 : 1) + .onChange(of: model.autoUploadImage) { _, newValue in + if !newValue { + model.autoUploadVideo = true } - if model.autoUploadImage { - Toggle(NSLocalizedString("_wifi_only_", comment: ""), isOn: $model.autoUploadWWAnPhoto) - .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.autoUploadWWAnPhoto) { _, newValue in - model.handleAutoUploadWWAnPhotoChange(newValue: newValue) - } + model.handleAutoUploadImageChange(newValue: newValue) } - }) - // Auto Upload Video - Section(content: { - Toggle(NSLocalizedString("_autoupload_videos_", comment: ""), isOn: $model.autoUploadVideo) + if model.autoUploadImage { + Toggle( + NSLocalizedString("_wifi_only_", comment: ""), + isOn: $model.autoUploadWWAnPhoto + ) .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.autoUploadVideo) { _, newValue in - if !newValue { model.autoUploadImage = true } - model.handleAutoUploadVideoChange(newValue: newValue) + .onChange(of: model.autoUploadWWAnPhoto) { _, newValue in + model.handleAutoUploadWWAnPhotoChange( + newValue: newValue + ) } - - if model.autoUploadVideo { - Toggle(NSLocalizedString("_wifi_only_", comment: ""), isOn: $model.autoUploadWWAnVideo) - .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.autoUploadWWAnVideo) { _, newValue in - model.handleAutoUploadWWAnVideoChange(newValue: newValue) - } } }) - // Auto Upload create subfolder Section(content: { - Toggle(NSLocalizedString("_autoupload_create_subfolder_", comment: ""), isOn: $model.autoUploadCreateSubfolder) + Toggle( + NSLocalizedString("_autoupload_videos_", comment: ""), + isOn: $model.autoUploadVideo + ) + .font(.body) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) + .opacity(model.autoUploadStart ? 0.15 : 1) + .onChange(of: model.autoUploadVideo) { _, newValue in + if !newValue { + model.autoUploadImage = true + } + + model.handleAutoUploadVideoChange(newValue: newValue) + } + + if model.autoUploadVideo { + Toggle( + NSLocalizedString("_wifi_only_", comment: ""), + isOn: $model.autoUploadWWAnVideo + ) .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.autoUploadCreateSubfolder) { _, newValue in - model.handleAutoUploadCreateSubfolderChange(newValue: newValue) + .onChange(of: model.autoUploadWWAnVideo) { _, newValue in + model.handleAutoUploadWWAnVideoChange( + newValue: newValue + ) } + } + }) + + Section(content: { + Toggle( + NSLocalizedString( + "_autoupload_create_subfolder_", + comment: "" + ), + isOn: $model.autoUploadCreateSubfolder + ) + .font(.body) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) + .opacity(model.autoUploadStart ? 0.15 : 1) + .onChange(of: model.autoUploadCreateSubfolder) { _, newValue in + model.handleAutoUploadCreateSubfolderChange( + newValue: newValue + ) + } if model.autoUploadCreateSubfolder { - Picker(NSLocalizedString("_autoupload_subfolder_granularity_", comment: ""), selection: $model.autoUploadSubfolderGranularity) { - Text(NSLocalizedString("_daily_", comment: "")).tag(Granularity.daily) + Picker( + NSLocalizedString( + "_autoupload_subfolder_granularity_", + comment: "" + ), + selection: $model.autoUploadSubfolderGranularity + ) { + Text(NSLocalizedString("_daily_", comment: "")) + .tag(Granularity.daily) .font(.body) - Text(NSLocalizedString("_monthly_", comment: "")).tag(Granularity.monthly) + + Text(NSLocalizedString("_monthly_", comment: "")) + .tag(Granularity.monthly) .font(.body) - Text(NSLocalizedString("_yearly_", comment: "")).tag(Granularity.yearly) + + Text(NSLocalizedString("_yearly_", comment: "")) + .tag(Granularity.yearly) .font(.body) } .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.autoUploadSubfolderGranularity) { _, newValue in - model.handleAutoUploadSubfolderGranularityChange(newValue: newValue) + .onChange( + of: model.autoUploadSubfolderGranularity + ) { _, newValue in + model.handleAutoUploadSubfolderGranularityChange( + newValue: newValue + ) } } }, footer: { - Text(NSLocalizedString("_autoupload_create_subfolder_footer_", comment: "")) - .font(.footnote) + Text( + NSLocalizedString( + "_autoupload_create_subfolder_footer_", + comment: "" + ) + ) + .font(.footnote) }) - // Location Section(content: { - Toggle(NSLocalizedString("_enable_background_location_title_", comment: ""), isOn: $model.locationAutoUploadPermissionGranted) - .font(.body) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) - .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.locationAutoUploadPermissionGranted) { _, newValue in - model.handleLocationChange(newValue: newValue) - } + Toggle( + NSLocalizedString( + "_enable_background_location_title_", + comment: "" + ), + isOn: $model.locationAutoUploadPermissionGranted + ) + .font(.body) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) + .opacity(model.autoUploadStart ? 0.15 : 1) + .onChange( + of: model.locationAutoUploadPermissionGranted + ) { _, newValue in + model.handleLocationChange(newValue: newValue) + } }, footer: { - Text(NSLocalizedString("_enable_background_location_footer_", comment: "")) - .font(.footnote) + Text( + NSLocalizedString( + "_enable_background_location_footer_", + comment: "" + ) + ) + .font(.footnote) }) } .disabled(model.autoUploadStart) @@ -301,18 +461,32 @@ struct NCAutoUploadView: View { @ViewBuilder var autoUploadStartButton: some View { - Section(content: { - let toggle = Toggle(isOn: model.autoUploadSinceDate != nil || model.autoUploadStart ? $model.autoUploadStart : $showUploadAllPhotosWarning) { - Text(model.autoUploadStart ? "_stop_autoupload_" : "_start_autoupload_") - .font(.body) - .padding(.horizontal, 20) - .padding(.vertical, 10) + Section { + let toggleBinding = model.autoUploadSinceDate != nil || + model.autoUploadStart + ? $model.autoUploadStart + : $showUploadAllPhotosWarning + + let toggle = Toggle(isOn: toggleBinding) { + Text( + model.autoUploadStart + ? "_stop_autoupload_" + : "_start_autoupload_" + ) + .font(.body) + .padding(.horizontal, 20) + .padding(.vertical, 10) } .cappedFont(.body, maxDynamicType: .accessibility2) - .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) + .tint( + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + ) .onChange(of: model.autoUploadStart) { _, newValue in - albumModel.populateSelectedAlbums() - model.handleAutoUploadChange(newValue: newValue, assetCollections: albumModel.selectedAlbums) + handleAutoUploadStartChange(newValue) } .font(.headline) @@ -324,16 +498,53 @@ struct NCAutoUploadView: View { } else { toggle .font(.body) - .toggleStyle(AutoUploadProminentButtonStyle(model: model)) + .toggleStyle( + AutoUploadProminentButtonStyle(model: model) + ) + } + } + } + + private func handleAutoUploadStartChange(_ newValue: Bool) { + albumModel.populateSelectedAlbums() + + if newValue && !autoUploadAccountReplacementConfirmed { + let assetCollections = albumModel.selectedAlbums + + Task { + if let account = await model.getOtherAutoUploadAccount() { + replaceAutoUploadAccountName = account.alias.isEmpty + ? account.account + : account.alias + + model.autoUploadStart = false + showReplaceAutoUploadAccount = true + } else { + model.handleAutoUploadChange( + newValue: true, + assetCollections: assetCollections + ) + } } - }) + + return + } + + autoUploadAccountReplacementConfirmed = false + + model.handleAutoUploadChange( + newValue: newValue, + assetCollections: albumModel.selectedAlbums + ) } private func updateAutoUploadCounterSubscription() { - autoUploadCounter.start(account: model.session.account, - urlBase: model.session.urlBase, - userId: model.session.userId, - autoUploadStart: model.autoUploadStart) + autoUploadCounter.start( + account: model.session.account, + urlBase: model.session.urlBase, + userId: model.session.userId, + autoUploadStart: model.autoUploadStart + ) } private func stopAutoUploadCounterSubscription() { @@ -347,6 +558,7 @@ var noPermissionsView: some View { Text("_access_photo_not_enabled_") .padding() .font(.body) + Text("_access_photo_not_enabled_msg_") .font(.body) } @@ -355,10 +567,18 @@ var noPermissionsView: some View { .background(Color(UIColor.systemGroupedBackground)) } -// Custom prominent brand button style used for Toggle-as-Button +/// A prominent brand style used for toggle buttons. private struct AutoUploadProminentButtonStyle: ToggleStyle { let model: NCAutoUploadModel - private var onBackground: Color { Color(NCBrandColor.shared.getElement(account: model.session.account)) } + + private var onBackground: Color { + Color( + NCBrandColor.shared.getElement( + account: model.session.account + ) + ) + } + private let offBackground = Color(UIColor.systemGray5) private let onForeground = Color.white private let offForeground = Color.primary @@ -369,17 +589,41 @@ private struct AutoUploadProminentButtonStyle: ToggleStyle { configuration.isOn.toggle() } label: { configuration.label - .foregroundColor(configuration.isOn ? onForeground : offForeground) + .foregroundColor( + configuration.isOn + ? onForeground + : offForeground + ) .padding(.vertical, 10) - .contentShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + .contentShape( + RoundedRectangle( + cornerRadius: cornerRadius, + style: .continuous + ) + ) } .buttonStyle(.plain) .background( - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - .fill((configuration.isOn ? onBackground : offBackground)) + RoundedRectangle( + cornerRadius: cornerRadius, + style: .continuous + ) + .fill( + configuration.isOn + ? onBackground + : offBackground + ) + ) + .animation( + .easeOut(duration: 0.15), + value: configuration.isOn + ) + .shadow( + color: .black.opacity(0.2), + radius: 10, + x: 0, + y: 3 ) - .animation(.easeOut(duration: 0.15), value: configuration.isOn) - .shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: 3) } } @@ -389,28 +633,42 @@ struct ConfirmAutoUploadSheet: View { var body: some View { VStack(spacing: 16) { - // Title - Text(NSLocalizedString("_auto_upload_all_photos_warning_title_", comment: "")) - .font(.headline) - .multilineTextAlignment(.center) + Text( + NSLocalizedString( + "_auto_upload_all_photos_warning_title_", + comment: "" + ) + ) + .font(.headline) + .multilineTextAlignment(.center) - // Message - Text(NSLocalizedString("_auto_upload_all_photos_warning_message_", comment: "")) - .font(.subheadline) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) + Text( + NSLocalizedString( + "_auto_upload_all_photos_warning_message_", + comment: "" + ) + ) + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) - Spacer().frame(height: 20) + Spacer() + .frame(height: 20) if model.existsAutoUpload() { Button { model.autoUploadStart = true isPresented = false } label: { - Text(NSLocalizedString("_confirm_continue_", comment: "")) - .font(.body) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) + Text( + NSLocalizedString( + "_confirm_continue_", + comment: "" + ) + ) + .font(.body) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) @@ -419,15 +677,20 @@ struct ConfirmAutoUploadSheet: View { model.autoUploadStart = true isPresented = false } label: { - Text(NSLocalizedString("_confirm_resetting_", comment: "")) - .font(.body) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - + Text( + NSLocalizedString( + "_confirm_resetting_", + comment: "" + ) + ) + .font(.body) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - Spacer().frame(height: 20) + Spacer() + .frame(height: 20) Button(role: .cancel) { model.autoUploadStart = false @@ -439,7 +702,6 @@ struct ConfirmAutoUploadSheet: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - } else { Button { model.autoUploadStart = true @@ -452,7 +714,8 @@ struct ConfirmAutoUploadSheet: View { } .buttonStyle(.borderedProminent) - Spacer().frame(height: 20) + Spacer() + .frame(height: 20) Button(role: .cancel) { model.autoUploadStart = false @@ -471,6 +734,9 @@ struct ConfirmAutoUploadSheet: View { } #Preview { - NCAutoUploadView(model: NCAutoUploadModel(controller: nil), albumModel: AlbumModel(controller: nil)) - .environment(NCAutoUploadCounter()) + NCAutoUploadView( + model: NCAutoUploadModel(controller: nil), + albumModel: AlbumModel(controller: nil) + ) + .environment(NCAutoUploadCounter()) } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 139af9a09e..bb00a8d909 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -716,6 +716,8 @@ "_select_date_" = "Select date"; "_always_play_with_vlc_" = "Always play with VLC"; "_account_not_available_" = "Account not available"; +"_change_autoupload_account_title_" = "Change auto upload account?"; +"_change_autoupload_account_message_" = "Auto upload is already enabled for “%@”. Continuing will disable it for that account and cancel its pending uploads."; // Tip "_tip_pdf_thumbnails_" = "Swipe left from the right edge of the screen to show the thumbnails"; diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index 575ac01607..fc0041d7b6 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -112,10 +112,22 @@ final class TransfersViewModel: ObservableObject, NCMetadataDownloadTransfersSuc } func cancel(item: tableMetadata) async { - guard let metadata = await self.database.getMetadataFromOcIdAndocIdTransferAsync(item.ocIdTransfer) else { + guard let metadata = await database.getMetadataFromOcIdAndocIdTransferAsync(item.ocIdTransfer) else { return } - await NCNetworking.shared.cancelTask(metadata: metadata) + + guard !metadata.backgroundUploadJobIdentifier.isEmpty else { + await networking.cancelTask(metadata: metadata) + return + } + + if metadata.backgroundUploadJobIdentifier == "pending" { + await database.deleteMetadataAsync(id: metadata.ocId) + return + } + + metadata.backgroundUploadCancellationRequested = true + await database.replaceMetadataAsync(ocId: metadata.ocId, metadata: metadata) } func progress(for item: tableMetadata) -> Float { diff --git a/iOSClient/Utility/NCUtility.swift b/iOSClient/Utility/NCUtility.swift index b209d5ba15..dfb57bbaf2 100644 --- a/iOSClient/Utility/NCUtility.swift +++ b/iOSClient/Utility/NCUtility.swift @@ -138,7 +138,7 @@ final class NCUtility: NSObject, Sendable { return isEqual } - #if !EXTENSION_FILE_PROVIDER_EXTENSION +#if !EXTENSION_FILE_PROVIDER_EXTENSION && !EXTENSION_BACKGROUNDUPLOAD func getLocation(latitude: Double, longitude: Double, completion: @escaping (String?) -> Void) { let geocoder = CLGeocoder() let llocation = CLLocation(latitude: latitude, longitude: longitude) @@ -159,7 +159,7 @@ final class NCUtility: NSObject, Sendable { } } } - #endif +#endif // https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/19692719#19692719 // https://stackoverflow.com/questions/27556807/swift-pointer-problems-with-mach-task-basic-info/27559770#27559770 diff --git a/iOSClient/Utility/NCUtilityFileSystem.swift b/iOSClient/Utility/NCUtilityFileSystem.swift index 86d0898a32..c687ac9edf 100644 --- a/iOSClient/Utility/NCUtilityFileSystem.swift +++ b/iOSClient/Utility/NCUtilityFileSystem.swift @@ -228,7 +228,7 @@ final class NCUtilityFileSystem: NSObject, @unchecked Sendable { let fileNameSize: UInt64 = fileNameAttribute[FileAttributeKey.size] as? UInt64 ?? 0 let fileNameViewAttribute = try fileManager.attributesOfItem(atPath: fileNameViewPath) let fileNameViewSize: UInt64 = fileNameViewAttribute[FileAttributeKey.size] as? UInt64 ?? 0 -#if EXTENSION_FILE_PROVIDER_EXTENSION +#if EXTENSION_FILE_PROVIDER_EXTENSION || EXTENSION_BACKGROUNDUPLOAD return (fileNameViewSize == metadata.size) && metadata.size > 0 #else if metadata.isDirectoryE2EE == true { @@ -903,31 +903,34 @@ final class NCUtilityFileSystem: NSObject, @unchecked Sendable { } } - func createGranularityPath(asset: PHAsset? = nil, serverUrlBase: String? = nil) -> String { - let autoUploadSubfolderGranularity = NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularity() + func createGranularityPath(asset: PHAsset? = nil, serverUrlBase: String? = nil, granularity: Int? = nil) -> String { + let selectedGranularity = granularity ?? NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularity() let dateFormatter = DateFormatter() let date = asset?.creationDate ?? Date() var path = "" dateFormatter.dateFormat = "yyyy" let year = dateFormatter.string(from: date) + dateFormatter.dateFormat = "MM" let month = dateFormatter.string(from: date) + dateFormatter.dateFormat = "dd" let day = dateFormatter.string(from: date) - if autoUploadSubfolderGranularity == NCGlobal.shared.subfolderGranularityYearly { - path = "\(year)" - } else if autoUploadSubfolderGranularity == NCGlobal.shared.subfolderGranularityDaily { + + if selectedGranularity == NCGlobal.shared.subfolderGranularityYearly { + path = year + } else if selectedGranularity == NCGlobal.shared.subfolderGranularityDaily { path = "\(year)/\(month)/\(day)" - } else { // Month Granularity is default + } else { path = "\(year)/\(month)" } if let serverUrlBase { return serverUrlBase + "/" + path - } else { - return path } + + return path } func extractFileIdFromFPath(from urlString: String?) -> String? {