Make account metadata a managed resource file

This commit is contained in:
Maurice Parker 2019-09-13 16:12:19 -05:00
parent f7bb58ea4d
commit b24e6a4725
5 changed files with 96 additions and 55 deletions

View File

@ -167,18 +167,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0)
private var unreadCounts = [String: Int]() // [feedID: Int] 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<Feed>() private var _flattenedFeeds = Set<Feed>()
private var flattenedFeedsNeedUpdate = true 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() var metadata = AccountMetadata()
private var metadataDirty = false {
didSet {
queueSaveAccountMetadatafNeeded()
}
}
private let feedMetadataPath: String private let feedMetadataPath: String
private typealias FeedMetadataDictionary = [String: FeedMetadata] private typealias FeedMetadataDictionary = [String: FeedMetadata]
@ -249,7 +244,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID) self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID)
self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist") self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist")
self.metadataPath = (dataFolder as NSString).appendingPathComponent("Settings.plist")
switch type { switch type {
case .onMyMac: case .onMyMac:
@ -768,12 +762,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
} }
@objc func saveAccountMetadataIfNeeded() {
if metadataDirty && !isDeleted {
saveAccountMetadata()
}
}
// MARK: - Hashable // MARK: - Hashable
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
@ -791,7 +779,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
extension Account: AccountMetadataDelegate { extension Account: AccountMetadataDelegate {
func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) { func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) {
metadataDirty = true metadataFile.markAsDirty()
} }
} }
@ -946,22 +934,11 @@ private extension Account {
private extension Account { private extension Account {
func pullObjectsFromDisk() { func pullObjectsFromDisk() {
loadAccountMetadata() metadataFile.load()
loadFeedMetadata() loadFeedMetadata()
opmlFile.load() 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() { func loadFeedMetadata() {
let url = URL(fileURLWithPath: feedMetadataPath) let url = URL(fileURLWithPath: feedMetadataPath)
guard let data = try? Data(contentsOf: url) else { 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 // MARK: - Private

View File

@ -10,6 +10,7 @@
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; }; 5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; };
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; }; 5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.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 */; }; 513323082281070D00C30F19 /* AccountFeedSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedSyncTest.swift */; };
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; }; 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 */; }; 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 = "<group>"; }; 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = "<group>"; };
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; }; 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; }; 5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; };
513323072281070C00C30F19 /* AccountFeedSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedSyncTest.swift; sourceTree = "<group>"; }; 513323072281070C00C30F19 /* AccountFeedSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedSyncTest.swift; sourceTree = "<group>"; };
513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; };
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
@ -337,6 +339,7 @@
846E77531F6F00E300A165E2 /* AccountManager.swift */, 846E77531F6F00E300A165E2 /* AccountManager.swift */,
5170743B232AEDB500A461A3 /* OPMLFile.swift */, 5170743B232AEDB500A461A3 /* OPMLFile.swift */,
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */, 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */,
510BD110232C3801002692E4 /* AccountMetadataFile.swift */,
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
8419740D1F6DD25F006346C4 /* Container.swift */, 8419740D1F6DD25F006346C4 /* Container.swift */,
@ -600,6 +603,7 @@
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */, 5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
841974011F6DD1EC006346C4 /* Folder.swift in Sources */, 841974011F6DD1EC006346C4 /* Folder.swift in Sources */,
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */,
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */, 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */,
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */, 515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */, 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,

View File

@ -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)
}
}
}

View File

@ -17,7 +17,7 @@ final class OPMLFile {
private let fileURL: URL private let fileURL: URL
private let account: Account private let account: Account
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, reload: reloadCallback, save: saveCallback) private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
init(filename: String, account: Account) { init(filename: String, account: Account) {
self.fileURL = URL(fileURLWithPath: filename) self.fileURL = URL(fileURLWithPath: filename)
@ -43,6 +43,7 @@ private extension OPMLFile {
func loadCallback() { func loadCallback() {
guard let opmlItems = parsedOPMLItems() else { return } guard let opmlItems = parsedOPMLItems() else { return }
BatchUpdate.shared.perform { BatchUpdate.shared.perform {
account.topLevelFeeds.removeAll()
account.loadOPMLItems(opmlItems, parentFolder: nil) account.loadOPMLItems(opmlItems, parentFolder: nil)
} }
} }
@ -68,14 +69,6 @@ private extension OPMLFile {
} }
} }
func reloadCallback() {
guard let opmlItems = parsedOPMLItems() else { return }
BatchUpdate.shared.perform {
account.topLevelFeeds.removeAll()
account.loadOPMLItems(opmlItems, parentFolder: nil)
}
}
func parsedOPMLItems() -> [RSOPMLItem]? { func parsedOPMLItems() -> [RSOPMLItem]? {
var fileData: Data? = nil var fileData: Data? = nil

@ -1 +1 @@
Subproject commit 1adcf4087b6cb5806c81d8cdfd68d73858408ae5 Subproject commit ee343a204d2f402240fe1c226ff4b8dbe33a3129