2020-03-22 22:35:03 +01:00
|
|
|
//
|
|
|
|
// CloudKitAccountZone.swift
|
|
|
|
// Account
|
|
|
|
//
|
|
|
|
// Created by Maurice Parker on 3/21/20.
|
|
|
|
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2020-03-30 00:12:34 +02:00
|
|
|
import os.log
|
|
|
|
import RSWeb
|
2020-04-01 01:10:35 +02:00
|
|
|
import RSParser
|
2020-03-22 22:35:03 +01:00
|
|
|
import CloudKit
|
|
|
|
|
2020-05-10 23:59:23 +02:00
|
|
|
enum CloudKitAccountZoneError: LocalizedError {
|
|
|
|
case unknown
|
|
|
|
var errorDescription: String? {
|
|
|
|
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
|
|
|
|
}
|
|
|
|
}
|
2020-03-22 22:35:03 +01:00
|
|
|
final class CloudKitAccountZone: CloudKitZone {
|
|
|
|
|
|
|
|
static var zoneID: CKRecordZone.ID {
|
|
|
|
return CKRecordZone.ID(zoneName: "Account", ownerName: CKCurrentUserDefaultName)
|
|
|
|
}
|
|
|
|
|
2020-03-30 00:12:34 +02:00
|
|
|
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
|
|
|
|
|
|
|
weak var container: CKContainer?
|
|
|
|
weak var database: CKDatabase?
|
2020-04-01 15:00:24 +02:00
|
|
|
var delegate: CloudKitZoneDelegate?
|
2020-03-22 22:35:03 +01:00
|
|
|
|
2020-03-28 14:53:03 +01:00
|
|
|
struct CloudKitWebFeed {
|
2020-04-04 22:04:38 +02:00
|
|
|
static let recordType = "AccountWebFeed"
|
2020-03-28 14:53:03 +01:00
|
|
|
struct Fields {
|
|
|
|
static let url = "url"
|
2020-04-26 18:26:41 +02:00
|
|
|
static let name = "name"
|
2020-03-28 14:53:03 +01:00
|
|
|
static let editedName = "editedName"
|
2020-05-06 01:25:33 +02:00
|
|
|
static let homePageURL = "homePageURL"
|
2020-04-01 01:10:35 +02:00
|
|
|
static let containerExternalIDs = "containerExternalIDs"
|
2020-03-28 14:53:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-30 22:15:45 +02:00
|
|
|
struct CloudKitContainer {
|
2020-04-04 22:04:38 +02:00
|
|
|
static let recordType = "AccountContainer"
|
2020-03-30 22:15:45 +02:00
|
|
|
struct Fields {
|
|
|
|
static let isAccount = "isAccount"
|
|
|
|
static let name = "name"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-30 00:12:34 +02:00
|
|
|
init(container: CKContainer) {
|
2020-03-22 22:35:03 +01:00
|
|
|
self.container = container
|
|
|
|
self.database = container.privateCloudDatabase
|
|
|
|
}
|
2020-04-01 01:10:35 +02:00
|
|
|
|
|
|
|
func importOPML(rootExternalID: String, items: [RSOPMLItem], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
var records = [CKRecord]()
|
|
|
|
var feedRecords = [String: CKRecord]()
|
|
|
|
|
|
|
|
func processFeed(feedSpecifier: RSOPMLFeedSpecifier, containerExternalID: String) {
|
|
|
|
if let webFeedRecord = feedRecords[feedSpecifier.feedURL], var containerExternalIDs = webFeedRecord[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
|
|
|
containerExternalIDs.append(containerExternalID)
|
|
|
|
webFeedRecord[CloudKitWebFeed.Fields.containerExternalIDs] = containerExternalIDs
|
|
|
|
} else {
|
|
|
|
let webFeedRecord = newWebFeedCKRecord(feedSpecifier: feedSpecifier, containerExternalID: containerExternalID)
|
|
|
|
records.append(webFeedRecord)
|
|
|
|
feedRecords[feedSpecifier.feedURL] = webFeedRecord
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for item in items {
|
|
|
|
if let feedSpecifier = item.feedSpecifier {
|
|
|
|
processFeed(feedSpecifier: feedSpecifier, containerExternalID: rootExternalID)
|
|
|
|
} else {
|
|
|
|
if let title = item.titleFromAttributes {
|
|
|
|
let containerRecord = newContainerCKRecord(name: title)
|
|
|
|
records.append(containerRecord)
|
|
|
|
item.children?.forEach { itemChild in
|
|
|
|
if let feedSpecifier = itemChild.feedSpecifier {
|
|
|
|
processFeed(feedSpecifier: feedSpecifier, containerExternalID: containerRecord.externalID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:04:38 +02:00
|
|
|
save(records, completion: completion)
|
2020-04-01 01:10:35 +02:00
|
|
|
}
|
2020-03-22 22:35:03 +01:00
|
|
|
|
2020-03-29 15:52:59 +02:00
|
|
|
/// Persist a web feed record to iCloud and return the external key
|
2020-05-06 01:25:33 +02:00
|
|
|
func createWebFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
|
2020-04-04 20:36:54 +02:00
|
|
|
let recordID = CKRecord.ID(recordName: url.md5String, zoneID: Self.zoneID)
|
|
|
|
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: recordID)
|
2020-03-31 09:20:47 +02:00
|
|
|
record[CloudKitWebFeed.Fields.url] = url
|
2020-04-26 18:26:41 +02:00
|
|
|
record[CloudKitWebFeed.Fields.name] = name
|
2020-03-27 19:59:42 +01:00
|
|
|
if let editedName = editedName {
|
2020-03-31 09:20:47 +02:00
|
|
|
record[CloudKitWebFeed.Fields.editedName] = editedName
|
2020-03-27 19:59:42 +01:00
|
|
|
}
|
2020-05-06 01:25:33 +02:00
|
|
|
if let homePageURL = homePageURL {
|
|
|
|
record[CloudKitWebFeed.Fields.homePageURL] = homePageURL
|
|
|
|
}
|
|
|
|
|
2020-03-31 04:11:57 +02:00
|
|
|
guard let containerExternalID = container.externalID else {
|
|
|
|
completion(.failure(CloudKitZoneError.invalidParameter))
|
|
|
|
return
|
|
|
|
}
|
2020-03-31 09:20:47 +02:00
|
|
|
record[CloudKitWebFeed.Fields.containerExternalIDs] = [containerExternalID]
|
2020-03-31 04:11:57 +02:00
|
|
|
|
2020-03-31 09:20:47 +02:00
|
|
|
save(record) { result in
|
2020-03-29 15:52:59 +02:00
|
|
|
switch result {
|
|
|
|
case .success:
|
2020-03-31 09:20:47 +02:00
|
|
|
completion(.success(record.externalID))
|
2020-03-29 15:52:59 +02:00
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
2020-03-27 19:59:42 +01:00
|
|
|
}
|
2020-03-22 22:35:03 +01:00
|
|
|
|
2020-03-31 04:11:57 +02:00
|
|
|
/// Rename the given web feed
|
2020-03-30 00:53:11 +02:00
|
|
|
func renameWebFeed(_ webFeed: WebFeed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
guard let externalID = webFeed.externalID else {
|
|
|
|
completion(.failure(CloudKitZoneError.invalidParameter))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
|
|
|
|
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: recordID)
|
|
|
|
record[CloudKitWebFeed.Fields.editedName] = editedName
|
|
|
|
|
2020-03-31 09:20:47 +02:00
|
|
|
save(record) { result in
|
2020-03-30 00:53:11 +02:00
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
completion(.success(()))
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 12:02:33 +02:00
|
|
|
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
|
|
|
|
func removeWebFeed(_ webFeed: WebFeed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
2020-03-31 10:30:53 +02:00
|
|
|
guard let fromContainerExternalID = from.externalID else {
|
|
|
|
completion(.failure(CloudKitZoneError.invalidParameter))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fetch(externalID: webFeed.externalID) { result in
|
|
|
|
switch result {
|
|
|
|
case .success(let record):
|
2020-04-04 12:02:33 +02:00
|
|
|
|
2020-03-31 10:30:53 +02:00
|
|
|
if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
|
|
|
var containerExternalIDSet = Set(containerExternalIDs)
|
|
|
|
containerExternalIDSet.remove(fromContainerExternalID)
|
2020-04-04 12:02:33 +02:00
|
|
|
|
2020-03-31 10:30:53 +02:00
|
|
|
if containerExternalIDSet.isEmpty {
|
2020-04-04 12:02:33 +02:00
|
|
|
self.delete(externalID: webFeed.externalID) { result in
|
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
completion(.success(true))
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 10:30:53 +02:00
|
|
|
} else {
|
2020-04-04 12:02:33 +02:00
|
|
|
|
2020-03-31 10:30:53 +02:00
|
|
|
record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
2020-04-04 12:02:33 +02:00
|
|
|
self.save(record) { result in
|
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
completion(.success(false))
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 10:30:53 +02:00
|
|
|
}
|
|
|
|
}
|
2020-04-04 12:02:33 +02:00
|
|
|
|
2020-03-31 10:30:53 +02:00
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
|
|
|
completion(.failure(CloudKitZoneError.invalidParameter))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fetch(externalID: webFeed.externalID) { result in
|
|
|
|
switch result {
|
|
|
|
case .success(let record):
|
|
|
|
if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
|
|
|
var containerExternalIDSet = Set(containerExternalIDs)
|
|
|
|
containerExternalIDSet.remove(fromContainerExternalID)
|
|
|
|
containerExternalIDSet.insert(toContainerExternalID)
|
|
|
|
record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
|
|
|
self.save(record, completion: completion)
|
|
|
|
}
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func addWebFeed(_ webFeed: WebFeed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
guard let toContainerExternalID = to.externalID else {
|
|
|
|
completion(.failure(CloudKitZoneError.invalidParameter))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fetch(externalID: webFeed.externalID) { result in
|
|
|
|
switch result {
|
|
|
|
case .success(let record):
|
|
|
|
if let containerExternalIDs = record[CloudKitWebFeed.Fields.containerExternalIDs] as? [String] {
|
|
|
|
var containerExternalIDSet = Set(containerExternalIDs)
|
|
|
|
containerExternalIDSet.insert(toContainerExternalID)
|
|
|
|
record[CloudKitWebFeed.Fields.containerExternalIDs] = Array(containerExternalIDSet)
|
|
|
|
self.save(record, completion: completion)
|
|
|
|
}
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
2020-03-30 22:15:45 +02:00
|
|
|
}
|
|
|
|
|
2020-05-10 23:59:23 +02:00
|
|
|
func findWebFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
|
|
|
|
guard let folderExternalID = folder.externalID else {
|
|
|
|
completion(.failure(CloudKitAccountZoneError.unknown))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let predicate = NSPredicate(format: "containerExternalIDs CONTAINS %@", folderExternalID)
|
|
|
|
let ckQuery = CKQuery(recordType: CloudKitWebFeed.recordType, predicate: predicate)
|
|
|
|
|
|
|
|
query(ckQuery) { result in
|
|
|
|
switch result {
|
|
|
|
case .success(let records):
|
|
|
|
let webFeedExternalIds = records.map { $0.externalID }
|
|
|
|
completion(.success(webFeedExternalIds))
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-30 22:15:45 +02:00
|
|
|
func findOrCreateAccount(completion: @escaping (Result<String, Error>) -> Void) {
|
2020-04-01 03:42:39 +02:00
|
|
|
let predicate = NSPredicate(format: "isAccount = \"1\"")
|
2020-03-30 22:15:45 +02:00
|
|
|
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
|
|
|
|
|
2020-04-06 09:15:28 +02:00
|
|
|
database?.perform(ckQuery, inZoneWith: Self.zoneID) { [weak self] records, error in
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
switch CloudKitZoneResult.resolve(error) {
|
|
|
|
case .success:
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
if records!.count > 0 {
|
|
|
|
completion(.success(records![0].externalID))
|
|
|
|
} else {
|
|
|
|
self.createContainer(name: "Account", isAccount: true, completion: completion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case .retry(let timeToWait):
|
|
|
|
self.retryIfPossible(after: timeToWait) {
|
|
|
|
self.findOrCreateAccount(completion: completion)
|
|
|
|
}
|
|
|
|
case .zoneNotFound, .userDeletedZone:
|
|
|
|
self.createZoneRecord() { result in
|
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
self.findOrCreateAccount(completion: completion)
|
|
|
|
case .failure(let error):
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
completion(.failure(CloudKitError(error)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
2020-03-30 22:15:45 +02:00
|
|
|
self.createContainer(name: "Account", isAccount: true, completion: completion)
|
|
|
|
}
|
|
|
|
}
|
2020-04-12 22:57:00 +02:00
|
|
|
|
2020-03-30 22:15:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func createFolder(name: String, completion: @escaping (Result<String, Error>) -> Void) {
|
|
|
|
createContainer(name: name, isAccount: false, completion: completion)
|
|
|
|
}
|
|
|
|
|
|
|
|
func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
guard let externalID = folder.externalID else {
|
2020-03-29 15:52:59 +02:00
|
|
|
completion(.failure(CloudKitZoneError.invalidParameter))
|
|
|
|
return
|
|
|
|
}
|
2020-03-30 22:15:45 +02:00
|
|
|
|
|
|
|
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
|
|
|
|
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: recordID)
|
|
|
|
record[CloudKitContainer.Fields.name] = name
|
|
|
|
|
2020-03-31 09:20:47 +02:00
|
|
|
save(record) { result in
|
2020-03-30 22:15:45 +02:00
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
completion(.success(()))
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
delete(externalID: folder.externalID, completion: completion)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension CloudKitAccountZone {
|
|
|
|
|
2020-04-01 01:10:35 +02:00
|
|
|
func newWebFeedCKRecord(feedSpecifier: RSOPMLFeedSpecifier, containerExternalID: String) -> CKRecord {
|
|
|
|
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: generateRecordID())
|
|
|
|
record[CloudKitWebFeed.Fields.url] = feedSpecifier.feedURL
|
|
|
|
if let editedName = feedSpecifier.title {
|
|
|
|
record[CloudKitWebFeed.Fields.editedName] = editedName
|
|
|
|
}
|
2020-05-06 01:25:33 +02:00
|
|
|
if let homePageURL = feedSpecifier.homePageURL {
|
|
|
|
record[CloudKitWebFeed.Fields.homePageURL] = homePageURL
|
|
|
|
}
|
2020-04-01 01:10:35 +02:00
|
|
|
record[CloudKitWebFeed.Fields.containerExternalIDs] = [containerExternalID]
|
|
|
|
return record
|
|
|
|
}
|
|
|
|
|
|
|
|
func newContainerCKRecord(name: String) -> CKRecord {
|
|
|
|
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
|
|
|
|
record[CloudKitContainer.Fields.name] = name
|
2020-04-01 03:42:39 +02:00
|
|
|
record[CloudKitContainer.Fields.isAccount] = "0"
|
2020-04-01 01:10:35 +02:00
|
|
|
return record
|
|
|
|
}
|
|
|
|
|
2020-03-30 22:15:45 +02:00
|
|
|
func createContainer(name: String, isAccount: Bool, completion: @escaping (Result<String, Error>) -> Void) {
|
|
|
|
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
|
|
|
|
record[CloudKitContainer.Fields.name] = name
|
2020-04-01 03:42:39 +02:00
|
|
|
record[CloudKitContainer.Fields.isAccount] = isAccount ? "1" : "0"
|
2020-03-30 22:15:45 +02:00
|
|
|
|
2020-03-31 09:20:47 +02:00
|
|
|
save(record) { result in
|
2020-03-30 22:15:45 +02:00
|
|
|
switch result {
|
|
|
|
case .success:
|
|
|
|
completion(.success(record.externalID))
|
|
|
|
case .failure(let error):
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
}
|
2020-03-29 15:52:59 +02:00
|
|
|
}
|
2020-03-22 22:35:03 +01:00
|
|
|
|
|
|
|
}
|