From ffb8829f4f9f8e2022f7631d6f30c101d9436ba1 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 15 Dec 2024 20:20:33 +0100 Subject: [PATCH] Reorganize Core Data containers (#1017) Before anything, remove any code related to App Group containers from tvOS target because they are not available. Include the beta receipt override, it's broken for that reason. In short: - Store all Core Data containers locally. Do not use the App Group for Core Data for consistency across platforms. - Store logs in the App Group on iOS/macOS, but locally on tvOS (see `urlForCaches`). Then, rather than one container per model, merge models into: - Local: Providers - Remote: Profiles + Preferences (now in the same CloudKit container) Reuse the remote model for backups too. This change is safe because: - Local profiles are stored via Network Extension in the keychain, not Core Data - Remote profiles are re-imported via CloudKit sync - Providers are re-downloaded on first use - Preferences are lost, but they are "cheap" data - Profile backups are lost, but they were hidden anyway --- Library/Package.swift | 5 +- .../AppData+Preferences.swift | 8 +-- .../AppDataProfiles/AppData+Profiles.swift | 10 +--- .../AppDataProviders/AppData+Providers.swift | 10 +--- .../Domain/BundleConfiguration+AppGroup.swift | 56 ++++++++++++++++--- .../Domain/BundleConfiguration+Main.swift | 2 - .../CommonLibrary/Domain/Constants.swift | 8 +-- .../CommonLibrary/Resources/Constants.json | 6 +- .../UILibrary/Business/AppContext.swift | 7 ++- .../xcschemes/Passepartout.xcscheme | 2 +- Passepartout/App/App.entitlements | 1 - Passepartout/App/App.plist | 2 - .../App/Context/AppContext+Shared.swift | 54 ++++++++++++------ Passepartout/Config.xcconfig | 1 - .../Tunnel/Context/TunnelContext+Shared.swift | 10 +++- Passepartout/Tunnel/Tunnel.plist | 2 - 16 files changed, 107 insertions(+), 77 deletions(-) diff --git a/Library/Package.swift b/Library/Package.swift index c002b782..8bafa80a 100644 --- a/Library/Package.swift +++ b/Library/Package.swift @@ -49,10 +49,7 @@ let package = Package( ), .library( name: "TunnelLibrary", - targets: [ - "AppDataPreferences", - "CommonLibrary" - ] + targets: ["CommonLibrary"] ), .library( name: "UIAccessibility", diff --git a/Library/Sources/AppDataPreferences/AppData+Preferences.swift b/Library/Sources/AppDataPreferences/AppData+Preferences.swift index a772a792..c00ba345 100644 --- a/Library/Sources/AppDataPreferences/AppData+Preferences.swift +++ b/Library/Sources/AppDataPreferences/AppData+Preferences.swift @@ -24,14 +24,8 @@ // import AppData -import CoreData import Foundation extension AppData { - public static var cdPreferencesModel: NSManagedObjectModel { - guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { - fatalError("Unable to build Core Data model (Preferences v3)") - } - return model - } + public static let preferencesBundle: Bundle = .module } diff --git a/Library/Sources/AppDataProfiles/AppData+Profiles.swift b/Library/Sources/AppDataProfiles/AppData+Profiles.swift index 3977f8e8..3a84eea5 100644 --- a/Library/Sources/AppDataProfiles/AppData+Profiles.swift +++ b/Library/Sources/AppDataProfiles/AppData+Profiles.swift @@ -24,16 +24,8 @@ // import AppData -import CoreData import Foundation extension AppData { - - @MainActor - public static let cdProfilesModel: NSManagedObjectModel = { - guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { - fatalError("Unable to build Core Data model (Profiles v3)") - } - return model - }() + public static let profilesBundle: Bundle = .module } diff --git a/Library/Sources/AppDataProviders/AppData+Providers.swift b/Library/Sources/AppDataProviders/AppData+Providers.swift index 92079692..c5be47a3 100644 --- a/Library/Sources/AppDataProviders/AppData+Providers.swift +++ b/Library/Sources/AppDataProviders/AppData+Providers.swift @@ -24,16 +24,8 @@ // import AppData -import CoreData import Foundation extension AppData { - - @MainActor - public static let cdProvidersModel: NSManagedObjectModel = { - guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { - fatalError("Unable to build Core Data model (Providers v3)") - } - return model - }() + public static let providersBundle: Bundle = .module } diff --git a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift index eb1f6ab8..ea883682 100644 --- a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift +++ b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift @@ -30,28 +30,44 @@ import PassepartoutKit extension BundleConfiguration { public static var urlForAppLog: URL { - urlForGroupCaches.appending(path: Constants.shared.log.appPath) + urlForCaches.appending(path: Constants.shared.log.appPath) } public static var urlForTunnelLog: URL { - urlForGroupCaches.appending(path: Constants.shared.log.tunnelPath) + urlForCaches.appending(path: Constants.shared.log.tunnelPath) } - public static var urlForBetaReceipt: URL { - urlForGroupCaches.appending(path: Constants.shared.tunnel.betaReceiptPath) + public static var urlForBetaReceipt: URL? { +#if os(iOS) + urlForCaches.appending(path: Constants.shared.tunnel.betaReceiptPath) +#else + nil +#endif } } +// App Group container is not available on tvOS (#1007) + +#if !os(tvOS) + extension BundleConfiguration { - public static var urlForGroupCaches: URL { + public static var urlForCaches: URL { let url = appGroupURL.appending(components: "Library", "Caches") - try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + } catch { + pp_log(.app, .fault, "Unable to create group caches directory: \(error)") + } return url } - public static var urlForGroupDocuments: URL { + public static var urlForDocuments: URL { let url = appGroupURL.appending(components: "Library", "Documents") - try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + } catch { + pp_log(.app, .fault, "Unable to create group documents directory: \(error)") + } return url } } @@ -66,3 +82,27 @@ private extension BundleConfiguration { return url } } + +#else + +extension BundleConfiguration { + public static var urlForCaches: URL { + do { + return try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + } catch { + pp_log(.app, .fault, "Unable to create user documents directory: \(error)") + return URL(fileURLWithPath: NSTemporaryDirectory()) + } + } + + public static var urlForDocuments: URL { + do { + return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + } catch { + pp_log(.app, .fault, "Unable to create user documents directory: \(error)") + return URL(fileURLWithPath: NSTemporaryDirectory()) + } + } +} + +#endif diff --git a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift index 9a9272e3..d49150e4 100644 --- a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift +++ b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift @@ -34,8 +34,6 @@ extension BundleConfiguration { case cloudKitId - case cloudKitPreferencesId - case userLevel case groupId diff --git a/Library/Sources/CommonLibrary/Domain/Constants.swift b/Library/Sources/CommonLibrary/Domain/Constants.swift index 83319b8e..c203ab58 100644 --- a/Library/Sources/CommonLibrary/Domain/Constants.swift +++ b/Library/Sources/CommonLibrary/Domain/Constants.swift @@ -28,13 +28,11 @@ import PassepartoutKit public struct Constants: Decodable, Sendable { public struct Containers: Decodable, Sendable { - public let localProfiles: String + public let local: String - public let remoteProfiles: String + public let remote: String - public let providers: String - - public let preferences: String + public let backup: String public let legacyV2: String diff --git a/Library/Sources/CommonLibrary/Resources/Constants.json b/Library/Sources/CommonLibrary/Resources/Constants.json index 677fea5c..23d6f6c7 100644 --- a/Library/Sources/CommonLibrary/Resources/Constants.json +++ b/Library/Sources/CommonLibrary/Resources/Constants.json @@ -1,9 +1,9 @@ { "bundleKey": "AppConfig", "containers": { - "localProfiles": "Profiles-v3", - "remoteProfiles": "Profiles-v3.remote", - "providers": "Providers-v3", + "local": "Local", + "remote": "Remote", + "backup": "Backup", "preferences": "Preferences-v3", "legacyV2": "Profiles", "legacyV2TV": "SharedProfiles" diff --git a/Library/Sources/UILibrary/Business/AppContext.swift b/Library/Sources/UILibrary/Business/AppContext.swift index 4617a04f..bb45007d 100644 --- a/Library/Sources/UILibrary/Business/AppContext.swift +++ b/Library/Sources/UILibrary/Business/AppContext.swift @@ -46,7 +46,7 @@ public final class AppContext: ObservableObject, Sendable { public let tunnel: ExtendedTunnel - private let tunnelReceiptURL: URL + private let tunnelReceiptURL: URL? private var launchTask: Task? @@ -62,7 +62,7 @@ public final class AppContext: ObservableObject, Sendable { preferencesManager: PreferencesManager, registry: Registry, tunnel: ExtendedTunnel, - tunnelReceiptURL: URL + tunnelReceiptURL: URL? ) { self.iapManager = iapManager self.migrationManager = migrationManager @@ -128,7 +128,8 @@ private extension AppContext { .store(in: &subscriptions) // copy release receipt to tunnel for TestFlight eligibility (once is enough, it won't change) - if let appReceiptURL = Bundle.main.appStoreProductionReceiptURL { + if let tunnelReceiptURL, + let appReceiptURL = Bundle.main.appStoreProductionReceiptURL { do { pp_log(.App.iap, .info, "\tCopy release receipt to tunnel...") try? FileManager.default.removeItem(at: tunnelReceiptURL) diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme index 8e21e845..946e30cb 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme @@ -85,7 +85,7 @@ + isEnabled = "NO"> com.apple.developer.icloud-container-identifiers $(CFG_CLOUDKIT_ID) - $(CFG_CLOUDKIT_PREFERENCES_ID) $(CFG_LEGACY_V2_CLOUDKIT_ID) $(CFG_LEGACY_V2_TV_CLOUDKIT_ID) diff --git a/Passepartout/App/App.plist b/Passepartout/App/App.plist index 35ccd315..b2e85be7 100644 --- a/Passepartout/App/App.plist +++ b/Passepartout/App/App.plist @@ -8,8 +8,6 @@ $(CFG_APP_STORE_ID) cloudKitId $(CFG_CLOUDKIT_ID) - cloudKitPreferencesId - $(CFG_CLOUDKIT_PREFERENCES_ID) groupId $(CFG_GROUP_ID) iapBundlePrefix diff --git a/Passepartout/App/Context/AppContext+Shared.swift b/Passepartout/App/Context/AppContext+Shared.swift index a634399c..b98e9785 100644 --- a/Passepartout/App/Context/AppContext+Shared.swift +++ b/Passepartout/App/Context/AppContext+Shared.swift @@ -29,6 +29,7 @@ import AppDataProfiles import AppDataProviders import CommonLibrary import CommonUtils +import CoreData import Foundation import LegacyV2 import PassepartoutKit @@ -39,6 +40,18 @@ extension AppContext { static let shared: AppContext = { let dependencies: Dependencies = .shared + guard let cdRemoteModel = NSManagedObjectModel.mergedModel(from: [ + AppData.profilesBundle, + AppData.preferencesBundle + ]) else { + fatalError("Unable to load remote model") + } + guard let cdLocalModel = NSManagedObjectModel.mergedModel(from: [ + AppData.providersBundle + ]) else { + fatalError("Unable to load local model") + } + let iapManager = IAPManager( customUserLevel: dependencies.customUserLevel, inAppHelper: dependencies.simulatedAppProductHelper(), @@ -54,8 +67,8 @@ extension AppContext { let remoteRepositoryBlock: (Bool) -> ProfileRepository = { let remoteStore = CoreDataPersistentStore( logger: dependencies.coreDataLogger(), - containerName: Constants.shared.containers.remoteProfiles, - model: AppData.cdProfilesModel, + containerName: Constants.shared.containers.remote, + model: cdRemoteModel, cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil, author: nil ) @@ -71,8 +84,13 @@ extension AppContext { ) } return ProfileManager( - repository: dependencies.mainProfileRepository(environment: tunnelEnvironment), - backupRepository: dependencies.backupProfileRepository(), + repository: dependencies.mainProfileRepository( + model: cdRemoteModel, + environment: tunnelEnvironment + ), + backupRepository: dependencies.backupProfileRepository( + model: cdRemoteModel + ), remoteRepositoryBlock: remoteRepositoryBlock, mirrorsRemoteRepository: dependencies.mirrorsRemoteRepository, processor: processor @@ -89,8 +107,8 @@ extension AppContext { let providerManager: ProviderManager = { let store = CoreDataPersistentStore( logger: dependencies.coreDataLogger(), - containerName: Constants.shared.containers.providers, - model: AppData.cdProvidersModel, + containerName: Constants.shared.containers.local, + model: cdLocalModel, cloudKitIdentifier: nil, author: nil ) @@ -126,9 +144,9 @@ extension AppContext { let preferencesManager: PreferencesManager = { let preferencesStore = CoreDataPersistentStore( logger: dependencies.coreDataLogger(), - containerName: Constants.shared.containers.preferences, - model: AppData.cdPreferencesModel, - cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId), + containerName: Constants.shared.containers.remote, + model: cdRemoteModel, + cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitId), author: nil ) return PreferencesManager( @@ -207,11 +225,11 @@ private extension Dependencies { FakeTunnelStrategy(environment: environment, dataCountInterval: 1000) } - func mainProfileRepository(environment: TunnelEnvironment) -> ProfileRepository { - coreDataProfileRepository(observingResults: true) + func mainProfileRepository(model: NSManagedObjectModel, environment: TunnelEnvironment) -> ProfileRepository { + coreDataProfileRepository(model: model, observingResults: true) } - func backupProfileRepository() -> ProfileRepository? { + func backupProfileRepository(model: NSManagedObjectModel) -> ProfileRepository? { nil } } @@ -225,12 +243,12 @@ private extension Dependencies { neStrategy(environment: environment) } - func mainProfileRepository(environment: TunnelEnvironment) -> ProfileRepository { + func mainProfileRepository(model: NSManagedObjectModel, environment: TunnelEnvironment) -> ProfileRepository { neProfileRepository(environment: environment) } - func backupProfileRepository() -> ProfileRepository? { - coreDataProfileRepository(observingResults: false) + func backupProfileRepository(model: NSManagedObjectModel) -> ProfileRepository? { + coreDataProfileRepository(model: model, observingResults: false) } } @@ -261,11 +279,11 @@ private extension Dependencies { ) } - func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository { + func coreDataProfileRepository(model: NSManagedObjectModel, observingResults: Bool) -> ProfileRepository { let store = CoreDataPersistentStore( logger: coreDataLogger(), - containerName: Constants.shared.containers.localProfiles, - model: AppData.cdProfilesModel, + containerName: Constants.shared.containers.backup, + model: model, cloudKitIdentifier: nil, author: nil ) diff --git a/Passepartout/Config.xcconfig b/Passepartout/Config.xcconfig index 96ea0697..756d6c2c 100644 --- a/Passepartout/Config.xcconfig +++ b/Passepartout/Config.xcconfig @@ -47,7 +47,6 @@ CFG_LOGIN_ITEM_ID = $(CFG_APP_ID).LoginItem CFG_TUNNEL_ID = $(CFG_APP_ID).Tunnel CFG_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT).v3 -CFG_CLOUDKIT_PREFERENCES_ID = $(CFG_CLOUDKIT_ROOT).v3.Preferences CFG_LEGACY_V2_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT) CFG_LEGACY_V2_TV_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT).Shared diff --git a/Passepartout/Tunnel/Context/TunnelContext+Shared.swift b/Passepartout/Tunnel/Context/TunnelContext+Shared.swift index fada055d..5edd4b53 100644 --- a/Passepartout/Tunnel/Context/TunnelContext+Shared.swift +++ b/Passepartout/Tunnel/Context/TunnelContext+Shared.swift @@ -51,11 +51,17 @@ private extension Dependencies { func tunnelReceiptReader() -> AppReceiptReader { FallbackReceiptReader( main: StoreKitReceiptReader(), - beta: KvittoReceiptReader(url: betaReceiptURL) + beta: betaReceiptURL.map { + KvittoReceiptReader(url: $0) + } ) } - var betaReceiptURL: URL { + var betaReceiptURL: URL? { +#if !os(tvOS) BundleConfiguration.urlForBetaReceipt // copied by AppContext.onLaunch +#else + nil +#endif } } diff --git a/Passepartout/Tunnel/Tunnel.plist b/Passepartout/Tunnel/Tunnel.plist index 22a65386..db76be5d 100644 --- a/Passepartout/Tunnel/Tunnel.plist +++ b/Passepartout/Tunnel/Tunnel.plist @@ -10,8 +10,6 @@ $(CFG_KEYCHAIN_GROUP_ID) tunnelId $(CFG_TUNNEL_ID) - cloudKitPreferencesId - $(CFG_CLOUDKIT_PREFERENCES_ID) NSExtension