//
// PersistenceManager.swift
// Passepartout
//
// Created by Davide De Rosa on 4/6/22.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see .
//
import CloudKit
import Combine
import CoreData
import Foundation
import PassepartoutLibrary
@MainActor
final class PersistenceManager: ObservableObject {
let store: KeyValueStore
private let ckContainerId: String
private let ckSharedContainerId: String
private let ckCoreDataZone: String
private var vpnPersistence: VPNPersistence?
private var sharedVPNPersistence: VPNPersistence?
private var providersPersistence: ProvidersPersistence?
private(set) var isCloudSyncingEnabled: Bool {
didSet {
pp_log.info("CloudKit enabled: \(isCloudSyncingEnabled)")
didChangePersistence.send()
}
}
@Published private(set) var isErasingCloudKitStore = false
let didChangePersistence = PassthroughSubject()
init(store: KeyValueStore,
ckContainerId: String,
ckSharedContainerId: String,
ckCoreDataZone: String) {
self.store = store
self.ckContainerId = ckContainerId
self.ckSharedContainerId = ckSharedContainerId
self.ckCoreDataZone = ckCoreDataZone
isCloudSyncingEnabled = store.canEnableCloudSyncing
// set once
if persistenceAuthor == nil {
persistenceAuthor = UUID().uuidString
}
}
func loadVPNPersistence(withName containerName: String) -> VPNPersistence {
let persistence = VPNPersistence(withName: containerName,
cloudKit: isCloudSyncingEnabled,
cloudKitIdentifier: nil,
author: persistenceAuthor)
vpnPersistence = persistence
return persistence
}
func loadSharedVPNPersistence(withName containerName: String) -> VPNPersistence {
let persistence = VPNPersistence(withName: containerName,
cloudKit: true,
cloudKitIdentifier: ckSharedContainerId,
author: persistenceAuthor)
sharedVPNPersistence = persistence
return persistence
}
func loadProvidersPersistence(withName containerName: String) -> ProvidersPersistence {
let persistence = ProvidersPersistence(withName: containerName, cloudKit: false, author: persistenceAuthor)
providersPersistence = persistence
return persistence
}
}
// MARK: CloudKit
extension PersistenceManager {
func eraseCloudKitStore() async {
isErasingCloudKitStore = true
await Self.eraseCloudKitStore(
fromContainerWithId: ckContainerId,
zoneId: .init(zoneName: ckCoreDataZone)
)
await Self.eraseCloudKitStore(
fromContainerWithId: ckSharedContainerId,
zoneId: .init(zoneName: ckCoreDataZone)
)
isErasingCloudKitStore = false
}
// WARNING: this is not running on main actor
private static func eraseCloudKitStore(fromContainerWithId containerId: String, zoneId: CKRecordZone.ID) async {
do {
let container = CKContainer(identifier: containerId)
let db = container.privateCloudDatabase
try await db.deleteRecordZone(withID: zoneId)
} catch {
pp_log.error("Unable to erase CloudKit store: \(error)")
}
}
}
// MARK: KeyValueStore
private extension KeyValueStore {
private var cloudKitToken: Any? {
FileManager.default.ubiquityIdentityToken
}
private var isCloudKitSupported: Bool {
#if !os(tvOS)
cloudKitToken != nil
#else
true
#endif
}
var canEnableCloudSyncing: Bool {
isCloudKitSupported && shouldEnableCloudSyncing
}
var shouldEnableCloudSyncing: Bool {
get {
value(forLocation: PersistenceManager.StoreKey.shouldEnableCloudSyncing) ?? false
}
set {
setValue(newValue, forLocation: PersistenceManager.StoreKey.shouldEnableCloudSyncing)
}
}
}
extension PersistenceManager {
private(set) var persistenceAuthor: String? {
get {
store.value(forLocation: StoreKey.persistenceAuthor)
}
set {
store.setValue(newValue, forLocation: StoreKey.persistenceAuthor)
}
}
var shouldEnableCloudSyncing: Bool {
get {
store.shouldEnableCloudSyncing
}
set {
objectWillChange.send()
store.shouldEnableCloudSyncing = newValue
// iCloud may be externally disabled from the device settings
let newIsCloudSyncingEnabled = store.canEnableCloudSyncing
guard newIsCloudSyncingEnabled != isCloudSyncingEnabled else {
pp_log.debug("CloudKit state did not change")
return
}
isCloudSyncingEnabled = newIsCloudSyncingEnabled
}
}
}
// TODO: iCloud, restore private after dropping migration from 2.2.0
// private extension PersistenceManager {
extension PersistenceManager {
enum StoreKey: String, KeyStoreDomainLocation {
case persistenceAuthor
case shouldEnableCloudSyncing
var domain: String {
"Passepartout.PersistenceManager"
}
}
}