Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Modules/Sources/WordPressCore/ApiCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ import WordPressApiCache
import WordPressShared

extension WordPressApiCache {
public static func openExistingOnDiskCache() throws -> WordPressApiCache? {
let cacheURL = URL.libraryDirectory.appending(path: "app.sqlite")
return try openExistingOnDiskCache(file: cacheURL)
}

static func openExistingOnDiskCache(file: URL) throws -> WordPressApiCache? {
guard FileManager.default.fileExists(atPath: file.path) else {
return nil
}
let cache = try WordPressApiCache(url: file)
_ = try cache.performMigrations()
return cache
}

static func bootstrap() -> WordPressApiCache {
let instance: WordPressApiCache = .onDiskCache() ?? .memoryCache()
instance.startListeningForUpdates()
Expand Down
56 changes: 56 additions & 0 deletions Modules/Tests/WordPressCoreTests/ApiCacheTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import Testing
import WordPressApiCache
@testable import WordPressCore

@Suite
struct ApiCacheTests {
@Test
func missingFileReturnsNilWithoutCreatingFile() throws {
let url = makeTemporaryCacheURL()
defer { try? FileManager.default.removeItem(at: url) }

let cache = try WordPressApiCache.openExistingOnDiskCache(file: url)

#expect(cache == nil)
#expect(!FileManager.default.fileExists(atPath: url.path))
}

@Test
func existingMigratedFileReturnsCache() throws {
let url = makeTemporaryCacheURL()
defer { try? FileManager.default.removeItem(at: url) }
do {
let cache = try WordPressApiCache(url: url)
_ = try cache.performMigrations()
}

let cache = try WordPressApiCache.openExistingOnDiskCache(file: url)

#expect(cache != nil)
#expect(FileManager.default.fileExists(atPath: url.path))
}

@Test
func invalidFileThrowsWithoutChangingFile() throws {
let url = makeTemporaryCacheURL()
defer { try? FileManager.default.removeItem(at: url) }
let originalData = Data("invalid sqlite database".utf8)
try originalData.write(to: url)
var thrownError: (any Error)?

do {
_ = try WordPressApiCache.openExistingOnDiskCache(file: url)
} catch {
thrownError = error
}

#expect(thrownError != nil)
#expect(try Data(contentsOf: url) == originalData)
}

private func makeTemporaryCacheURL() -> URL {
FileManager.default.temporaryDirectory
.appending(path: "WordPressApiCacheTests-\(UUID().uuidString).sqlite")
}
}
49 changes: 49 additions & 0 deletions WordPress/Classes/Networking/WordPressApiCacheCleanup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation
import WordPressAPI
import WordPressAPIInternal
import WordPressApiCache
import WordPressCore
import WordPressData

enum WordPressApiCacheCleanup {
static func removeCachedData(for blog: Blog) {
removeCachedData(for: [blog])
}

static func removeCachedData(for blogs: Set<Blog>) {
removeCachedData(for: Array(blogs))
}

private static func removeCachedData(for blogs: [Blog]) {
let cache: WordPressApiCache
do {
guard let existingCache = try WordPressApiCache.openExistingOnDiskCache() else {
return
}
cache = existingCache
} catch {
Loggers.app.error("Failed to open WordPress API cache for cleanup: \(error)")
return
}

for blog in blogs {
do {
// `restApiRootURL` remains after an Atomic site's application
// password is removed, so it records that direct transport was
// configured without requiring the credential itself.
let isDirect = !blog.isHostedAtWPcom || (blog.isAtomic && blog.restApiRootURL != nil)
if isDirect {
_ = try cache.removeSelfHostedSite(url: blog.getUrl())
} else if let siteID = blog.dotComID?.intValue {
_ = try cache.removeWordpressComSite(siteId: WpComSiteId(siteID))
} else {
Loggers.app.error(
"Skipped WordPress API cache cleanup because the site's durable metadata is incomplete"
)
}
} catch {
Loggers.app.error("Failed to remove WordPress API cached data: \(error)")
}
}
}
}
2 changes: 2 additions & 0 deletions WordPress/Classes/Services/AccountService+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ extension AccountService {
WordPressClientFactory.shared.evictInstance(for: TaggedManagedObjectID($0))
}

WordPressApiCacheCleanup.removeCachedData(for: account.blogs ?? [])

let objectID = TaggedManagedObjectID(account)
coreDataStack.performAndSave { context in
do {
Expand Down
5 changes: 5 additions & 0 deletions WordPress/Classes/Services/BlogService+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ extension BlogService {
WordPressClientFactory.shared.evictInstance(for: TaggedManagedObjectID(blog))
}

@objc(removeWordPressApiCachedDataForBlog:)
public func removeWordPressApiCachedData(for blog: Blog) {
WordPressApiCacheCleanup.removeCachedData(for: blog)
}

@objc public func unscheduleBloggingReminders(for blog: Blog) {
do {
let scheduler = try ReminderScheduleCoordinator()
Expand Down
1 change: 1 addition & 0 deletions WordPress/Classes/Services/BlogService.m
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ - (void)removeBlog:(Blog *)blog
[blog.xmlrpcApi invalidateAndCancelTasks];
[self unscheduleBloggingRemindersFor:blog];
[self evictWordPressClientForBlog:blog];
[self removeWordPressApiCachedDataForBlog:blog];

WPAccount *account = blog.account;

Expand Down