|
|
|
|
@ -13,13 +13,19 @@ public class StoreCenter { |
|
|
|
|
/// The main instance |
|
|
|
|
public static let main: StoreCenter = StoreCenter() |
|
|
|
|
|
|
|
|
|
/// The name of the directory to store the json files |
|
|
|
|
let directoryName: String |
|
|
|
|
|
|
|
|
|
/// A dictionary of Stores associated to their id |
|
|
|
|
fileprivate var _stores: [String: Store] = [:] |
|
|
|
|
|
|
|
|
|
lazy var mainStore: Store = { Store(storeCenter: self) }() |
|
|
|
|
|
|
|
|
|
/// A KeychainStore object used to store the user's token |
|
|
|
|
var keychainStore: KeychainStore? = nil |
|
|
|
|
var tokenKeychain: KeychainService? = nil |
|
|
|
|
|
|
|
|
|
/// A KeychainStore object used to store the user's token |
|
|
|
|
var deviceKeychain: KeychainService = KeychainStore(serverId: "lestorage.device") |
|
|
|
|
|
|
|
|
|
/// Force the absence of synchronization |
|
|
|
|
public var forceNoSynchronization: Bool = false |
|
|
|
|
@ -55,10 +61,12 @@ public class StoreCenter { |
|
|
|
|
/// The URL manager |
|
|
|
|
fileprivate var _urlManager: URLManager? = nil |
|
|
|
|
|
|
|
|
|
/// Memory only alternate device id for testing purpose |
|
|
|
|
var alternateDeviceId: String? = nil |
|
|
|
|
var classProject: String? = nil |
|
|
|
|
|
|
|
|
|
init() { |
|
|
|
|
init(directoryName: String? = nil) { |
|
|
|
|
|
|
|
|
|
self.directoryName = directoryName ?? "storage" |
|
|
|
|
self._createDirectory() |
|
|
|
|
|
|
|
|
|
self._setupNotifications() |
|
|
|
|
|
|
|
|
|
@ -71,17 +79,17 @@ public class StoreCenter { |
|
|
|
|
// Logger.log("device Id = \(self.deviceId())") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func configureURLs(secureScheme: Bool, domain: String) { |
|
|
|
|
public func configureURLs(secureScheme: Bool, domain: String, webSockets: Bool = true) { |
|
|
|
|
let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain) |
|
|
|
|
self._urlManager = urlManager |
|
|
|
|
self._services = Services(storeCenter: self, url: urlManager.api) |
|
|
|
|
self.keychainStore = KeychainStore(serverId: urlManager.api) |
|
|
|
|
self.tokenKeychain = KeychainStore(serverId: urlManager.api) |
|
|
|
|
|
|
|
|
|
self._dataAccess = self.mainStore.registerSynchronizedCollection() |
|
|
|
|
|
|
|
|
|
Logger.log("Sync URL: \(urlManager.api)") |
|
|
|
|
|
|
|
|
|
if self.userId != nil { |
|
|
|
|
if webSockets && self.userId != nil { |
|
|
|
|
self._configureWebSocket() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -205,17 +213,17 @@ public class StoreCenter { |
|
|
|
|
/// Returns the stored token |
|
|
|
|
public func token() throws -> String { |
|
|
|
|
guard self.userName != nil else { throw StoreError.missingUsername } |
|
|
|
|
guard let keychainStore else { throw StoreError.missingKeychainStore } |
|
|
|
|
return try keychainStore.getValue() |
|
|
|
|
guard let tokenKeychain else { throw StoreError.missingKeychainStore } |
|
|
|
|
return try tokenKeychain.getValue() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func rawTokenShouldNotBeUsed() throws -> String? { |
|
|
|
|
return try self.keychainStore?.getValue() |
|
|
|
|
return try self.tokenKeychain?.getValue() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Disconnect the user from the storage and resets collection |
|
|
|
|
public func disconnect() { |
|
|
|
|
try? self.keychainStore?.deleteValue() |
|
|
|
|
try? self.tokenKeychain?.deleteValue() |
|
|
|
|
|
|
|
|
|
self.resetApiCalls() |
|
|
|
|
self._failedAPICallsCollection?.reset() |
|
|
|
|
@ -251,27 +259,23 @@ public class StoreCenter { |
|
|
|
|
/// - token: the token to store |
|
|
|
|
func storeToken(username: String, token: String) throws { |
|
|
|
|
self._settingsStorage.item.username = username |
|
|
|
|
guard let keychainStore else { throw StoreError.missingKeychainStore } |
|
|
|
|
try keychainStore.deleteValue() |
|
|
|
|
try keychainStore.add(username: username, value: token) |
|
|
|
|
guard let tokenKeychain else { throw StoreError.missingKeychainStore } |
|
|
|
|
try tokenKeychain.deleteValue() |
|
|
|
|
try tokenKeychain.add(username: username, value: token) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns a generated device id |
|
|
|
|
/// If created, stores it inside the keychain to get a consistent value even if the app is deleted |
|
|
|
|
/// as UIDevice.current.identifierForVendor value changes when the app is deleted and installed again |
|
|
|
|
func deviceId() -> String { |
|
|
|
|
if let alternateDeviceId { |
|
|
|
|
return alternateDeviceId |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let keychainStore = KeychainStore(serverId: "lestorage.main") |
|
|
|
|
do { |
|
|
|
|
return try keychainStore.getValue() |
|
|
|
|
return try self.deviceKeychain.getValue() |
|
|
|
|
} catch { |
|
|
|
|
let deviceId: String = |
|
|
|
|
UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString |
|
|
|
|
do { |
|
|
|
|
try keychainStore.add(value: deviceId) |
|
|
|
|
try self.deviceKeychain.add(value: deviceId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
@ -279,6 +283,32 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - File system |
|
|
|
|
|
|
|
|
|
/// Creates the store directory |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - directory: the name of the directory |
|
|
|
|
fileprivate func _createDirectory() { |
|
|
|
|
FileManager.default.createDirectoryInDocuments(directoryName: self.directoryName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func directoryURL() throws -> URL { |
|
|
|
|
return try FileUtils.pathForDirectoryInDocuments(directory: self.directoryName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the URL of the Storable json file |
|
|
|
|
func jsonFileURL<T: Storable>(for type: T.Type) throws -> URL { |
|
|
|
|
var storageDirectory = try self.directoryURL() |
|
|
|
|
storageDirectory.append(component: T.fileName()) |
|
|
|
|
return storageDirectory |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func write(content: String, fileName: String) throws { |
|
|
|
|
var fileURL = try self.directoryURL() |
|
|
|
|
fileURL.append(component: fileName) |
|
|
|
|
try content.write(to: fileURL, atomically: false, encoding: .utf8) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Api Calls management |
|
|
|
|
|
|
|
|
|
/// Instantiates and loads an ApiCallCollection with the provided type |
|
|
|
|
@ -350,8 +380,11 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
Task { |
|
|
|
|
do { |
|
|
|
|
try FileManager.default.removeItem(at: Log.urlForJSONFile()) |
|
|
|
|
try FileManager.default.removeItem(at: FailedAPICall.urlForJSONFile()) |
|
|
|
|
let logURL = try self.jsonFileURL(for: Log.self) |
|
|
|
|
try FileManager.default.removeItem(at: logURL) |
|
|
|
|
|
|
|
|
|
let facURL = try self.jsonFileURL(for: FailedAPICall.self) |
|
|
|
|
try FileManager.default.removeItem(at: facURL) |
|
|
|
|
|
|
|
|
|
let facApiCallCollection: ApiCallCollection<FailedAPICall> = try self.apiCallCollection() |
|
|
|
|
await facApiCallCollection.reset() |
|
|
|
|
@ -530,6 +563,18 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func testSynchronizeOnceAsync() async throws { |
|
|
|
|
guard self.isAuthenticated else { |
|
|
|
|
throw StoreError.missingToken |
|
|
|
|
} |
|
|
|
|
let lastSync = self._settingsStorage.item.lastSynchronization |
|
|
|
|
let syncGetCollection: ApiCallCollection<GetSyncData> = try self.apiCallCollection() |
|
|
|
|
|
|
|
|
|
let getSyncData = GetSyncData() |
|
|
|
|
getSyncData.date = lastSync |
|
|
|
|
await syncGetCollection.executeSingleGet(instance: getSyncData) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func sendGetRequest<T: SyncedStorable>(_ type: T.Type, storeId: String?, clear: Bool) async throws { |
|
|
|
|
guard self._canSynchronise(), self.canPerformGet(T.self) else { |
|
|
|
|
return |
|
|
|
|
@ -635,7 +680,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
Logger.log(">>> UPDATE \(updateArray.count) \(className)") |
|
|
|
|
|
|
|
|
|
let type = try StoreCenter.classFromName(className) |
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
|
|
|
|
|
for updateItem in updateArray { |
|
|
|
|
|
|
|
|
|
@ -723,8 +768,8 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns a Type object for a class name |
|
|
|
|
static func classFromName(_ className: String) throws -> any SyncedStorable.Type { |
|
|
|
|
if let type = ClassLoader.getClass(className) { |
|
|
|
|
func classFromName(_ className: String) throws -> any SyncedStorable.Type { |
|
|
|
|
if let type = ClassLoader.getClass(className, classProject: self.classProject) { |
|
|
|
|
if let syncedType = type as? any SyncedStorable.Type { |
|
|
|
|
return syncedType |
|
|
|
|
} else { |
|
|
|
|
@ -773,7 +818,7 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
do { |
|
|
|
|
let type = try StoreCenter.classFromName(model) |
|
|
|
|
let type = try self.classFromName(model) |
|
|
|
|
try self._store(id: storeId).deleteNoSync(type: type, id: id) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
@ -787,7 +832,7 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
do { |
|
|
|
|
let type = try StoreCenter.classFromName(model) |
|
|
|
|
let type = try self.classFromName(model) |
|
|
|
|
if self._instanceShared(id: id, type: type) { |
|
|
|
|
let count = self.mainStore.referenceCount(type: type, id: id) |
|
|
|
|
if count == 0 { |
|
|
|
|
@ -948,7 +993,7 @@ public class StoreCenter { |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - identifier: The name of the directory |
|
|
|
|
public func destroyStore(identifier: String) { |
|
|
|
|
let directory = "\(Store.storageDirectory)/\(identifier)" |
|
|
|
|
let directory = "\(self.directoryName)/\(identifier)" |
|
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: directory) |
|
|
|
|
self._stores.removeValue(forKey: identifier) |
|
|
|
|
} |
|
|
|
|
|