diff --git a/.gitmodules b/.gitmodules index c241e984..a4527d8f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "Cache"] - path = Cache - url = git@github.com:hyperoslo/Cache.git [submodule "Gifu"] path = Gifu url = git://github.com/kaishin/Gifu.git diff --git a/Cache b/Cache deleted file mode 160000 index 8c42c575..00000000 --- a/Cache +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8c42c575cf28b2ff0e780c9728721e9a8891c92e diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 5a10b431..4bcc566e 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -14,8 +14,6 @@ 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; }; 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; }; 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; }; - 0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; }; - 0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D14BAE22B34A2800642648 /* GalleryViewController.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; @@ -113,7 +111,6 @@ D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; }; D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; }; - D6311C5625B4CEA000B27539 /* CachingDiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C5525B4CEA000B27539 /* CachingDiskStorage.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; }; @@ -228,6 +225,9 @@ D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; }; D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */; }; + D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10E25B62D2400298D0F /* DiskCache.swift */; }; + D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; }; + D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; }; D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; }; D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; }; D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; }; @@ -304,7 +304,6 @@ D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; }; D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; }; D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; }; - D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; }; D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; }; D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; @@ -358,7 +357,6 @@ files = ( D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */, D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */, - 0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -472,7 +470,6 @@ D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = ""; }; D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = ""; }; D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = ""; }; - D6311C5525B4CEA000B27539 /* CachingDiskStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachingDiskStorage.swift; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = ""; }; @@ -589,6 +586,9 @@ D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = ""; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = ""; }; D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTablePrefetching.swift; sourceTree = ""; }; + D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = ""; }; + D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = ""; }; + D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = ""; }; D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = ""; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = ""; }; D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = ""; }; @@ -668,7 +668,6 @@ D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = ""; }; D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = ""; }; D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = ""; }; - D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = ""; }; D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; @@ -699,7 +698,6 @@ D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */, D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */, D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */, - 0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */, D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1498,10 +1496,11 @@ D6F1F84E2193B9BE00F5FE67 /* Caching */ = { isa = PBXGroup; children = ( - D6F1F84C2193B56E00F5FE67 /* Cache.swift */, - 04DACE8D212CC7CC009840C4 /* ImageCache.swift */, + D6A6C11425B62E9700298D0F /* CacheExpiry.swift */, + D6A6C10E25B62D2400298D0F /* DiskCache.swift */, + D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */, D6311C4F25B3765B00B27539 /* ImageDataCache.swift */, - D6311C5525B4CEA000B27539 /* CachingDiskStorage.swift */, + 04DACE8D212CC7CC009840C4 /* ImageCache.swift */, ); path = Caching; sourceTree = ""; @@ -1880,7 +1879,6 @@ D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, - D6311C5625B4CEA000B27539 /* CachingDiskStorage.swift in Sources */, D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */, @@ -1921,6 +1919,7 @@ D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */, D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */, D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */, + D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */, D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */, D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, @@ -1944,6 +1943,7 @@ D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */, + D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */, D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */, @@ -2020,6 +2020,7 @@ D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */, + D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */, D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, @@ -2045,7 +2046,6 @@ D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */, D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */, D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */, - D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */, D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */, 0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */, D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */, diff --git a/Tusker.xcworkspace/contents.xcworkspacedata b/Tusker.xcworkspace/contents.xcworkspacedata index 5223b08d..b46d306a 100644 --- a/Tusker.xcworkspace/contents.xcworkspacedata +++ b/Tusker.xcworkspace/contents.xcworkspacedata @@ -7,9 +7,6 @@ - - diff --git a/Tusker/Caching/Cache.swift b/Tusker/Caching/Cache.swift deleted file mode 100644 index 5a38e9e8..00000000 --- a/Tusker/Caching/Cache.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Cache.swift -// Tusker -// -// Created by Shadowfacts on 11/7/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import Foundation -import Cache - -/// Wrapper around Cache library that provides an API for transparently using any storage type -enum Cache { - case memory(MemoryStorage) - case disk(DiskStorage) - case hybrid(HybridStorage) - - @available(*, deprecated, message: "disk-based caches synchronously interact with the file system. Avoid using if possible.") - func existsObject(forKey key: String) throws -> Bool { - switch self { - case let .memory(memory): - return try memory.existsObject(forKey: key) - case let .disk(disk): - return try disk.existsObject(forKey: key) - case let .hybrid(hybrid): - return try hybrid.existsObject(forKey: key) - } - } - - func object(forKey key: String) throws -> T { - switch self { - case let .memory(memory): - return try memory.object(forKey: key) - case let .disk(disk): - return try disk.object(forKey: key) - case let .hybrid(hybrid): - return try hybrid.object(forKey: key) - } - } - - func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws { - switch self { - case let .memory(memory): - memory.setObject(object, forKey: key, expiry: expiry) - case let .disk(disk): - try disk.setObject(object, forKey: key, expiry: expiry) - case let .hybrid(hybrid): - try hybrid.setObject(object, forKey: key, expiry: expiry) - } - } - - func removeAll() throws { - switch self { - case let .memory(memory): - memory.removeAll() - case let .disk(disk): - try disk.removeAll() - case let .hybrid(hybrid): - try hybrid.removeAll() - } - } -} diff --git a/Tusker/Caching/CacheExpiry.swift b/Tusker/Caching/CacheExpiry.swift new file mode 100644 index 00000000..0d24143a --- /dev/null +++ b/Tusker/Caching/CacheExpiry.swift @@ -0,0 +1,30 @@ +// +// CacheExpiry.swift +// Tusker +// +// Created by Shadowfacts on 1/18/21. +// Copyright © 2021 Shadowfacts. All rights reserved. +// + +import Foundation + +enum CacheExpiry { + case never + case seconds(TimeInterval) + case date(Date) + + var date: Date { + switch self { + case .never: + return .distantFuture + case let .seconds(seconds): + return Date().addingTimeInterval(seconds) + case let .date(date): + return date + } + } + + var isExpired: Bool { + return date.timeIntervalSinceNow < 0 + } +} diff --git a/Tusker/Caching/CachingDiskStorage.swift b/Tusker/Caching/CachingDiskStorage.swift deleted file mode 100644 index e666d20b..00000000 --- a/Tusker/Caching/CachingDiskStorage.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// CachingDiskStorage.swift -// Tusker -// -// Created by Shadowfacts on 1/17/21. -// Copyright © 2021 Shadowfacts. All rights reserved. -// - -import Foundation -import Cache - -/// This class wraps a `DiskStorage` and maintains an in-memory cache of whether objects -/// exist on disk to avoid unnecessary disk I/O when calling synchronous methods like -/// `existsObject(forKey:)`. -class CachingDiskStorage: StorageAware { - - private let storage: DiskStorage - private var files = [String: FileState]() - - init(config: DiskConfig, transformer: Transformer) throws { - storage = try DiskStorage(config: config, transformer: transformer) - } - - private func state(for key: String) -> FileState { - return files[key] ?? .unknown - } - - func entry(forKey key: String) throws -> Entry { - if state(for: key) == .doesNotExist { - throw StorageError.notFound - } - return try storage.entry(forKey: key) - } - - func existsObject(forKey key: String) throws -> Bool { - switch state(for: key) { - case .unknown: - return try storage.existsObject(forKey: key) - case .exists: - return true - case .doesNotExist: - return false - } - } - - func removeObject(forKey key: String) throws { - try storage.removeObject(forKey: key) - files[key] = .doesNotExist - } - - func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws { - try storage.setObject(object, forKey: key, expiry: expiry) - files[key] = .exists - } - - func removeAll() throws { - try storage.removeAll() - files.removeAll() - } - - func removeExpiredObjects() throws { - try storage.removeExpiredObjects() - } - -} - -extension CachingDiskStorage { - private enum FileState { - case unknown, exists, doesNotExist - } -} diff --git a/Tusker/Caching/DiskCache.swift b/Tusker/Caching/DiskCache.swift new file mode 100644 index 00000000..e702728b --- /dev/null +++ b/Tusker/Caching/DiskCache.swift @@ -0,0 +1,143 @@ +// +// DiskCache.swift +// Tusker +// +// Created by Shadowfacts on 1/18/21. +// Copyright © 2021 Shadowfacts. All rights reserved. +// + +import Foundation +import CryptoKit + +struct DiskCacheTransformer { + let toData: (T) throws -> Data + let fromData: (Data) throws -> T +} + +class DiskCache { + + let fileManager: FileManager + let path: String + let defaultExpiry: CacheExpiry + let transformer: DiskCacheTransformer + + private var fileStates = [String: FileState]() + + init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer, fileManager: FileManager = .default) throws { + self.defaultExpiry = defaultExpiry + self.transformer = transformer + self.fileManager = fileManager + + let cacheDir = try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + + self.path = cacheDir.appendingPathComponent(name, isDirectory: true).path + + try createDirectory() + } + + private func createDirectory() throws { + if !fileManager.fileExists(atPath: path) { + try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } + } + + private func makeFileName(for key: String) -> String { + let ext = (key as NSString).pathExtension + + let digest = Insecure.MD5.hash(data: key.data(using: .utf8)!) + let hash = digest.map { String($0, radix: 16) }.joined() + + if ext.isEmpty { + return hash + } else { + return "\(hash).\(ext)" + } + } + + private func makeFilePath(for key: String) -> String { + return (path as NSString).appendingPathComponent(makeFileName(for: key)) + } + + private func fileState(forKey key: String) -> FileState { + return fileStates[key] ?? .unknown + } + + func setObject(_ object: T, forKey key: String) throws { + let data = try transformer.toData(object) + let path = makeFilePath(for: key) + guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else { + throw Error.couldNotCreateFile + } + fileStates[key] = .exists + } + + func removeObject(forKey key: String) throws { + let path = makeFilePath(for: key) + try fileManager.removeItem(atPath: path) + fileStates[key] = .doesNotExist + } + + func existsObject(forKey key: String) throws -> Bool { + switch fileState(forKey: key) { + case .exists: + return true + case .doesNotExist: + return false + case .unknown: + let path = makeFilePath(for: key) + guard fileManager.fileExists(atPath: path) else { + return false + } + let attributes = try fileManager.attributesOfItem(atPath: path) + guard let date = attributes[.modificationDate] as? Date else { + throw Error.malformedFileAttributes + } + return date.timeIntervalSinceNow >= 0 + } + } + + func object(forKey key: String) throws -> T { + let path = makeFilePath(for: key) + let attributes = try fileManager.attributesOfItem(atPath: path) + + guard let date = attributes[.modificationDate] as? Date else { + throw Error.malformedFileAttributes + } + guard date.timeIntervalSinceNow >= 0 else { + try fileManager.removeItem(atPath: path) + fileStates[key] = .doesNotExist + throw Error.expired + } + + let data = try Data(contentsOf: URL(fileURLWithPath: path, isDirectory: false)) + let object = try transformer.fromData(data) + return object + } + + func removeAll() throws { + try fileManager.removeItem(atPath: path) + try createDirectory() + fileStates.removeAll() + } + +} + +extension DiskCache { + enum Error: Swift.Error { + case malformedFileAttributes + case couldNotCreateFile + case expired + } +} + +extension DiskCache { + enum FileState { + case exists, doesNotExist, unknown + } +} + +extension DiskCache where T == Data { + convenience init(name: String, defaultExpiry: CacheExpiry) throws { + try self.init(name: name, defaultExpiry: defaultExpiry, transformer: DiskCacheTransformer(toData: { $0 }, fromData: { $0 })) + } +} diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index 4f0840a3..f8c8a5e8 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -28,7 +28,7 @@ class ImageCache { private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default) - init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry? = nil, desiredSize: CGSize? = nil) { + init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) { // todo: might not always want to use UIScreen.main for this, e.g. Catalyst? let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale)) self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize) diff --git a/Tusker/Caching/ImageDataCache.swift b/Tusker/Caching/ImageDataCache.swift index 201ef63b..ead9ea23 100644 --- a/Tusker/Caching/ImageDataCache.swift +++ b/Tusker/Caching/ImageDataCache.swift @@ -11,19 +11,17 @@ import Cache class ImageDataCache { - private let memory: MemoryStorage - private let disk: CachingDiskStorage? + private let memory: MemoryCache + private let disk: DiskCache? private let storeOriginalDataInMemory: Bool private let desiredPixelSize: CGSize? - init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry?, storeOriginalDataInMemory: Bool, desiredPixelSize: CGSize?) { - let memoryConfig = MemoryConfig(expiry: memoryExpiry) - self.memory = MemoryStorage(config: memoryConfig) + init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry?, storeOriginalDataInMemory: Bool, desiredPixelSize: CGSize?) { + self.memory = MemoryCache(defaultExpiry: memoryExpiry) if let diskExpiry = diskExpiry { - let diskConfig = DiskConfig(name: name, expiry: diskExpiry) - self.disk = try! CachingDiskStorage(config: diskConfig, transformer: TransformerFactory.forData()) + self.disk = try! DiskCache(name: name, defaultExpiry: diskExpiry) } else { self.disk = nil } @@ -33,7 +31,7 @@ class ImageDataCache { } func has(_ key: String) throws -> Bool { - if try memory.existsObject(forKey: key) { + if memory.existsObject(forKey: key) { return true } else if let disk = self.disk, try disk.existsObject(forKey: key) { diff --git a/Tusker/Caching/MemoryCache.swift b/Tusker/Caching/MemoryCache.swift new file mode 100644 index 00000000..2ba805a0 --- /dev/null +++ b/Tusker/Caching/MemoryCache.swift @@ -0,0 +1,69 @@ +// +// MemoryCache.swift +// Tusker +// +// Created by Shadowfacts on 1/18/21. +// Copyright © 2021 Shadowfacts. All rights reserved. +// + +import Foundation + +class MemoryCache { + + private let cache = NSCache() + private let defaultExpiry: CacheExpiry + + init(defaultExpiry: CacheExpiry) { + self.defaultExpiry = defaultExpiry + } + + + func setObject(_ object: T, forKey key: String) { + let entry = Entry(expiresAt: defaultExpiry.date, object: object) + cache.setObject(entry, forKey: key as NSString) + } + + func removeObject(forKey key: String) { + cache.removeObject(forKey: key as NSString) + } + + func existsObject(forKey key: String) -> Bool { + return cache.object(forKey: key as NSString) != nil + } + + func object(forKey key: String) throws -> T { + guard let entry = cache.object(forKey: key as NSString) else { + throw Error.notFound + } + + guard entry.expiresAt.timeIntervalSinceNow >= 0 else { + cache.removeObject(forKey: key as NSString) + throw Error.expired + } + + return entry.object + } + + func removeAll() { + cache.removeAllObjects() + } +} + +extension MemoryCache { + enum Error: Swift.Error { + case notFound + case expired + } +} + +extension MemoryCache { + class Entry { + let expiresAt: Date + let object: T + + init(expiresAt: Date, object: T) { + self.expiresAt = expiresAt + self.object = object + } + } +}