diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 86a117d52..3f2a51dc9 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -129,7 +129,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public var folders: Set? = Set() private var feedDictionaryNeedsUpdate = true private var _idToFeedDictionary = [String: Feed]() - private var idToFeedDictionary: [String: Feed] { + var idToFeedDictionary: [String: Feed] { if feedDictionaryNeedsUpdate { rebuildFeedDictionaries() } @@ -172,17 +172,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, private var flattenedFeedsNeedUpdate = true private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self) - private lazy var metadataFile = AccountMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("Settings.opml"), account: self) + private lazy var metadataFile = AccountMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("Settings.plist"), account: self) var metadata = AccountMetadata() - private let feedMetadataPath: String - private typealias FeedMetadataDictionary = [String: FeedMetadata] - private var feedMetadata = FeedMetadataDictionary() - private var feedMetadataDirty = false { - didSet { - queueSaveFeedMetadataIfNeeded() - } - } + private lazy var feedMetadataFile = FeedMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist"), account: self) + typealias FeedMetadataDictionary = [String: FeedMetadata] + var feedMetadata = FeedMetadataDictionary() private var startingUp = true @@ -243,8 +238,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3") self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID) - self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist") - switch type { case .onMyMac: defaultName = Account.defaultLocalAccountName @@ -266,8 +259,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil) - pullObjectsFromDisk() - + metadataFile.load() + feedMetadataFile.load() + opmlFile.load() + DispatchQueue.main.async { self.fetchAllUnreadCounts() } @@ -756,12 +751,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } - @objc func saveFeedMetadataIfNeeded() { - if feedMetadataDirty && !isDeleted { - saveFeedMetadata() - } - } - // MARK: - Hashable public func hash(into hasher: inout Hasher) { @@ -788,7 +777,7 @@ extension Account: AccountMetadataDelegate { extension Account: FeedMetadataDelegate { func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) { - feedMetadataDirty = true + feedMetadataFile.markAsDirty() guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else { return } @@ -929,55 +918,6 @@ private extension Account { } } -// MARK: - Disk (Private) - -private extension Account { - - func pullObjectsFromDisk() { - metadataFile.load() - loadFeedMetadata() - opmlFile.load() - } - - func loadFeedMetadata() { - let url = URL(fileURLWithPath: feedMetadataPath) - guard let data = try? Data(contentsOf: url) else { - return - } - let decoder = PropertyListDecoder() - feedMetadata = (try? decoder.decode(FeedMetadataDictionary.self, from: data)) ?? FeedMetadataDictionary() - feedMetadata.values.forEach { $0.delegate = self } - } - - func queueSaveFeedMetadataIfNeeded() { - Account.saveQueue.add(self, #selector(saveFeedMetadataIfNeeded)) - } - - private func metadataForOnlySubscribedToFeeds() -> FeedMetadataDictionary { - let feedIDs = idToFeedDictionary.keys - return feedMetadata.filter { (feedID: String, metadata: FeedMetadata) -> Bool in - return feedIDs.contains(metadata.feedID) - } - } - - func saveFeedMetadata() { - feedMetadataDirty = false - - let d = metadataForOnlySubscribedToFeeds() - let encoder = PropertyListEncoder() - encoder.outputFormat = .binary - let url = URL(fileURLWithPath: feedMetadataPath) - do { - let data = try encoder.encode(d) - try data.write(to: url) - } - catch { - assertionFailure(error.localizedDescription) - } - } - -} - // MARK: - Private private extension Account { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 07ef7aded..ca261c228 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; }; 5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; }; 510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; }; + 510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */; }; 513323082281070D00C30F19 /* AccountFeedSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedSyncTest.swift */; }; 5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; }; 5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; }; @@ -124,6 +125,7 @@ 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = ""; }; 5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = ""; }; 510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = ""; }; + 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMetadataFile.swift; sourceTree = ""; }; 513323072281070C00C30F19 /* AccountFeedSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedSyncTest.swift; sourceTree = ""; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = ""; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = ""; }; @@ -347,6 +349,7 @@ 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */, 844B297C2106C7EC004020B3 /* Feed.swift */, 84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */, + 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */, 841974001F6DD1EC006346C4 /* Folder.swift */, 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */, 5165D71F22835E9800D9D53D /* FeedFinder */, @@ -576,6 +579,7 @@ 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, + 510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */, 515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */, diff --git a/Frameworks/Account/AccountMetadataFile.swift b/Frameworks/Account/AccountMetadataFile.swift index df2ca6918..e27455c07 100644 --- a/Frameworks/Account/AccountMetadataFile.swift +++ b/Frameworks/Account/AccountMetadataFile.swift @@ -9,11 +9,10 @@ import Foundation import os.log import RSCore -import RSParser final class AccountMetadataFile { - private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "opmlFile") + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "accountMetadataFile") private let fileURL: URL private let account: Account @@ -68,7 +67,7 @@ private extension AccountMetadataFile { let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) - fileCoordinator.coordinate(writingItemAt: fileURL, options: .forReplacing, error: errorPointer, byAccessor: { writeURL in + fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in do { let data = try encoder.encode(account.metadata) try data.write(to: writeURL) diff --git a/Frameworks/Account/FeedMetadataFile.swift b/Frameworks/Account/FeedMetadataFile.swift new file mode 100644 index 000000000..739518232 --- /dev/null +++ b/Frameworks/Account/FeedMetadataFile.swift @@ -0,0 +1,94 @@ +// +// FeedMetadataFile.swift +// Account +// +// Created by Maurice Parker on 9/13/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log +import RSCore + +final class FeedMetadataFile { + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "feedMetadataFile") + + private let fileURL: URL + private let account: Account + private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback) + + init(filename: String, account: Account) { + self.fileURL = URL(fileURLWithPath: filename) + self.account = account + } + + func markAsDirty() { + managedFile.markAsDirty() + } + + func queueSaveToDiskIfNeeded() { + managedFile.queueSaveToDiskIfNeeded() + } + + func load() { + managedFile.load() + } + +} + +private extension FeedMetadataFile { + + func loadCallback() { + + let errorPointer: NSErrorPointer = nil + let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) + + fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in + if let fileData = try? Data(contentsOf: readURL) { + let decoder = PropertyListDecoder() + account.feedMetadata = (try? decoder.decode(Account.FeedMetadataDictionary.self, from: fileData)) ?? Account.FeedMetadataDictionary() + } + }) + + if let error = errorPointer?.pointee { + os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription) + } + + account.feedMetadata.values.forEach { $0.delegate = account } + + } + + func saveCallback() { + guard !account.isDeleted else { return } + + let feedMetadata = metadataForOnlySubscribedToFeeds() + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + + let errorPointer: NSErrorPointer = nil + let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) + + fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in + do { + let data = try encoder.encode(feedMetadata) + try data.write(to: writeURL) + } catch let error as NSError { + os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription) + } + }) + + if let error = errorPointer?.pointee { + os_log(.error, log: log, "Save to disk coordination failed: %@.", error.localizedDescription) + } + } + + private func metadataForOnlySubscribedToFeeds() -> Account.FeedMetadataDictionary { + let feedIDs = account.idToFeedDictionary.keys + return account.feedMetadata.filter { (feedID: String, metadata: FeedMetadata) -> Bool in + return feedIDs.contains(metadata.feedID) + } + } + +} diff --git a/Frameworks/Account/OPMLFile.swift b/Frameworks/Account/OPMLFile.swift index 8be408237..c21d1f8d3 100644 --- a/Frameworks/Account/OPMLFile.swift +++ b/Frameworks/Account/OPMLFile.swift @@ -56,7 +56,7 @@ private extension OPMLFile { let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) - fileCoordinator.coordinate(writingItemAt: fileURL, options: .forReplacing, error: errorPointer, byAccessor: { writeURL in + fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in do { try opmlDocumentString.write(to: writeURL, atomically: true, encoding: .utf8) } catch let error as NSError {