// // OPMLFile.swift // Account // // Created by Maurice Parker on 9/12/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // import Foundation import os.log import RSCore import RSParser final class OPMLFile { 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 load() { managedFile.load() } func save() { managedFile.saveIfNecessary() } func suspend() { managedFile.suspend() } func resume() { managedFile.resume() } } private extension OPMLFile { func loadCallback() { guard let fileData = opmlFileData() else { return } // Don't rebuild the account if the OPML hasn't changed since the last save guard let opml = String(data: fileData, encoding: .utf8), opml != opmlDocument() else { return } guard let opmlItems = parsedOPMLItems(fileData: fileData) else { return } BatchUpdate.shared.perform { account.topLevelWebFeeds.removeAll() account.loadOPMLItems(opmlItems, parentFolder: nil) } } func saveCallback() { guard !account.isDeleted else { return } let opmlDocumentString = opmlDocument() let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) 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 { os_log(.error, log: log, "OPML save to disk failed: %@.", error.localizedDescription) } }) if let error = errorPointer?.pointee { os_log(.error, log: log, "OPML save to disk coordination failed: %@.", error.localizedDescription) } } func opmlFileData() -> Data? { var fileData: Data? = nil let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator(filePresenter: managedFile) fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in do { fileData = try Data(contentsOf: readURL) } catch { // Commented out because it’s not an error on first run. // TODO: make it so we know if it’s first run or not. //NSApplication.shared.presentError(error) os_log(.error, log: log, "OPML read from disk failed: %@.", error.localizedDescription) } }) if let error = errorPointer?.pointee { os_log(.error, log: log, "OPML read from disk coordination failed: %@.", error.localizedDescription) } return fileData } func parsedOPMLItems(fileData: Data) -> [RSOPMLItem]? { let parserData = ParserData(url: fileURL.absoluteString, data: fileData) var opmlDocument: RSOPMLDocument? do { opmlDocument = try RSOPMLParser.parseOPML(with: parserData) } catch { os_log(.error, log: log, "OPML Import failed: %@.", error.localizedDescription) return nil } return opmlDocument?.children } func opmlDocument() -> String { let escapedTitle = account.nameForDisplay.escapingSpecialXMLCharacters let openingText = """ \(escapedTitle) """ let middleText = account.OPMLString(indentLevel: 0, allowCustomAttributes: true) let closingText = """ """ let opml = openingText + middleText + closingText return opml } }