From 9f00fe0c60b7793a629cae724b0360fb27aeb618 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jul 2026 08:22:00 +0200 Subject: [PATCH 1/7] feat: persist media preview backfill failures Add Realm-backed APIs to track and clear media preview backfill failures, and bump the database schema to version 413. Signed-off-by: Marino Faggiana --- Brand/Database.swift | 2 +- Nextcloud.xcodeproj/project.pbxproj | 22 +++- ...CManageDatabase+MediaPreviewBackfill.swift | 111 ++++++++++++++++++ 3 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift diff --git a/Brand/Database.swift b/Brand/Database.swift index b829712b0d..3e692dd6b7 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 = 412 +let databaseSchemaVersion: UInt64 = 413 diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 5302517d6b..8f4b36cc1e 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -763,6 +763,11 @@ F7BD0A022C4689A4003A4A6D /* NCMedia+CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BD0A012C4689A4003A4A6D /* NCMedia+CollectionViewDelegate.swift */; }; F7BD0A042C4689E9003A4A6D /* NCMedia+MediaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BD0A032C4689E9003A4A6D /* NCMedia+MediaLayout.swift */; }; F7BD71E62636EAFC00643C34 /* NCNetworkingE2EE.swift in Sources */ = {isa = PBXBuildFile; fileRef = F785EE9C246196DF00B3F945 /* NCNetworkingE2EE.swift */; }; + F7BDC1D2300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; + F7BDC1D3300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; + F7BDC1D4300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; + F7BDC1D5300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; + F7BDC1D6300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; F7BF9D832934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; F7BF9D842934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; @@ -1756,6 +1761,7 @@ F7BD09FF2C468925003A4A6D /* NCMedia+CollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+CollectionViewDataSource.swift"; sourceTree = ""; }; F7BD0A012C4689A4003A4A6D /* NCMedia+CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+CollectionViewDelegate.swift"; sourceTree = ""; }; F7BD0A032C4689E9003A4A6D /* NCMedia+MediaLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+MediaLayout.swift"; sourceTree = ""; }; + F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+MediaPreviewBackfill.swift"; sourceTree = ""; }; F7BE7C25290AC8C9002ABB61 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intent.strings; sourceTree = ""; }; F7BE7C27290ADEFD002ABB61 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intent.strings; sourceTree = ""; }; F7BE7C29290ADEFD002ABB61 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Intent.strings; sourceTree = ""; }; @@ -3094,16 +3100,15 @@ F7BAAD951ED5A63D00B7EAD4 /* Data */ = { isa = PBXGroup; children = ( - F76340F32EBDE9740056F538 /* NCManageDatabaseCore.swift */, F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */, AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */, AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */, F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */, F749B650297B0F2400087535 /* NCManageDatabase+Avatar.swift */, - F7D61EA52EBF168E007F865B /* NCManageDatabase+TableCapabilities.swift */, F763D29C2A249C4500A3C901 /* NCManageDatabase+Capabilities.swift */, F74B6D942A7E239A00F03C5F /* NCManageDatabase+Chunk.swift */, F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */, + F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */, F7D68FCB28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift */, F78A10BE29322E8A008499B8 /* NCManageDatabase+Directory.swift */, AA8D31522D41052300FE2775 /* NCManageDatabase+DownloadLimit.swift */, @@ -3111,22 +3116,24 @@ F73EF7B62B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift */, F73EF7BE2B02250B0087E6E9 /* NCManageDatabase+GPS.swift */, F757CC8129E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift */, - F764C3E02FFB7DF800029FD5 /* NCManageDatabase+MediaMetadataBackfill.swift */, F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */, F77E8C1E2E79717D00EAE68F /* NCManageDatabase+LivePhoto.swift */, F7864ACB2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift */, + F764C3E02FFB7DF800029FD5 /* NCManageDatabase+MediaMetadataBackfill.swift */, + F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */, AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */, - F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */, F7B769A72B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift */, F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */, F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */, F749B649297B0CBB00087535 /* NCManageDatabase+Share.swift */, + F7D61EA52EBF168E007F865B /* NCManageDatabase+TableCapabilities.swift */, F76340FB2EBDF64A0056F538 /* NCManageDatabase+Tag.swift */, F73EF7D62B0226080087E6E9 /* NCManageDatabase+Tip.swift */, F73EF7DE2B02266C0087E6E9 /* NCManageDatabase+Trash.swift */, F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */, - F7CADEFA2EA1591D0057849E /* NCMetadataUploadTranfersSuccess.swift */, + F76340F32EBDE9740056F538 /* NCManageDatabaseCore.swift */, F7C630832FFF6DDF00257EEB /* NCMetadataDownloadTranfersSuccess.swift */, + F7CADEFA2EA1591D0057849E /* NCMetadataUploadTranfersSuccess.swift */, ); path = Data; sourceTree = ""; @@ -4364,6 +4371,7 @@ F7D7A7712DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */, AF4BF62127562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, F702F2D225EE5B5C008F8E80 /* NCGlobal.swift in Sources */, + F7BDC1D3300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */, F7B82F182EBFA3B700F5F242 /* NCNetworking.swift in Sources */, F72FD3BA297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, F3E173C62C9B1067006D177A /* AwakeMode.swift in Sources */, @@ -4571,6 +4579,7 @@ AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */, F74D50362C9856D300BBBF4C /* NCCollectionViewDataSource.swift in Sources */, F7A573692E190387009C9257 /* NCShareExtensionData.swift in Sources */, + F7BDC1D4300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */, F7BD71E62636EAFC00643C34 /* NCNetworkingE2EE.swift in Sources */, F7F878AF1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, F7327E3B2B73B8D600A462C7 /* Array+Extension.swift in Sources */, @@ -4649,6 +4658,7 @@ F711D63128F44801003F43C8 /* IntentHandler.swift in Sources */, F760A4952FE95D33001B212E /* NetworkingTasks.swift in Sources */, F76DEE9728F808AF0041B1C9 /* LockscreenData.swift in Sources */, + F7BDC1D6300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */, F72EA95A28B7BD0D00C88F0C /* FilesWidgetView.swift in Sources */, F768823C2C0DD231001CF441 /* NCPreferences.swift in Sources */, F71F6D082B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, @@ -5145,6 +5155,7 @@ F3E173C02C9B1067006D177A /* AwakeMode.swift in Sources */, F7CAFE182F164B9500DB35A5 /* NCCollectionViewCommon+CellDelegate.swift in Sources */, F711A4DC2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, + F7BDC1D5300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */, AF4BF61E27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5197,6 +5208,7 @@ F768823B2C0DD231001CF441 /* NCPreferences.swift in Sources */, AA8D31532D41052300FE2775 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7A8D74228F18261008BBE1C /* NCUtility.swift in Sources */, + F7BDC1D2300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */, F7A8D73A28F17E28008BBE1C /* NCManageDatabase+Video.swift in Sources */, F7CAFE1F2F17A37C00DB35A5 /* ProgressQuantizer.swift in Sources */, F7D61EA72EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, diff --git a/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift b/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift new file mode 100644 index 0000000000..1be2dbd35c --- /dev/null +++ b/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit +import RealmSwift +import NextcloudKit + +enum MediaPreviewBackfillStatus: Int { + case pending = 0 + case completed + case temporarilyFailed + case permanentlyFailed +} + +final class tableMediaPreviewBackfill: Object { + @Persisted(primaryKey: true) var id: String + + @Persisted var account: String + @Persisted var ocId: String + @Persisted var errorCode: Int = 0 + @Persisted var date: Date = Date() + + convenience init(account: String, ocId: String, errorCode: Int) { + self.init() + + self.id = Self.makeId(account: account, ocId: ocId) + self.account = account + self.ocId = ocId + self.errorCode = errorCode + self.date = Date() + } + + static func makeId(account: String, ocId: String) -> String { + "\(account)|\(ocId)" + } +} + +extension NCManageDatabase { + + // MARK: - Media preview backfill Realm read + + func getMediaPreviewBackfillAsync(account: String, ocId: String) async -> tableMediaPreviewBackfill? { + await core.performRealmReadAsync { realm in + let id = tableMediaPreviewBackfill.makeId(account: account, ocId: ocId) + + guard let backfill = realm.object( + ofType: tableMediaPreviewBackfill.self, + forPrimaryKey: id + ) else { + return nil + } + + return tableMediaPreviewBackfill(value: backfill) + } + } + + func isMediaPreviewBackfillFailedAsync(account: String, ocId: String) async -> Bool { + await core.performRealmReadAsync { realm in + realm.object( + ofType: tableMediaPreviewBackfill.self, + forPrimaryKey: tableMediaPreviewBackfill.makeId( + account: account, + ocId: ocId + ) + ) != nil + } ?? false + } + + // MARK: - Media preview backfill Realm write + + func addMediaPreviewBackfillFailureAsync(account: String, ocId: String, errorCode: Int) async { + await core.performRealmWriteAsync { realm in + let id = tableMediaPreviewBackfill.makeId(account: account, ocId: ocId) + + if let backfill = realm.object(ofType: tableMediaPreviewBackfill.self, forPrimaryKey: id) { + backfill.errorCode = errorCode + backfill.date = Date() + } else { + let backfill = tableMediaPreviewBackfill(account: account, ocId: ocId, errorCode: errorCode) + realm.add(backfill) + } + } + } + + func deleteMediaPreviewBackfillFailureAsync(account: String, ocId: String) async { + await core.performRealmWriteAsync { realm in + let id = tableMediaPreviewBackfill.makeId(account: account, ocId: ocId) + + guard let backfill = realm.object(ofType: tableMediaPreviewBackfill.self, forPrimaryKey: id) else { + return + } + + realm.delete(backfill) + } + } + + func deleteMediaPreviewBackfillFailuresAsync( + account: String + ) async { + await core.performRealmWriteAsync { realm in + let backfills = realm.objects(tableMediaPreviewBackfill.self) + .where { + $0.account == account + } + + realm.delete(backfills) + } + } +} From 9e0ec73e9683aa33368308d7eb31c23e71e040fe Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jul 2026 08:59:43 +0200 Subject: [PATCH 2/7] refactor: split media background processing Extract metadata backfill, placeholder hydration, and preview backfill into dedicated processors and organize processing and refresh sources. Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 36 ++- .../AppDelegate+AppProcessing.swift | 6 +- .../NCMediaMetadataBackfillProcessor.swift} | 135 +--------- ...NCMediaPlaceholderHydrationProcessor.swift | 142 ++++++++++ .../NCMediaPreviewBackfillProcessor.swift | 254 ++++++++++++++++++ .../AppDelegate+AppRefresh.swift | 0 6 files changed, 430 insertions(+), 143 deletions(-) rename iOSClient/{ => Processor}/AppDelegate+AppProcessing.swift (96%) rename iOSClient/{NCMediaMetadataBackgroundProcessor.swift => Processor/NCMediaMetadataBackfillProcessor.swift} (52%) create mode 100644 iOSClient/Processor/NCMediaPlaceholderHydrationProcessor.swift create mode 100644 iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift rename iOSClient/{ => Refresh}/AppDelegate+AppRefresh.swift (100%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8f4b36cc1e..7c276108bb 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -768,6 +768,9 @@ F7BDC1D4300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; F7BDC1D5300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; F7BDC1D6300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */; }; + F7BDC1D8300F4E8E00C5D9FA /* NCMediaMetadataBackfillProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D7300F4E8A00C5D9FA /* NCMediaMetadataBackfillProcessor.swift */; }; + F7BDC1DA300F4F2800C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1D9300F4F2500C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift */; }; + F7BDC1DC300F4F9700C5D9FA /* NCMediaPreviewBackfillProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BDC1DB300F4F9500C5D9FA /* NCMediaPreviewBackfillProcessor.swift */; }; F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; F7BF9D832934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; F7BF9D842934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; @@ -794,7 +797,6 @@ F7C55CC92FB5CE74004A974F /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55CC32FB5CE74004A974F /* ActionViewController.swift */; }; F7C55CCA2FB5CE74004A974F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7C55CC42FB5CE74004A974F /* Images.xcassets */; }; F7C55CCC2FB5CE74004A974F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */; }; - F7C630712FFE206100257EEB /* NCMediaMetadataBackgroundProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C630702FFE206100257EEB /* NCMediaMetadataBackgroundProcessor.swift */; }; F7C6307F2FFE4F8000257EEB /* UIWindowScene+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C6307E2FFE4F8000257EEB /* UIWindowScene+Extension.swift */; }; F7C630802FFE4F8000257EEB /* UIWindowScene+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C6307E2FFE4F8000257EEB /* UIWindowScene+Extension.swift */; }; F7C630872FFF6DE100257EEB /* NCMetadataDownloadTranfersSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C630832FFF6DDF00257EEB /* NCMetadataDownloadTranfersSuccess.swift */; }; @@ -1762,6 +1764,9 @@ F7BD0A012C4689A4003A4A6D /* NCMedia+CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+CollectionViewDelegate.swift"; sourceTree = ""; }; F7BD0A032C4689E9003A4A6D /* NCMedia+MediaLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+MediaLayout.swift"; sourceTree = ""; }; F7BDC1D1300F440600C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+MediaPreviewBackfill.swift"; sourceTree = ""; }; + F7BDC1D7300F4E8A00C5D9FA /* NCMediaMetadataBackfillProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaMetadataBackfillProcessor.swift; sourceTree = ""; }; + F7BDC1D9300F4F2500C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaPlaceholderHydrationProcessor.swift; sourceTree = ""; }; + F7BDC1DB300F4F9500C5D9FA /* NCMediaPreviewBackfillProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaPreviewBackfillProcessor.swift; sourceTree = ""; }; F7BE7C25290AC8C9002ABB61 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intent.strings; sourceTree = ""; }; F7BE7C27290ADEFD002ABB61 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intent.strings; sourceTree = ""; }; F7BE7C29290ADEFD002ABB61 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Intent.strings; sourceTree = ""; }; @@ -1818,7 +1823,6 @@ F7C55CC32FB5CE74004A974F /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; F7C55CC42FB5CE74004A974F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; F7C55CC62FB5CE74004A974F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - F7C630702FFE206100257EEB /* NCMediaMetadataBackgroundProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaMetadataBackgroundProcessor.swift; sourceTree = ""; }; F7C6307E2FFE4F8000257EEB /* UIWindowScene+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowScene+Extension.swift"; sourceTree = ""; }; F7C630832FFF6DDF00257EEB /* NCMetadataDownloadTranfersSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMetadataDownloadTranfersSuccess.swift; sourceTree = ""; }; F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+RecommendedFiles.swift"; sourceTree = ""; }; @@ -3138,6 +3142,25 @@ path = Data; sourceTree = ""; }; + F7BDC1DD300F4FF800C5D9FA /* Processor */ = { + isa = PBXGroup; + children = ( + F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */, + F7BDC1DB300F4F9500C5D9FA /* NCMediaPreviewBackfillProcessor.swift */, + F7BDC1D9300F4F2500C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift */, + F7BDC1D7300F4E8A00C5D9FA /* NCMediaMetadataBackfillProcessor.swift */, + ); + path = Processor; + sourceTree = ""; + }; + F7BDC1DE300F502C00C5D9FA /* Refresh */ = { + isa = PBXGroup; + children = ( + F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */, + ); + path = Refresh; + sourceTree = ""; + }; F7BFFA621A24D7300044ED85 /* Login */ = { isa = PBXGroup; children = ( @@ -3532,16 +3555,15 @@ isa = PBXGroup; children = ( AA517BB42D66149900F8D37C /* .tx */, + F7BDC1DD300F4FF800C5D9FA /* Processor */, + F7BDC1DE300F502C00C5D9FA /* Refresh */, F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, - F7110ADF2F9773210095AA5C /* AppDelegate+AppRefresh.swift */, - F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */, F7CAFE1A2F16AA8600DB35A5 /* main.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */, F77DD6A72C5CC093009448FB /* NCSession.swift */, F76B649B2ADFFAED00014640 /* NCImageCache.swift */, F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */, - F7C630702FFE206100257EEB /* NCMediaMetadataBackgroundProcessor.swift */, F718E2572DF2D5C3004038AF /* NCBackgroundLocationUploadManager.swift */, F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */, F73CB5771ED46807005F2A5A /* NCBridgeSwift.h */, @@ -4830,7 +4852,6 @@ F7CDB5C52FA33CA300F72306 /* NCMediaViewerModel.swift in Sources */, F7CDB5C62FA33CA300F72306 /* NCMediaViewerView.swift in Sources */, F7CDB5CC2FA33CA300F72306 /* NCNextcloudMediaViewerLoader.swift in Sources */, - F7C630712FFE206100257EEB /* NCMediaMetadataBackgroundProcessor.swift in Sources */, F76341182EBE0BC60056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F79377052FBD86AF00DE56DE /* NCMediaViewerFloatingTitleView.swift in Sources */, F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */, @@ -4838,6 +4859,7 @@ F34E1AD92ECC839100FA10C3 /* EmojiTextField.swift in Sources */, F768822E2C0DD1E7001CF441 /* NCSettingsBundleHelper.swift in Sources */, F72408332B8A27C900F128E2 /* NCMedia+Command.swift in Sources */, + F7BDC1DC300F4F9700C5D9FA /* NCMediaPreviewBackfillProcessor.swift in Sources */, F755CB402B8CB13C00CE27E9 /* NCMediaLayout.swift in Sources */, F73EF7B72B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */, AF4BF61927562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */, @@ -5097,6 +5119,7 @@ F7327E352B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift in Sources */, F76687072B7D067400779E3F /* NCAudioRecorderViewController.swift in Sources */, F77B0F893008ABDC00EE4AE1 /* NCBlurEffectsTestView.swift in Sources */, + F7BDC1D8300F4E8E00C5D9FA /* NCMediaMetadataBackfillProcessor.swift in Sources */, AA8E03DA2D2ED83300E7E89C /* TransientShare.swift in Sources */, F7A03E2F2D425A14007AA677 /* NCFavoriteNavigationController.swift in Sources */, F343A4BB2A1E734600DDA874 /* Optional+Extension.swift in Sources */, @@ -5121,6 +5144,7 @@ F7C6307F2FFE4F8000257EEB /* UIWindowScene+Extension.swift in Sources */, F75CA1472962F13700B01130 /* NCHUDView.swift in Sources */, F77BB748289985270090FC19 /* UITabBarController+Extension.swift in Sources */, + F7BDC1DA300F4F2800C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift in Sources */, F7DA0F672F66AA0D0033020C /* ShowBanner.swift in Sources */, F752BA052E58C05200616A26 /* Maintenance.swift in Sources */, F763D29D2A249C4500A3C901 /* NCManageDatabase+Capabilities.swift in Sources */, diff --git a/iOSClient/AppDelegate+AppProcessing.swift b/iOSClient/Processor/AppDelegate+AppProcessing.swift similarity index 96% rename from iOSClient/AppDelegate+AppProcessing.swift rename to iOSClient/Processor/AppDelegate+AppProcessing.swift index 51ce75f833..565cd78ddb 100644 --- a/iOSClient/AppDelegate+AppProcessing.swift +++ b/iOSClient/Processor/AppDelegate+AppProcessing.swift @@ -95,13 +95,11 @@ extension AppDelegate { return false } - let mediaProcessor = NCMediaMetadataBackgroundProcessor() - nkLog(tag: self.global.logTagMediaBackfill, emoji: .start, message: "Start media metadata backfill for account \(activeAccount.account)") - let backfillStatus = await mediaProcessor.runBackfill( + let backfillStatus = await NCMediaMetadataBackfillProcessor().runBackfill( account: activeAccount, limit: 250 ) { offset, inserted, updated in @@ -123,7 +121,7 @@ extension AppDelegate { emoji: .start, message: "Start media metadata placeholder hydration for account \(account.account)") - let hydrationStatus = await mediaProcessor.runPlaceholderHydration( + let hydrationStatus = await NCMediaPlaceholderHydrationProcessor().runPlaceholderHydration( account: account, limit: 100 ) { succeeded in diff --git a/iOSClient/NCMediaMetadataBackgroundProcessor.swift b/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift similarity index 52% rename from iOSClient/NCMediaMetadataBackgroundProcessor.swift rename to iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift index 2563878f55..3ef69a8e6c 100644 --- a/iOSClient/NCMediaMetadataBackgroundProcessor.swift +++ b/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift @@ -5,7 +5,7 @@ import Foundation import NextcloudKit -final class NCMediaMetadataBackgroundProcessor { +final class NCMediaMetadataBackfillProcessor { enum BackfillStatus { case skippedAlreadyCompleted(account: String) case completed(account: String, processed: Int, inserted: Int, updated: Int) @@ -38,36 +38,6 @@ final class NCMediaMetadataBackgroundProcessor { } } - enum PlaceholderHydrationStatus { - case skippedNoPlaceholders(account: String) - case completed(account: String, total: Int, succeeded: Int, failed: Int) - case cancelled(account: String, total: Int, succeeded: Int, failed: Int) - - var isSuccessful: Bool { - switch self { - case .skippedNoPlaceholders, .completed: - return true - case .cancelled: - return false - } - } - - var logMessage: String { - switch self { - case .skippedNoPlaceholders(let account): - return "Media metadata placeholder hydration skipped for account \(account): no placeholders found" - - case .completed(let account, let total, let succeeded, let failed): - let pending = max(0, total - succeeded - failed) - return "Media metadata placeholder hydration completed for account \(account): total \(total) - succeeded \(succeeded) - failed \(failed) - pending \(pending)" - - case .cancelled(let account, let total, let succeeded, let failed): - let pending = max(0, total - succeeded - failed) - return "Media metadata placeholder hydration cancelled for account \(account): total \(total) - succeeded \(succeeded) - failed \(failed) - pending \(pending)" - } - } - } - /// Progressively scans the media archive and creates missing metadata placeholders. func runBackfill( account: tableAccount, @@ -155,106 +125,5 @@ final class NCMediaMetadataBackgroundProcessor { return .cancelled(account: account.account, processed: processed, inserted: inserted, updated: updated) } - - /// Completes media metadata placeholders by retrieving and storing their full properties. - func runPlaceholderHydration( - account: tableAccount, - limit: Int, - update: @escaping (_ succeeded: Int) async -> Void - ) async -> PlaceholderHydrationStatus { - let database = NCManageDatabase.shared - let maximumConcurrentRequests = min(8, NCBrandOptions.shared.httpMaximumConnectionsPerHost) - - var succeeded = 0 - var failed = 0 - - guard let metadatas = await database.getMetadatasAsync( - predicate: NSPredicate( - format: "account == %@ AND placeholder == true", - account.account - ), - sortedByKeyPath: "date", - ascending: false, - limit: limit - ), !metadatas.isEmpty else { - return .skippedNoPlaceholders(account: account.account) - } - - let total = metadatas.count - - func hydrate(_ metadata: tableMetadata) async -> Bool { - guard !Task.isCancelled else { - return false - } - - let result = await NextcloudKit.shared.readFileOrFolderAsync( - serverUrlFileName: metadata.serverUrlFileName, - depth: "0", - account: metadata.account - ) - - guard !Task.isCancelled else { - return false - } - - switch result.error.errorCode { - case 0: - if let file = result.files?.first { - let metadata = await NCManageDatabaseCreateMetadata().convertFileToMetadataAsync(file) - await database.addMetadataAsync(metadata) - } - return true - - case 404: - await database.deleteMetadataAsync(ocId: metadata.ocId) - return true - - default: - return false - } - } - - await withTaskGroup(of: Bool.self) { group in - var iterator = metadatas.makeIterator() - - for _ in 0.. Void + ) async -> PlaceholderHydrationStatus { + let database = NCManageDatabase.shared + let maximumConcurrentRequests = min(8, NCBrandOptions.shared.httpMaximumConnectionsPerHost) + + var succeeded = 0 + var failed = 0 + + guard let metadatas = await database.getMetadatasAsync( + predicate: NSPredicate( + format: "account == %@ AND placeholder == true", + account.account + ), + sortedByKeyPath: "date", + ascending: false, + limit: limit + ), !metadatas.isEmpty else { + return .skippedNoPlaceholders(account: account.account) + } + + let total = metadatas.count + + func hydrate(_ metadata: tableMetadata) async -> Bool { + guard !Task.isCancelled else { + return false + } + + let result = await NextcloudKit.shared.readFileOrFolderAsync( + serverUrlFileName: metadata.serverUrlFileName, + depth: "0", + account: metadata.account + ) + + guard !Task.isCancelled else { + return false + } + + switch result.error.errorCode { + case 0: + if let file = result.files?.first { + let metadata = await NCManageDatabaseCreateMetadata().convertFileToMetadataAsync(file) + await database.addMetadataAsync(metadata) + } + return true + + case 404: + await database.deleteMetadataAsync(ocId: metadata.ocId) + return true + + default: + return false + } + } + + await withTaskGroup(of: Bool.self) { group in + var iterator = metadatas.makeIterator() + + for _ in 0.. Void + ) async -> PreviewBackfillStatus { + let database = NCManageDatabase.shared + let maximumConcurrentRequests = min( + 8, + NCBrandOptions.shared.httpMaximumConnectionsPerHost + ) + + guard !metadatas.isEmpty else { + return .skippedNoMetadatas( + account: account.account + ) + } + + let total = metadatas.count + + var succeeded = 0 + var failed = 0 + var skipped = 0 + + enum PreviewResult { + case succeeded + case failed + case skipped + case cancelled + } + + func process( + _ metadata: tableMetadata + ) async -> PreviewResult { + guard !Task.isCancelled else { + return .cancelled + } + + let alreadyFailed = await database + .isMediaPreviewBackfillFailedAsync( + account: metadata.account, + ocId: metadata.ocId + ) + + guard !alreadyFailed else { + return .skipped + } + + guard !Task.isCancelled else { + return .cancelled + } + + let error = await requestPreview(metadata: metadata) + + guard !Task.isCancelled else { + return .cancelled + } + + guard error.errorCode == 0 else { + await database.addMediaPreviewBackfillFailureAsync( + account: metadata.account, + ocId: metadata.ocId, + errorCode: error.errorCode + ) + + return .failed + } + + return .succeeded + } + + await withTaskGroup( + of: PreviewResult.self + ) { group in + var iterator = metadatas.makeIterator() + + for _ in 0.. NKError { + guard !Task.isCancelled else { + return NKError( + errorCode: NSURLErrorCancelled, + errorDescription: "Cancelled" + ) + } + + let result = await NextcloudKit.shared.downloadPreviewAsync( + fileId: metadata.fileId, + etag: metadata.etag, + account: metadata.account + ) + + guard !Task.isCancelled else { + return NKError( + errorCode: NSURLErrorCancelled, + errorDescription: "Cancelled" + ) + } + + guard result.error == .success, + let data = result.responseData?.data else { + return result.error + } + + let image = NCUtility().createImageFileFrom(data: data,metadata: metadata, ext: NCGlobal.shared.previewExt1024) + + guard image != nil else { + return NKError( + errorCode: NCGlobal.shared.errorInternalError, + errorDescription: "Unable to create preview image" + ) + } + + return .success + } +} diff --git a/iOSClient/AppDelegate+AppRefresh.swift b/iOSClient/Refresh/AppDelegate+AppRefresh.swift similarity index 100% rename from iOSClient/AppDelegate+AppRefresh.swift rename to iOSClient/Refresh/AppDelegate+AppRefresh.swift From ba3dfba6a85cad97ef9b0631a80ee171aad56dc0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jul 2026 09:52:14 +0200 Subject: [PATCH 3/7] refactor: streamline media preview backfill processing Select a limited batch of missing previews inside the processor and skip previously failed items. Simplify failure tracking and clear it when no previews remain or an account is removed. Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 +- ...CManageDatabase+MediaPreviewBackfill.swift | 91 +++--------- iOSClient/Data/NCManageDatabase.swift | 1 + .../NCMediaPreviewBackfillProcessor.swift | 132 ++++++++---------- 4 files changed, 79 insertions(+), 149 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 7c276108bb..ff4545ed5c 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -3146,9 +3146,9 @@ isa = PBXGroup; children = ( F7110AE32F9774130095AA5C /* AppDelegate+AppProcessing.swift */, - F7BDC1DB300F4F9500C5D9FA /* NCMediaPreviewBackfillProcessor.swift */, - F7BDC1D9300F4F2500C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift */, F7BDC1D7300F4E8A00C5D9FA /* NCMediaMetadataBackfillProcessor.swift */, + F7BDC1D9300F4F2500C5D9FA /* NCMediaPlaceholderHydrationProcessor.swift */, + F7BDC1DB300F4F9500C5D9FA /* NCMediaPreviewBackfillProcessor.swift */, ); path = Processor; sourceTree = ""; diff --git a/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift b/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift index 1be2dbd35c..dc916df550 100644 --- a/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift +++ b/iOSClient/Data/NCManageDatabase+MediaPreviewBackfill.swift @@ -7,33 +7,18 @@ import UIKit import RealmSwift import NextcloudKit -enum MediaPreviewBackfillStatus: Int { - case pending = 0 - case completed - case temporarilyFailed - case permanentlyFailed -} - final class tableMediaPreviewBackfill: Object { - @Persisted(primaryKey: true) var id: String + @Persisted(primaryKey: true) var key: String = "" @Persisted var account: String @Persisted var ocId: String - @Persisted var errorCode: Int = 0 - @Persisted var date: Date = Date() - convenience init(account: String, ocId: String, errorCode: Int) { + convenience init(account: String, ocId: String) { self.init() - self.id = Self.makeId(account: account, ocId: ocId) + self.key = "\(account)-\(ocId)" self.account = account self.ocId = ocId - self.errorCode = errorCode - self.date = Date() - } - - static func makeId(account: String, ocId: String) -> String { - "\(account)|\(ocId)" } } @@ -41,71 +26,27 @@ extension NCManageDatabase { // MARK: - Media preview backfill Realm read - func getMediaPreviewBackfillAsync(account: String, ocId: String) async -> tableMediaPreviewBackfill? { + func getFailedMediaPreviewOcIdsAsync(account: String) async -> Set { await core.performRealmReadAsync { realm in - let id = tableMediaPreviewBackfill.makeId(account: account, ocId: ocId) - - guard let backfill = realm.object( - ofType: tableMediaPreviewBackfill.self, - forPrimaryKey: id - ) else { - return nil - } - - return tableMediaPreviewBackfill(value: backfill) - } - } + let results = realm.objects(tableMediaPreviewBackfill.self) + .where { + $0.account == account + } - func isMediaPreviewBackfillFailedAsync(account: String, ocId: String) async -> Bool { - await core.performRealmReadAsync { realm in - realm.object( - ofType: tableMediaPreviewBackfill.self, - forPrimaryKey: tableMediaPreviewBackfill.makeId( - account: account, - ocId: ocId - ) - ) != nil - } ?? false + return Set(results.map(\.ocId)) + } ?? [] } // MARK: - Media preview backfill Realm write - func addMediaPreviewBackfillFailureAsync(account: String, ocId: String, errorCode: Int) async { - await core.performRealmWriteAsync { realm in - let id = tableMediaPreviewBackfill.makeId(account: account, ocId: ocId) - - if let backfill = realm.object(ofType: tableMediaPreviewBackfill.self, forPrimaryKey: id) { - backfill.errorCode = errorCode - backfill.date = Date() - } else { - let backfill = tableMediaPreviewBackfill(account: account, ocId: ocId, errorCode: errorCode) - realm.add(backfill) - } - } - } - - func deleteMediaPreviewBackfillFailureAsync(account: String, ocId: String) async { + func addMediaPreviewBackfillFailureAsync(account: String, ocId: String) async { await core.performRealmWriteAsync { realm in - let id = tableMediaPreviewBackfill.makeId(account: account, ocId: ocId) - - guard let backfill = realm.object(ofType: tableMediaPreviewBackfill.self, forPrimaryKey: id) else { - return - } - - realm.delete(backfill) - } - } - - func deleteMediaPreviewBackfillFailuresAsync( - account: String - ) async { - await core.performRealmWriteAsync { realm in - let backfills = realm.objects(tableMediaPreviewBackfill.self) - .where { - $0.account == account - } + let item = tableMediaPreviewBackfill() + item.key = "\(account)-\(ocId)" + item.account = account + item.ocId = ocId - realm.delete(backfills) + realm.add(item, update: .modified) } } } diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index 83d15f4f79..567f4b43c5 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -238,6 +238,7 @@ final class NCManageDatabase: @unchecked Sendable { self.clearTable(tableLivePhoto.self, account: account) self.clearTable(tableLocalFile.self, account: account) self.clearTable(tableMediaMetadataBackfill.self, account: account) + self.clearTable(tableMediaPreviewBackfill.self, account: account) self.clearTable(tableMetadata.self, account: account) self.clearTable(tableMetadataTag.self, account: account) self.clearTable(tableRecommendedFiles.self, account: account) diff --git a/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift b/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift index 4da8de1379..89f3642b5a 100644 --- a/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift +++ b/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift @@ -7,25 +7,25 @@ import NextcloudKit final class NCMediaPreviewBackfillProcessor { enum PreviewBackfillStatus { - case skippedNoMetadatas(account: String) + case skippedNoPreviews(account: String) + case completed( account: String, total: Int, succeeded: Int, - failed: Int, - skipped: Int + failed: Int ) + case cancelled( account: String, total: Int, succeeded: Int, - failed: Int, - skipped: Int + failed: Int ) var isSuccessful: Bool { switch self { - case .skippedNoMetadatas, .completed: + case .skippedNoPreviews, .completed: return true case .cancelled: @@ -35,36 +35,29 @@ final class NCMediaPreviewBackfillProcessor { var logMessage: String { switch self { - case .skippedNoMetadatas(let account): - return "Media preview backfill skipped for account \(account): no metadata found" + case .skippedNoPreviews(let account): + return "Media preview backfill skipped for account \(account): no previews to process" case .completed( let account, let total, let succeeded, - let failed, - let skipped + let failed ): - let pending = max( - 0, - total - succeeded - failed - skipped - ) - - return "Media preview backfill completed for account \(account): total \(total) - succeeded \(succeeded) - failed \(failed) - skipped \(skipped) - pending \(pending)" + return "Media preview backfill completed for account \(account): total \(total) - succeeded \(succeeded) - failed \(failed)" case .cancelled( let account, let total, let succeeded, - let failed, - let skipped + let failed ): let pending = max( 0, - total - succeeded - failed - skipped + total - succeeded - failed ) - return "Media preview backfill cancelled for account \(account): total \(total) - succeeded \(succeeded) - failed \(failed) - skipped \(skipped) - pending \(pending)" + return "Media preview backfill cancelled for account \(account): total \(total) - succeeded \(succeeded) - failed \(failed) - pending \(pending)" } } } @@ -72,55 +65,65 @@ final class NCMediaPreviewBackfillProcessor { /// Retrieves missing media previews while skipping previews that previously failed. func runPreviewBackfill( account: tableAccount, - metadatas: [tableMetadata], + limit: Int, update: @escaping ( _ succeeded: Int, - _ failed: Int, - _ skipped: Int + _ failed: Int ) async -> Void ) async -> PreviewBackfillStatus { let database = NCManageDatabase.shared - let maximumConcurrentRequests = min( - 8, - NCBrandOptions.shared.httpMaximumConnectionsPerHost - ) + let utilityFileSystem = NCUtilityFileSystem() + let maximumConcurrentRequests = min(8, NCBrandOptions.shared.httpMaximumConnectionsPerHost) + let session = NCSession.Session(account: account.account, urlBase: account.urlBase, user: account.user, userId: account.userId) + let mediaPredicate = NCImageCache.shared.getMediaPredicate( + session: session, + mediaPath: account.mediaPath, + showOnlyImages: false, + showOnlyVideos: false) + guard let metadatasMedia = await database.getMetadatasAsync(predicate: mediaPredicate, sortedByKeyPath: "date", ascending: false) else { + return .skippedNoPreviews(account: account.account) + } + + let failedOcIds = await database.getFailedMediaPreviewOcIdsAsync(account: account.account) + var metadatas: [tableMetadata] = [] + metadatas.reserveCapacity(limit) + + for metadata in metadatasMedia { + guard !Task.isCancelled else { + break + } + guard !failedOcIds.contains(metadata.ocId) else { + continue + } + let imageExists = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag, userId: metadata.userId, urlBase: metadata.urlBase) + + guard !imageExists else { + continue + } + + metadatas.append(metadata) + if metadatas.count >= limit { + break + } + } guard !metadatas.isEmpty else { - return .skippedNoMetadatas( - account: account.account - ) + await database.clearTableAsync(tableMediaPreviewBackfill.self, account: account.account) + return .skippedNoPreviews(account: account.account) } let total = metadatas.count var succeeded = 0 var failed = 0 - var skipped = 0 enum PreviewResult { case succeeded case failed - case skipped case cancelled } - func process( - _ metadata: tableMetadata - ) async -> PreviewResult { - guard !Task.isCancelled else { - return .cancelled - } - - let alreadyFailed = await database - .isMediaPreviewBackfillFailedAsync( - account: metadata.account, - ocId: metadata.ocId - ) - - guard !alreadyFailed else { - return .skipped - } - + func process(_ metadata: tableMetadata) async -> PreviewResult { guard !Task.isCancelled else { return .cancelled } @@ -132,21 +135,14 @@ final class NCMediaPreviewBackfillProcessor { } guard error.errorCode == 0 else { - await database.addMediaPreviewBackfillFailureAsync( - account: metadata.account, - ocId: metadata.ocId, - errorCode: error.errorCode - ) - + await database.addMediaPreviewBackfillFailureAsync(account: metadata.account, ocId: metadata.ocId) return .failed } return .succeeded } - await withTaskGroup( - of: PreviewResult.self - ) { group in + await withTaskGroup(of: PreviewResult.self) { group in var iterator = metadatas.makeIterator() for _ in 0.. Date: Tue, 21 Jul 2026 09:57:21 +0200 Subject: [PATCH 4/7] feat: run media preview backfill during app processing Add dedicated logging for media preview backfill progress and completion. Signed-off-by: Marino Faggiana --- iOSClient/NCGlobal.swift | 1 + .../Processor/AppDelegate+AppProcessing.swift | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 91fecd360d..7bc8e895ad 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -393,6 +393,7 @@ final class NCGlobal: Sendable { let logTagViewer = "VIEWERS" let logTagMediaBackfill = "MEDIA BACKFILL" let logTagMediaPlaceholder = "MEDIA PLACEHOLDER" + let logTagMediaPreview = "MEDIA PREVIEW" // USER DEFAULTS // diff --git a/iOSClient/Processor/AppDelegate+AppProcessing.swift b/iOSClient/Processor/AppDelegate+AppProcessing.swift index 565cd78ddb..7f36ae7707 100644 --- a/iOSClient/Processor/AppDelegate+AppProcessing.swift +++ b/iOSClient/Processor/AppDelegate+AppProcessing.swift @@ -137,6 +137,27 @@ extension AppDelegate { guard !Task.isCancelled else { return false } + + nkLog(tag: self.global.logTagMediaPreview, + emoji: .start, + message: "Start media preview backfill for account \(account.account)") + + let previewStatus = await NCMediaPreviewBackfillProcessor().runPreviewBackfill( + account: account, + limit: 100 + ) { succeeded, failed in + nkLog(tag: self.global.logTagMediaPreview, + emoji: .info, + message: "Media preview backfill progress: succeeded \(succeeded) - failed \(failed) account \(account.account)") + } + + nkLog(tag: self.global.logTagMediaPreview, + emoji: previewStatus.isSuccessful ? .stop : .error, + message: previewStatus.logMessage) + + guard !Task.isCancelled else { + return false + } } return true From 47fc418967762ac40cb4704fdf72c8bec2f60063 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jul 2026 10:00:17 +0200 Subject: [PATCH 5/7] lint Signed-off-by: Marino Faggiana --- iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift | 1 - iOSClient/Processor/NCMediaPlaceholderHydrationProcessor.swift | 2 -- 2 files changed, 3 deletions(-) diff --git a/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift b/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift index 3ef69a8e6c..9aacb48b57 100644 --- a/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift +++ b/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift @@ -126,4 +126,3 @@ final class NCMediaMetadataBackfillProcessor { return .cancelled(account: account.account, processed: processed, inserted: inserted, updated: updated) } } - diff --git a/iOSClient/Processor/NCMediaPlaceholderHydrationProcessor.swift b/iOSClient/Processor/NCMediaPlaceholderHydrationProcessor.swift index 3112768da0..7120db2830 100644 --- a/iOSClient/Processor/NCMediaPlaceholderHydrationProcessor.swift +++ b/iOSClient/Processor/NCMediaPlaceholderHydrationProcessor.swift @@ -137,6 +137,4 @@ final class NCMediaPlaceholderHydrationProcessor { return .completed(account: account.account, total: total, succeeded: succeeded, failed: failed) } - } - From 71f585c8c7ba3528e4344f6b3692c7bd106829fc Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jul 2026 11:29:22 +0200 Subject: [PATCH 6/7] Update NCMediaPreviewBackfillProcessor.swift --- .../Processor/NCMediaPreviewBackfillProcessor.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift b/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift index 89f3642b5a..6ac1017be0 100644 --- a/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift +++ b/iOSClient/Processor/NCMediaPreviewBackfillProcessor.swift @@ -228,14 +228,7 @@ final class NCMediaPreviewBackfillProcessor { return result.error } - let image = NCUtility().createImageFileFrom(data: data, metadata: metadata, ext: NCGlobal.shared.previewExt1024) - - guard image != nil else { - return NKError( - errorCode: NCGlobal.shared.errorInternalError, - errorDescription: "Unable to create preview image" - ) - } + NCUtility().createImageFileFrom(data: data, metadata: metadata) return .success } From 30988d8d72ca281bd543e27f0970b686de4e724c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jul 2026 12:02:03 +0200 Subject: [PATCH 7/7] Update NCManageDatabase.swift --- iOSClient/Data/NCManageDatabase.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index 567f4b43c5..8e2c57b477 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -207,6 +207,8 @@ final class NCManageDatabase: @unchecked Sendable { self.clearTable(tableExternalSites.self) self.clearTable(tableLivePhoto.self) self.clearTable(tableLocalFile.self) + self.clearTable(tableMediaMetadataBackfill.self) + self.clearTable(tableMediaPreviewBackfill.self) self.clearTable(tableMetadata.self) self.clearTable(tableMetadataTag.self) self.clearTable(tableRecommendedFiles.self)