diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 920f1f162..86a117d52 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -167,18 +167,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) private var unreadCounts = [String: Int]() // [feedID: Int] - private lazy var opmlFile: OPMLFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self) private var _flattenedFeeds = Set() private var flattenedFeedsNeedUpdate = true - private let metadataPath: String + 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) var metadata = AccountMetadata() - private var metadataDirty = false { - didSet { - queueSaveAccountMetadatafNeeded() - } - } private let feedMetadataPath: String private typealias FeedMetadataDictionary = [String: FeedMetadata] @@ -249,7 +244,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID) self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist") - self.metadataPath = (dataFolder as NSString).appendingPathComponent("Settings.plist") switch type { case .onMyMac: @@ -768,12 +762,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } - @objc func saveAccountMetadataIfNeeded() { - if metadataDirty && !isDeleted { - saveAccountMetadata() - } - } - // MARK: - Hashable public func hash(into hasher: inout Hasher) { @@ -791,7 +779,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, extension Account: AccountMetadataDelegate { func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) { - metadataDirty = true + metadataFile.markAsDirty() } } @@ -946,22 +934,11 @@ private extension Account { private extension Account { func pullObjectsFromDisk() { - loadAccountMetadata() + metadataFile.load() loadFeedMetadata() opmlFile.load() } - func loadAccountMetadata() { - let url = URL(fileURLWithPath: metadataPath) - guard let data = try? Data(contentsOf: url) else { - metadata.delegate = self - return - } - let decoder = PropertyListDecoder() - metadata = (try? decoder.decode(AccountMetadata.self, from: data)) ?? AccountMetadata() - metadata.delegate = self - } - func loadFeedMetadata() { let url = URL(fileURLWithPath: feedMetadataPath) guard let data = try? Data(contentsOf: url) else { @@ -999,24 +976,6 @@ private extension Account { } } - func queueSaveAccountMetadatafNeeded() { - Account.saveQueue.add(self, #selector(saveAccountMetadataIfNeeded)) - } - - func saveAccountMetadata() { - metadataDirty = false - - let encoder = PropertyListEncoder() - encoder.outputFormat = .binary - let url = URL(fileURLWithPath: metadataPath) - do { - let data = try encoder.encode(metadata) - try data.write(to: url) - } - catch { - assertionFailure(error.localizedDescription) - } - } } // MARK: - Private diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 7723cfa9c..07ef7aded 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; }; 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 */; }; 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 */; }; @@ -122,6 +123,7 @@ 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -337,6 +339,7 @@ 846E77531F6F00E300A165E2 /* AccountManager.swift */, 5170743B232AEDB500A461A3 /* OPMLFile.swift */, 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */, + 510BD110232C3801002692E4 /* AccountMetadataFile.swift */, 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, 8419740D1F6DD25F006346C4 /* Container.swift */, @@ -600,6 +603,7 @@ 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, 5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */, 841974011F6DD1EC006346C4 /* Folder.swift in Sources */, + 510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */, 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */, 515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */, 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */, diff --git a/Frameworks/Account/AccountMetadataFile.swift b/Frameworks/Account/AccountMetadataFile.swift new file mode 100644 index 000000000..df2ca6918 --- /dev/null +++ b/Frameworks/Account/AccountMetadataFile.swift @@ -0,0 +1,85 @@ +// +// AccountMetadataFile.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 +import RSParser + +final class AccountMetadataFile { + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "opmlFile") + + 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 AccountMetadataFile { + + 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.metadata = (try? decoder.decode(AccountMetadata.self, from: fileData)) ?? AccountMetadata() + } + }) + + if let error = errorPointer?.pointee { + os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription) + } + + account.metadata.delegate = account + } + + func saveCallback() { + guard !account.isDeleted else { return } + + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + + let errorPointer: NSErrorPointer = nil + let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) + + fileCoordinator.coordinate(writingItemAt: fileURL, options: .forReplacing, error: errorPointer, byAccessor: { writeURL in + do { + let data = try encoder.encode(account.metadata) + 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) + } + } + +} diff --git a/Frameworks/Account/OPMLFile.swift b/Frameworks/Account/OPMLFile.swift index 809599c33..8be408237 100644 --- a/Frameworks/Account/OPMLFile.swift +++ b/Frameworks/Account/OPMLFile.swift @@ -11,81 +11,50 @@ import os.log import RSCore import RSParser -final class OPMLFile: NSObject, NSFilePresenter { +final class OPMLFile { - private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "account") + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "opmlFile") - private var isDirty = false { - didSet { - queueSaveToDiskIfNeeded() - } - } - - private var isLoading = false private let fileURL: URL private let account: Account - private let operationQueue: OperationQueue - - var presentedItemURL: URL? { - return fileURL - } - - var presentedItemOperationQueue: OperationQueue { - return operationQueue - } + private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback) init(filename: String, account: Account) { self.fileURL = URL(fileURLWithPath: filename) self.account = account - operationQueue = OperationQueue() - operationQueue.maxConcurrentOperationCount = 1 - - super.init() - - NSFileCoordinator.addFilePresenter(self) - } - - func presentedItemDidChange() { - DispatchQueue.main.async { - self.reload() - } } func markAsDirty() { - if !isLoading { - isDirty = true - } + managedFile.markAsDirty() } func queueSaveToDiskIfNeeded() { - Account.saveQueue.add(self, #selector(saveToDiskIfNeeded)) + managedFile.queueSaveToDiskIfNeeded() } func load() { - isLoading = true - guard let opmlItems = parsedOPMLItems() else { return } - BatchUpdate.shared.perform { - account.loadOPMLItems(opmlItems, parentFolder: nil) - } - isLoading = false + managedFile.load() } } private extension OPMLFile { - - @objc func saveToDiskIfNeeded() { - if isDirty && !account.isDeleted { - isDirty = false - save() + + func loadCallback() { + guard let opmlItems = parsedOPMLItems() else { return } + BatchUpdate.shared.perform { + account.topLevelFeeds.removeAll() + account.loadOPMLItems(opmlItems, parentFolder: nil) } } - - func save() { + + func saveCallback() { + guard !account.isDeleted else { return } + let opmlDocumentString = opmlDocument() let errorPointer: NSErrorPointer = nil - let fileCoordinator = NSFileCoordinator(filePresenter: self) + let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) fileCoordinator.coordinate(writingItemAt: fileURL, options: .forReplacing, error: errorPointer, byAccessor: { writeURL in do { @@ -100,21 +69,11 @@ private extension OPMLFile { } } - func reload() { - isLoading = true - guard let opmlItems = parsedOPMLItems() else { return } - BatchUpdate.shared.perform { - account.topLevelFeeds.removeAll() - account.loadOPMLItems(opmlItems, parentFolder: nil) - } - isLoading = false - } - func parsedOPMLItems() -> [RSOPMLItem]? { var fileData: Data? = nil let errorPointer: NSErrorPointer = nil - let fileCoordinator = NSFileCoordinator(filePresenter: self) + let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in do { diff --git a/submodules/RSCore b/submodules/RSCore index d640a2310..ee343a204 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit d640a2310b96a0a3d4d34c49c08c7bce195d0762 +Subproject commit ee343a204d2f402240fe1c226ff4b8dbe33a3129