mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-24 04:03:03 -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
|
@interface OPDSParser : NSObject
|
||||||
|
|
||||||
- (nonnull instancetype)init;
|
- (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;
|
- (nonnull NSSet *)getZimFileIDs NS_REFINED_FOR_SWIFT;
|
||||||
- (nullable ZimFileMetaData *)getZimFileMetaData:(nonnull NSUUID *)identifier NS_REFINED_FOR_SWIFT;
|
- (nullable ZimFileMetaData *)getZimFileMetaData:(nonnull NSUUID *)identifier NS_REFINED_FOR_SWIFT;
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)parseData:(nonnull NSData *)data {
|
- (BOOL)parseData:(nonnull NSData *)data using: (nonnull NSString *)urlHost {
|
||||||
try {
|
try {
|
||||||
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||||
if (content == nil) {
|
if (content == nil) {
|
||||||
@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
std::shared_ptr<kiwix::Manager> manager = std::make_shared<kiwix::Manager>(self.library);
|
std::shared_ptr<kiwix::Manager> manager = std::make_shared<kiwix::Manager>(self.library);
|
||||||
return manager->readOpds([content cStringUsingEncoding:NSUTF8StringEncoding],
|
return manager->readOpds([content cStringUsingEncoding:NSUTF8StringEncoding],
|
||||||
[@"https://library.kiwix.org" cStringUsingEncoding:NSUTF8StringEncoding]);
|
[urlHost cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||||
} catch (std::exception) {
|
} catch (std::exception) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
protocol Parser {
|
protocol Parser {
|
||||||
var zimFileIDs: Set<UUID> { get }
|
var zimFileIDs: Set<UUID> { get }
|
||||||
func parse(data: Data) throws
|
func parse(data: Data, urlHost: String) throws
|
||||||
func getMetaData(id: UUID) -> ZimFileMetaData?
|
func getMetaData(id: UUID) -> ZimFileMetaData?
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ extension OPDSParser: Parser {
|
|||||||
__getZimFileIDs() as? Set<UUID> ?? Set<UUID>()
|
__getZimFileIDs() as? Set<UUID> ?? Set<UUID>()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(data: Data) throws {
|
func parse(data: Data, urlHost: String) throws {
|
||||||
if !self.__parseData(data) {
|
if !self.__parseData(data, using: urlHost.removingSuffix("/")) {
|
||||||
throw LibraryRefreshError.parse
|
throw LibraryRefreshError.parse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ extension OPDSParser: Parser {
|
|||||||
struct DeletingParser: Parser {
|
struct DeletingParser: Parser {
|
||||||
let zimFileIDs: Set<UUID> = .init()
|
let zimFileIDs: Set<UUID> = .init()
|
||||||
|
|
||||||
func parse(data: Data) throws {
|
func parse(data: Data, urlHost: String) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMetaData(id: UUID) -> ZimFileMetaData? {
|
func getMetaData(id: UUID) -> ZimFileMetaData? {
|
||||||
|
@ -21,6 +21,11 @@ extension String {
|
|||||||
guard hasPrefix(value) else { return self }
|
guard hasPrefix(value) else { return self }
|
||||||
return String(dropFirst(value.count))
|
return String(dropFirst(value.count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removingSuffix(_ value: String) -> String {
|
||||||
|
guard hasSuffix(value) else { return self }
|
||||||
|
return String(dropLast(value.count))
|
||||||
|
}
|
||||||
|
|
||||||
func replacingRegex(
|
func replacingRegex(
|
||||||
matching pattern: String,
|
matching pattern: String,
|
||||||
|
@ -95,4 +95,25 @@ extension URL {
|
|||||||
components.scheme = "zim"
|
components.scheme = "zim"
|
||||||
return components.url ?? self
|
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 {
|
func testNewZimFileAndProperties() async throws {
|
||||||
let zimFileID = UUID()
|
let zimFileID = UUID()
|
||||||
HTTPTestingURLProtocol.handler = { urlProtocol in
|
HTTPTestingURLProtocol.handler = { urlProtocol in
|
||||||
|
let responseTestURL = URL(string: "https://response-testing.com/catalog/v2/entries?count=-1")!
|
||||||
let response = HTTPURLResponse(
|
let response = HTTPURLResponse(
|
||||||
url: URL.mock(),
|
url: responseTestURL,
|
||||||
statusCode: 200, httpVersion: nil, headerFields: [:]
|
statusCode: 200, httpVersion: nil, headerFields: [:]
|
||||||
)!
|
)!
|
||||||
let data = self.makeOPDSData(zimFileID: zimFileID).data(using: .utf8)!
|
let data = self.makeOPDSData(zimFileID: zimFileID).data(using: .utf8)!
|
||||||
@ -211,7 +212,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
|
|||||||
XCTAssertNil(zimFile.faviconData)
|
XCTAssertNil(zimFile.faviconData)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
zimFile.faviconURL,
|
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.fileDescription, "A selection of the best 50,000 Wikipedia articles")
|
||||||
XCTAssertEqual(zimFile.fileID, zimFileID)
|
XCTAssertEqual(zimFile.fileID, zimFileID)
|
||||||
|
@ -22,7 +22,7 @@ final class OPDSParserTests: XCTestCase {
|
|||||||
XCTExpectFailure("Requires work in dependency to resolve the issue.")
|
XCTExpectFailure("Requires work in dependency to resolve the issue.")
|
||||||
let content = "Invalid OPDS Data"
|
let content = "Invalid OPDS Data"
|
||||||
XCTAssertThrowsError(
|
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]
|
let incompatibleEncodings: [String.Encoding] = [.unicode, .utf16, .utf32]
|
||||||
try incompatibleEncodings.forEach { encoding in
|
try incompatibleEncodings.forEach { encoding in
|
||||||
XCTAssertThrowsError(
|
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"
|
"parsing with enconding \(encoding.description) should fail"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -73,10 +73,16 @@ final class OPDSParserTests: XCTestCase {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>
|
</feed>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
// Parse data
|
// Parse data
|
||||||
|
let responseTestURL = URL(string: "https://resp-test.org/")!
|
||||||
let parser = OPDSParser()
|
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
|
// check one zim file is populated
|
||||||
let zimFileID = UUID(uuidString: "1ec90eab-5724-492b-9529-893959520de4")!
|
let zimFileID = UUID(uuidString: "1ec90eab-5724-492b-9529-893959520de4")!
|
||||||
@ -108,7 +114,7 @@ final class OPDSParserTests: XCTestCase {
|
|||||||
)
|
)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
metadata.faviconURL,
|
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")
|
XCTAssertEqual(metadata.flavor, "maxi")
|
||||||
}
|
}
|
||||||
|
@ -43,5 +43,11 @@ final class URLContentPathTests: XCTestCase {
|
|||||||
"widgets.wp.com/likes/master.html?ver=20240530"
|
"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 insertionCount = 0
|
||||||
private var deletionCount = 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
|
@MainActor
|
||||||
init(
|
init(
|
||||||
@ -131,7 +131,7 @@ final class LibraryViewModel: ObservableObject {
|
|||||||
process.state = .inProgress
|
process.state = .inProgress
|
||||||
|
|
||||||
// refresh library
|
// 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)
|
// this is the case when we have no new data (304 http)
|
||||||
// but we still need to refresh the memory only stored
|
// but we still need to refresh the memory only stored
|
||||||
// zimfile categories to languages dictionary
|
// zimfile categories to languages dictionary
|
||||||
@ -156,7 +156,7 @@ final class LibraryViewModel: ObservableObject {
|
|||||||
return
|
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
|
// delete all old ISO Lang Code entries if needed, by passing in an empty parser
|
||||||
if defaults[.libraryUsingOldISOLangCodes] {
|
if defaults[.libraryUsingOldISOLangCodes] {
|
||||||
try await process(parser: DeletingParser())
|
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 {
|
do {
|
||||||
var request = URLRequest(url: Self.catalogURL, timeoutInterval: 20)
|
var request = URLRequest(url: Self.catalogURL, timeoutInterval: 20)
|
||||||
request.allHTTPHeaderFields = ["If-None-Match": defaults[.libraryETag]]
|
request.allHTTPHeaderFields = ["If-None-Match": defaults[.libraryETag]]
|
||||||
@ -260,19 +260,20 @@ final class LibraryViewModel: ObservableObject {
|
|||||||
guard let response = response as? HTTPURLResponse else { return nil }
|
guard let response = response as? HTTPURLResponse else { return nil }
|
||||||
switch response.statusCode {
|
switch response.statusCode {
|
||||||
case 200:
|
case 200:
|
||||||
|
let responseURL = response.url ?? Self.catalogURL
|
||||||
if let eTag = response.allHeaderFields["Etag"] as? String {
|
if let eTag = response.allHeaderFields["Etag"] as? String {
|
||||||
defaults[.libraryETag] = eTag
|
defaults[.libraryETag] = eTag
|
||||||
}
|
}
|
||||||
// OK to process further
|
// OK to process further
|
||||||
os_log("Retrieved OPDS Data, size: %llu bytes", log: Log.OPDS, type: .info, data.count)
|
os_log("Retrieved OPDS Data, size: %llu bytes", log: Log.OPDS, type: .info, data.count)
|
||||||
return data
|
return (data, responseURL)
|
||||||
case 304:
|
case 304:
|
||||||
return nil // already downloaded
|
return nil // already downloaded
|
||||||
default:
|
default:
|
||||||
throw LibraryRefreshError.retrieve(description: "HTTP Status \(response.statusCode).")
|
throw LibraryRefreshError.retrieve(description: "HTTP Status \(response.statusCode).")
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 {
|
if let error = error as? LibraryRefreshError {
|
||||||
throw error
|
throw error
|
||||||
} else {
|
} 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
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
let parser = OPDSParser()
|
let parser = OPDSParser()
|
||||||
do {
|
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)
|
continuation.resume(returning: parser)
|
||||||
} catch {
|
} catch {
|
||||||
continuation.resume(throwing: error)
|
continuation.resume(throwing: error)
|
||||||
|
@ -63,7 +63,7 @@ struct Favicon_Previews: PreviewProvider {
|
|||||||
category: .wikipedia,
|
category: .wikipedia,
|
||||||
imageData: nil,
|
imageData: nil,
|
||||||
imageURL: URL(
|
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)
|
).frame(width: 200, height: 200).previewLayout(.sizeThatFits)
|
||||||
Favicon(
|
Favicon(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user