mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-21 10:33:07 -04:00
Merge pull request #1198 from kiwix/1193-change-library-endpoint
Library endpoint changes
This commit is contained in:
commit
ecc9ba1556
@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface OPDSParser : NSObject
|
||||
|
||||
- (nonnull instancetype)init;
|
||||
- (BOOL)parseData:(nonnull NSData *)data NS_REFINED_FOR_SWIFT;
|
||||
- (BOOL)parseData:(nonnull NSData *)data using: (nonnull NSString *)urlHost NS_REFINED_FOR_SWIFT;
|
||||
- (nonnull NSSet *)getZimFileIDs NS_REFINED_FOR_SWIFT;
|
||||
- (nullable ZimFileMetaData *)getZimFileMetaData:(nonnull NSUUID *)identifier NS_REFINED_FOR_SWIFT;
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)parseData:(nonnull NSData *)data {
|
||||
- (BOOL)parseData:(nonnull NSData *)data using: (nonnull NSString *)urlHost {
|
||||
try {
|
||||
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
if (content == nil) {
|
||||
@ -47,7 +47,7 @@
|
||||
}
|
||||
std::shared_ptr<kiwix::Manager> manager = std::make_shared<kiwix::Manager>(self.library);
|
||||
return manager->readOpds([content cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
[@"https://library.kiwix.org" cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
[urlHost cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
} catch (std::exception) {
|
||||
return false;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
protocol Parser {
|
||||
var zimFileIDs: Set<UUID> { get }
|
||||
func parse(data: Data) throws
|
||||
func parse(data: Data, urlHost: String) throws
|
||||
func getMetaData(id: UUID) -> ZimFileMetaData?
|
||||
}
|
||||
|
||||
@ -24,8 +24,8 @@ extension OPDSParser: Parser {
|
||||
__getZimFileIDs() as? Set<UUID> ?? Set<UUID>()
|
||||
}
|
||||
|
||||
func parse(data: Data) throws {
|
||||
if !self.__parseData(data) {
|
||||
func parse(data: Data, urlHost: String) throws {
|
||||
if !self.__parseData(data, using: urlHost.removingSuffix("/")) {
|
||||
throw LibraryRefreshError.parse
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,7 @@ extension OPDSParser: Parser {
|
||||
struct DeletingParser: Parser {
|
||||
let zimFileIDs: Set<UUID> = .init()
|
||||
|
||||
func parse(data: Data) throws {
|
||||
func parse(data: Data, urlHost: String) throws {
|
||||
}
|
||||
|
||||
func getMetaData(id: UUID) -> ZimFileMetaData? {
|
||||
|
@ -21,6 +21,11 @@ extension String {
|
||||
guard hasPrefix(value) else { return self }
|
||||
return String(dropFirst(value.count))
|
||||
}
|
||||
|
||||
func removingSuffix(_ value: String) -> String {
|
||||
guard hasSuffix(value) else { return self }
|
||||
return String(dropLast(value.count))
|
||||
}
|
||||
|
||||
func replacingRegex(
|
||||
matching pattern: String,
|
||||
|
@ -95,4 +95,25 @@ extension URL {
|
||||
components.scheme = "zim"
|
||||
return components.url ?? self
|
||||
}
|
||||
|
||||
/// Remove the defined components one by one if found
|
||||
/// - Parameter pathComponents: eg: /package/details/more can be defined as: ["package", "details", "more"]
|
||||
/// - Returns: the modified url
|
||||
func trim(pathComponents: [String]) -> URL {
|
||||
var result = self
|
||||
for component in pathComponents.reversed() where component == result.lastPathComponent {
|
||||
result = result.deletingLastPathComponent()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Removes everything after ? or &
|
||||
/// - Returns: the modified URL, or the same if it fails to find the components
|
||||
func withoutQueryParams() -> URL {
|
||||
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
|
||||
return self
|
||||
}
|
||||
components.queryItems = nil
|
||||
return components.url ?? self
|
||||
}
|
||||
}
|
||||
|
@ -171,8 +171,9 @@ final class LibraryRefreshViewModelTest: XCTestCase {
|
||||
func testNewZimFileAndProperties() async throws {
|
||||
let zimFileID = UUID()
|
||||
HTTPTestingURLProtocol.handler = { urlProtocol in
|
||||
let responseTestURL = URL(string: "https://response-testing.com/catalog/v2/entries?count=-1")!
|
||||
let response = HTTPURLResponse(
|
||||
url: URL.mock(),
|
||||
url: responseTestURL,
|
||||
statusCode: 200, httpVersion: nil, headerFields: [:]
|
||||
)!
|
||||
let data = self.makeOPDSData(zimFileID: zimFileID).data(using: .utf8)!
|
||||
@ -211,7 +212,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
|
||||
XCTAssertNil(zimFile.faviconData)
|
||||
XCTAssertEqual(
|
||||
zimFile.faviconURL,
|
||||
URL(string: "https://library.kiwix.org/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
|
||||
URL(string: "https://response-testing.com/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
|
||||
)
|
||||
XCTAssertEqual(zimFile.fileDescription, "A selection of the best 50,000 Wikipedia articles")
|
||||
XCTAssertEqual(zimFile.fileID, zimFileID)
|
||||
|
@ -22,7 +22,7 @@ final class OPDSParserTests: XCTestCase {
|
||||
XCTExpectFailure("Requires work in dependency to resolve the issue.")
|
||||
let content = "Invalid OPDS Data"
|
||||
XCTAssertThrowsError(
|
||||
try OPDSParser().parse(data: content.data(using: .utf8)!)
|
||||
try OPDSParser().parse(data: content.data(using: .utf8)!, urlHost: "")
|
||||
)
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ final class OPDSParserTests: XCTestCase {
|
||||
let incompatibleEncodings: [String.Encoding] = [.unicode, .utf16, .utf32]
|
||||
try incompatibleEncodings.forEach { encoding in
|
||||
XCTAssertThrowsError(
|
||||
try OPDSParser().parse(data: content.data(using: encoding)!),
|
||||
try OPDSParser().parse(data: content.data(using: encoding)!, urlHost: ""),
|
||||
"parsing with enconding \(encoding.description) should fail"
|
||||
)
|
||||
}
|
||||
@ -73,10 +73,16 @@ final class OPDSParserTests: XCTestCase {
|
||||
</entry>
|
||||
</feed>
|
||||
"""
|
||||
|
||||
|
||||
// Parse data
|
||||
let responseTestURL = URL(string: "https://resp-test.org/")!
|
||||
let parser = OPDSParser()
|
||||
XCTAssertNoThrow(try parser.parse(data: content.data(using: .utf8)!))
|
||||
XCTAssertNoThrow(
|
||||
try parser.parse(
|
||||
data: content.data(using: .utf8)!,
|
||||
urlHost: responseTestURL.absoluteString
|
||||
)
|
||||
)
|
||||
|
||||
// check one zim file is populated
|
||||
let zimFileID = UUID(uuidString: "1ec90eab-5724-492b-9529-893959520de4")!
|
||||
@ -108,7 +114,7 @@ final class OPDSParserTests: XCTestCase {
|
||||
)
|
||||
XCTAssertEqual(
|
||||
metadata.faviconURL,
|
||||
URL(string: "https://library.kiwix.org/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
|
||||
URL(string: "https://resp-test.org/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
|
||||
)
|
||||
XCTAssertEqual(metadata.flavor, "maxi")
|
||||
}
|
||||
|
@ -43,5 +43,11 @@ final class URLContentPathTests: XCTestCase {
|
||||
"widgets.wp.com/likes/master.html?ver=20240530"
|
||||
])
|
||||
}
|
||||
|
||||
func test_trimming() {
|
||||
let inputURL = URL(string: "https://library.kiwix.org/catalog/v2/entries?count=-1")!
|
||||
let expectedURL = URL(string: "https://library.kiwix.org/")!
|
||||
XCTAssertEqual(inputURL.withoutQueryParams().trim(pathComponents: ["catalog", "v2", "entries"]), expectedURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ final class LibraryViewModel: ObservableObject {
|
||||
private var insertionCount = 0
|
||||
private var deletionCount = 0
|
||||
|
||||
private static let catalogURL = URL(string: "https://library.kiwix.org/catalog/v2/entries?count=-1")!
|
||||
private static let catalogURL = URL(string: "https://opds.library.kiwix.org/v2/entries?count=-1")!
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
@ -131,7 +131,7 @@ final class LibraryViewModel: ObservableObject {
|
||||
process.state = .inProgress
|
||||
|
||||
// refresh library
|
||||
guard let data = try await fetchData() else {
|
||||
guard case (var data, let responseURL)? = try await fetchData() else {
|
||||
// this is the case when we have no new data (304 http)
|
||||
// but we still need to refresh the memory only stored
|
||||
// zimfile categories to languages dictionary
|
||||
@ -156,7 +156,7 @@ final class LibraryViewModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
}
|
||||
let parser = try await parse(data: data)
|
||||
let parser = try await parse(data: data, urlHost: responseURL)
|
||||
// delete all old ISO Lang Code entries if needed, by passing in an empty parser
|
||||
if defaults[.libraryUsingOldISOLangCodes] {
|
||||
try await process(parser: DeletingParser())
|
||||
@ -252,7 +252,7 @@ final class LibraryViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchData() async throws -> Data? {
|
||||
private func fetchData() async throws -> (Data, URL)? {
|
||||
do {
|
||||
var request = URLRequest(url: Self.catalogURL, timeoutInterval: 20)
|
||||
request.allHTTPHeaderFields = ["If-None-Match": defaults[.libraryETag]]
|
||||
@ -260,19 +260,20 @@ final class LibraryViewModel: ObservableObject {
|
||||
guard let response = response as? HTTPURLResponse else { return nil }
|
||||
switch response.statusCode {
|
||||
case 200:
|
||||
let responseURL = response.url ?? Self.catalogURL
|
||||
if let eTag = response.allHeaderFields["Etag"] as? String {
|
||||
defaults[.libraryETag] = eTag
|
||||
}
|
||||
// OK to process further
|
||||
os_log("Retrieved OPDS Data, size: %llu bytes", log: Log.OPDS, type: .info, data.count)
|
||||
return data
|
||||
return (data, responseURL)
|
||||
case 304:
|
||||
return nil // already downloaded
|
||||
default:
|
||||
throw LibraryRefreshError.retrieve(description: "HTTP Status \(response.statusCode).")
|
||||
}
|
||||
} catch {
|
||||
os_log("Error retrieving OPDS Data: %s", log: Log.OPDS, type: .error)
|
||||
os_log("Error retrieving OPDS Data: %s", log: Log.OPDS, type: .error, error.localizedDescription)
|
||||
if let error = error as? LibraryRefreshError {
|
||||
throw error
|
||||
} else {
|
||||
@ -281,11 +282,15 @@ final class LibraryViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func parse(data: Data) async throws -> OPDSParser {
|
||||
private func parse(data: Data, urlHost: URL) async throws -> OPDSParser {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let parser = OPDSParser()
|
||||
do {
|
||||
try parser.parse(data: data)
|
||||
let urlHostString = urlHost
|
||||
.withoutQueryParams()
|
||||
.trim(pathComponents: ["catalog", "v2", "entries"])
|
||||
.absoluteString
|
||||
try parser.parse(data: data, urlHost: urlHostString)
|
||||
continuation.resume(returning: parser)
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
|
@ -63,7 +63,7 @@ struct Favicon_Previews: PreviewProvider {
|
||||
category: .wikipedia,
|
||||
imageData: nil,
|
||||
imageURL: URL(
|
||||
string: "https://library.kiwix.org/meta?name=favicon&content=wikipedia_en_climate_change_maxi_2021-12"
|
||||
string: "https://opds.library.kiwix.org/v2/illustration/e82e6816-a2dc-a7f0-2d15-58d24709db93/?size=48"
|
||||
)!
|
||||
).frame(width: 200, height: 200).previewLayout(.sizeThatFits)
|
||||
Favicon(
|
||||
|
Loading…
x
Reference in New Issue
Block a user