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
This commit is contained in:
Davide 2024-12-15 20:20:33 +01:00 committed by GitHub
parent 1751c94891
commit ffb8829f4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 107 additions and 77 deletions

View File

@ -49,10 +49,7 @@ let package = Package(
),
.library(
name: "TunnelLibrary",
targets: [
"AppDataPreferences",
"CommonLibrary"
]
targets: ["CommonLibrary"]
),
.library(
name: "UIAccessibility",

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -34,8 +34,6 @@ extension BundleConfiguration {
case cloudKitId
case cloudKitPreferencesId
case userLevel
case groupId

View File

@ -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

View File

@ -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"

View File

@ -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<Void, Error>?
@ -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)

View File

@ -85,7 +85,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "-pp_ui_testing"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-pp_fake_migration"

View File

@ -9,7 +9,6 @@
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>$(CFG_CLOUDKIT_ID)</string>
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
<string>$(CFG_LEGACY_V2_CLOUDKIT_ID)</string>
<string>$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)</string>
</array>

View File

@ -8,8 +8,6 @@
<string>$(CFG_APP_STORE_ID)</string>
<key>cloudKitId</key>
<string>$(CFG_CLOUDKIT_ID)</string>
<key>cloudKitPreferencesId</key>
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
<key>groupId</key>
<string>$(CFG_GROUP_ID)</string>
<key>iapBundlePrefix</key>

View File

@ -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
)

View File

@ -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

View File

@ -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
}
}

View File

@ -10,8 +10,6 @@
<string>$(CFG_KEYCHAIN_GROUP_ID)</string>
<key>tunnelId</key>
<string>$(CFG_TUNNEL_ID)</string>
<key>cloudKitPreferencesId</key>
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
</dict>
<key>NSExtension</key>
<dict>