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:
parent
1751c94891
commit
ffb8829f4f
|
@ -49,10 +49,7 @@ let package = Package(
|
|||
),
|
||||
.library(
|
||||
name: "TunnelLibrary",
|
||||
targets: [
|
||||
"AppDataPreferences",
|
||||
"CommonLibrary"
|
||||
]
|
||||
targets: ["CommonLibrary"]
|
||||
),
|
||||
.library(
|
||||
name: "UIAccessibility",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -34,8 +34,6 @@ extension BundleConfiguration {
|
|||
|
||||
case cloudKitId
|
||||
|
||||
case cloudKitPreferencesId
|
||||
|
||||
case userLevel
|
||||
|
||||
case groupId
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-pp_ui_testing"
|
||||
isEnabled = "YES">
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-pp_fake_migration"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue