From 170db84f19023cd1d637d5388d671d1e454ddaae Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 3 Oct 2021 22:19:28 +0900 Subject: [PATCH] Use a stable default cache key for local files --- .../ImageSource/ImageDataProvider.swift | 30 ++++++++++++++++++- Sources/General/ImageSource/Resource.swift | 2 +- .../ImageDataProviderTests.swift | 23 ++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Sources/General/ImageSource/ImageDataProvider.swift b/Sources/General/ImageSource/ImageDataProvider.swift index cbbc7300a..b52998751 100644 --- a/Sources/General/ImageSource/ImageDataProvider.swift +++ b/Sources/General/ImageSource/ImageDataProvider.swift @@ -88,7 +88,7 @@ public struct LocalFileImageDataProvider: ImageDataProvider { loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated)) ) { self.fileURL = fileURL - self.cacheKey = cacheKey ?? fileURL.absoluteString + self.cacheKey = cacheKey ?? fileURL.localFileCacheKey self.loadingQueue = loadingQueue } @@ -109,6 +109,34 @@ public struct LocalFileImageDataProvider: ImageDataProvider { } } +extension URL { + static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey" + + // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk, + // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for + // the same image in bundle might be different. + // + // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key + // for the image under the same path inside the bundle. + // + // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825) + var localFileCacheKey: String { + var validComponents: [String] = [] + for part in pathComponents.reversed() { + validComponents.append(part) + if part.hasSuffix(".app") || part.hasSuffix(".appex") { + break + } + } + let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))" + if let q = query { + return "\(fixedPath)?\(q)" + } else { + return fixedPath + } + } +} + /// Represents an image data provider for loading image from a given Base64 encoded string. public struct Base64ImageDataProvider: ImageDataProvider { diff --git a/Sources/General/ImageSource/Resource.swift b/Sources/General/ImageSource/Resource.swift index efd174b24..42950550a 100644 --- a/Sources/General/ImageSource/Resource.swift +++ b/Sources/General/ImageSource/Resource.swift @@ -45,7 +45,7 @@ extension Resource { /// `.network` is returned. public func convertToSource(overrideCacheKey: String? = nil) -> Source { return downloadURL.isFileURL ? - .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: overrideCacheKey ?? cacheKey)) : + .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: overrideCacheKey ?? downloadURL.localFileCacheKey)) : .network(ImageResource(downloadURL: downloadURL, cacheKey: overrideCacheKey ?? cacheKey)) } } diff --git a/Tests/KingfisherTests/ImageDataProviderTests.swift b/Tests/KingfisherTests/ImageDataProviderTests.swift index 29d838b45..767308c4d 100644 --- a/Tests/KingfisherTests/ImageDataProviderTests.swift +++ b/Tests/KingfisherTests/ImageDataProviderTests.swift @@ -25,7 +25,7 @@ // THE SOFTWARE. import XCTest -import Kingfisher +@testable import Kingfisher class ImageDataProviderTests: XCTestCase { @@ -36,7 +36,7 @@ class ImageDataProviderTests: XCTestCase { try! testImageData.write(to: fileURL) let provider = LocalFileImageDataProvider(fileURL: fileURL) - XCTAssertEqual(provider.cacheKey, fileURL.absoluteString) + XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(provider.fileURL, fileURL) let exp = expectation(description: #function) @@ -56,7 +56,7 @@ class ImageDataProviderTests: XCTestCase { try! testImageData.write(to: fileURL) let provider = LocalFileImageDataProvider(fileURL: fileURL, loadingQueue: .mainCurrentOrAsync) - XCTAssertEqual(provider.cacheKey, fileURL.absoluteString) + XCTAssertEqual(provider.cacheKey, fileURL.localFileCacheKey) XCTAssertEqual(provider.fileURL, fileURL) var called = false @@ -69,6 +69,23 @@ class ImageDataProviderTests: XCTestCase { XCTAssertTrue(called) } + func testLocalFileCacheKey() { + let url1 = URL(string: "file:///Users/onevcat/Library/Developer/CoreSimulator/Devices/ABC/data/Containers/Bundle/Application/DEF/Kingfisher-Demo.app/images/kingfisher-1.jpg")! + XCTAssertEqual(url1.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg") + + let url2 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.app/images/kingfisher-1.jpg")! + XCTAssertEqual(url2.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg") + + let url3 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.app/images/kingfisher-1.jpg?foo=bar")! + XCTAssertEqual(url3.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.app/images/kingfisher-1.jpg?foo=bar") + + let url4 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.appex/images/kingfisher-1.jpg")! + XCTAssertEqual(url4.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)/Kingfisher-Demo.appex/images/kingfisher-1.jpg") + + let url5 = URL(string: "file:///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.other/images/kingfisher-1.jpg")! + XCTAssertEqual(url5.localFileCacheKey, "\(URL.localFileCacheKeyPrefix)///private/var/containers/Bundle/Application/ABC/Kingfisher-Demo.other/images/kingfisher-1.jpg") + } + func testBase64ImageDataProvider() { let base64String = testImageData.base64EncodedString() let provider = Base64ImageDataProvider(base64String: base64String, cacheKey: "123")