diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index c1489b2cd..252235ddd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -6,8 +6,6 @@ import Utils import Views @MainActor final class HomeFeedViewModel: NSObject, ObservableObject { - let dateFormatter = DateFormatter.formatterISO8601 - var currentDetailViewModel: LinkItemDetailViewModel? private var fetchedResultsController: NSFetchedResultsController? @@ -110,7 +108,7 @@ import Views func syncItems(dataService: DataService) async { let syncStart = Date.now - let lastSyncDate = dateFormatter.date(from: dataService.lastItemSyncTime) ?? Date(timeIntervalSinceReferenceDate: 0) + let lastSyncDate = dataService.lastItemSyncTime try? await dataService.syncOfflineItemsWithServerIfNeeded() @@ -124,7 +122,7 @@ import Views self.isLoading = false } } else { - dataService.lastItemSyncTime = DateFormatter.formatterISO8601.string(from: syncStart) + dataService.lastItemSyncTime = syncStart } // If possible start prefetching new pages in the background diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift index 020909470..f38c75176 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift @@ -39,7 +39,7 @@ struct ManageAccountView: View { ) Button( action: { - dataService.resetCoreData() + dataService.resetLocalStorage() }, label: { Text(LocalText.manageAccountResetCache) } ) diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index 44403ccdf..74d5a8af6 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -543,8 +543,7 @@ } public static func pathForAudioDirectory(itemID: String) -> URL { - FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask)[0] + URL.om_documentsDirectory .appendingPathComponent("audio-\(itemID)/") } diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift index 8df38fb02..5af60dece 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift @@ -56,8 +56,7 @@ struct SpeechDocument: Decodable { } static func audioDirectory(pageId: String) -> URL { - FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask)[0] + URL.om_documentsDirectory .appendingPathComponent("audio-\(pageId)") } } @@ -213,12 +212,10 @@ struct SpeechSynthesizer { let data = try await downloadData(session: session, request: speechItem.urlRequest) - let tempPath = FileManager.default - .urls(for: .cachesDirectory, in: .userDomainMask)[0] + let tempPath = URL.om_cachesDirectory .appendingPathComponent(UUID().uuidString + ".mp3") - let tempSMPath = FileManager.default - .urls(for: .cachesDirectory, in: .userDomainMask)[0] + let tempSMPath = URL.om_cachesDirectory .appendingPathComponent(UUID().uuidString + ".speechMarks") do { diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index b0ccfba59..ac54c7491 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -38,7 +38,7 @@ public final class Authenticator: ObservableObject { } public func logout(dataService: DataService, isAccountDeletion: Bool = false) { - dataService.resetCoreData() + dataService.resetLocalStorage() clearCreds() Authenticator.unregisterIntercomUser?() isLoggedIn = false diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 5ca6c8aab..f878806bf 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -29,9 +29,22 @@ public final class DataService: ObservableObject { persistentContainer.viewContext } - @AppStorage(UserDefaultKey.lastItemSyncTime.rawValue) public var lastItemSyncTime: String = { - DateFormatter.formatterISO8601.string(from: Date(timeIntervalSinceReferenceDate: 0)) - }() + public var lastItemSyncTime: Date { + get { + guard + let str = UserDefaults.standard.string(forKey: UserDefaultKey.lastItemSyncTime.rawValue), + let date = DateFormatter.formatterISO8601.date(from: str) + else { + return Date(timeIntervalSinceReferenceDate: 0) + } + return date + } + set { + logger.trace("last item sync updated to \(newValue)") + let str = DateFormatter.formatterISO8601.string(from: newValue) + UserDefaults.standard.set(str, forKey: UserDefaultKey.lastItemSyncTime.rawValue) + } + } public init(appEnvironment: AppEnvironment, networker: Networker) { self.appEnvironment = appEnvironment @@ -43,7 +56,7 @@ public final class DataService: ObservableObject { backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump if isFirstTimeRunningNewAppBuild() { - resetCoreData() + resetLocalStorage() } else { persistentContainer.loadPersistentStores { _, error in if let error = error { @@ -102,10 +115,58 @@ public final class DataService: ObservableObject { } } - public func resetCoreData() { - lastItemSyncTime = DateFormatter.formatterISO8601.string(from: Date(timeIntervalSinceReferenceDate: 0)) + private func clearDownloadedFiles() { + let relevantTypes = ["pdf", "mp3", "speechMarks"] + let fileMgr = FileManager() + logger.trace("removing cached downloads") + + // clear the temporary files in the caches directory… + if let cacheFileURLs = try? fileMgr.contentsOfDirectory( + at: URL.om_cachesDirectory, + includingPropertiesForKeys: .none, + options: .skipsHiddenFiles + ) { + logger.trace("\(cacheFileURLs.count) file URLs in caches directory") + for fileURL in cacheFileURLs where relevantTypes.contains(fileURL.pathExtension) { + logger.trace("removing \(fileURL.absoluteString)") + try? fileMgr.removeItem(at: fileURL) + } + } + + // …and also the copies written to Documents + let resourceKeys = Set([.nameKey, .isDirectoryKey]) + if let documentsFileURLs = try? fileMgr.contentsOfDirectory( + at: URL.om_documentsDirectory, + includingPropertiesForKeys: Array(resourceKeys), + options: .skipsHiddenFiles + ) { + logger.trace("\(documentsFileURLs.count) file URLs in documents directory") + for fileURL in documentsFileURLs { + guard + let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys), + let isDirectory = resourceValues.isDirectory, + let name = resourceValues.name + else { + continue + } + if isDirectory { + if name.hasPrefix("audio-") { + logger.trace("removing \(fileURL.absoluteString)") + try? fileMgr.removeItem(at: fileURL) + } + } else if relevantTypes.contains(fileURL.pathExtension) { + logger.trace("removing \(fileURL.absoluteString)") + try? fileMgr.removeItem(at: fileURL) + } + } + } + } + + public func resetLocalStorage() { + lastItemSyncTime = Date(timeIntervalSinceReferenceDate: 0) clearCoreData() + clearDownloadedFiles() persistentContainer = PersistentContainer.make() persistentContainer.loadPersistentStores { _, error in diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift index ad776f9ce..a4d68bdce 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -93,7 +93,7 @@ public extension DataService { } } DispatchQueue.main.sync { - self.lastItemSyncTime = DateFormatter.formatterISO8601.string(from: Date.now) + self.lastItemSyncTime = Date.now onComplete() } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift index c22e01542..0f4315844 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -21,8 +21,7 @@ public extension DataService { var localPdfURL: URL? - let tempPath = FileManager.default - .urls(for: .cachesDirectory, in: .userDomainMask)[0] + let tempPath = URL.om_cachesDirectory .appendingPathComponent(UUID().uuidString + ".pdf") try await backgroundContext.perform { [weak self] in diff --git a/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift b/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift index b722edc3f..3a058ef6b 100644 --- a/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/PDFUtils.swift @@ -10,8 +10,7 @@ import QuickLookThumbnailing public enum PDFUtils { public static func copyToLocal(url: URL) throws -> String { let subPath = UUID().uuidString + ".pdf" - let dest = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask)[0] + let dest = URL.om_documentsDirectory .appendingPathComponent(subPath) try FileManager.default.copyItem(at: url, to: dest) @@ -20,8 +19,7 @@ public enum PDFUtils { public static func moveToLocal(url: URL) throws -> String { let subPath = UUID().uuidString + ".pdf" - let dest = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask)[0] + let dest = URL.om_documentsDirectory .appendingPathComponent(subPath) try FileManager.default.moveItem(at: url, to: dest) @@ -29,8 +27,7 @@ public enum PDFUtils { } public static func localPdfURL(filename: String) -> URL? { - let url = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask)[0] + let url = URL.om_documentsDirectory .appendingPathComponent(filename) return url diff --git a/apple/OmnivoreKit/Sources/Utils/URLExtension.swift b/apple/OmnivoreKit/Sources/Utils/URLExtension.swift new file mode 100644 index 000000000..0eb03a615 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Utils/URLExtension.swift @@ -0,0 +1,27 @@ +import Foundation + +public extension URL { + // swiftlint:disable:next identifier_name + static var om_documentsDirectory: URL { + if #unavailable(iOS 16, tvOS 16, macOS 13) { + guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + fatalError("Could not determine the user's documents directory") + } + return url + } else { + return URL.documentsDirectory + } + } + + // swiftlint:disable:next identifier_name + static var om_cachesDirectory: URL { + if #unavailable(iOS 16, tvOS 16, macOS 13) { + guard let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + fatalError("Could not determine the user's caches directory") + } + return url + } else { + return URL.cachesDirectory + } + } +}