Skip to content

Commit

Permalink
Port API changes from the Kotlin toolkit (#463)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu committed Aug 20, 2024
1 parent 7f97ee4 commit a014d59
Show file tree
Hide file tree
Showing 298 changed files with 22,964 additions and 8,063 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ env:
platform: ${{ 'iOS Simulator' }}
device: ${{ 'iPhone 15' }}
commit_sha: ${{ github.sha }}
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer

jobs:
build:
name: Build
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
env:
scheme: ${{ 'Readium-Package' }}
Expand Down Expand Up @@ -42,7 +42,7 @@ jobs:
lint:
name: Lint
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
env:
scripts: ${{ 'Sources/Navigator/EPUB/Scripts' }}
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:

int-dev:
name: Integration (Local)
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
defaults:
run:
Expand All @@ -98,7 +98,7 @@ jobs:
int-spm:
name: Integration (Swift Package Manager)
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
defaults:
run:
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
int-carthage:
name: Integration (Carthage)
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
defaults:
run:
Expand Down Expand Up @@ -157,7 +157,7 @@ jobs:
int-cocoapods:
name: Integration (CocoaPods)
if: github.event_name == 'push'
runs-on: macos-13
runs-on: macos-14
defaults:
run:
working-directory: TestApp
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ All notable changes to this project will be documented in this file. Take a look

#### Shared

* A new `Format` type was introduced to augment `MediaType` with more precise information about the format specifications of an `Asset`.
* `Fetcher` was replaced with a simpler `Container` type.
* `PublicationAsset` was replaced by `Asset`, which contains a `Format` and access to the underlying `Container` or `Resource`.
* The `ResourceError` hierarchy was revamped and simplified (see `ReadError`). Now it is your responsibility to provide a localized user message for each error case.
* The `Link` property key for archive-based publication assets (e.g. an EPUB/ZIP) is now `https://readium.org/webpub-manifest/properties#archive` instead of `archive`.
* The API of `HTTPServer` slightly changed to be more future-proof.

#### Streamer

* The `Streamer` object was deprecated in favor of smaller segregated APIs: `AssetRetriever` and `PublicationOpener`.

#### Navigator

* EPUB: The `scroll` preference is now forced to `true` when rendering vertical text (e.g. CJK vertical). [See this discussion for the rationale](https://github.com/readium/swift-toolkit/discussions/370).
Expand Down
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ github "dexman/Minizip" ~> 1.4.0
github "krzyzanowskim/CryptoSwift" ~> 1.8.0
github "ra1028/DifferenceKit" ~> 1.3.0
github "readium/GCDWebServer" ~> 4.0.0
github "scinfu/SwiftSoup" ~> 2.7.0
# There's a regression with 2.7.4 in SwiftSoup, because they used iOS 13 APIs without bumping the deployment target.
github "scinfu/SwiftSoup" == 2.7.1
github "stephencelis/SQLite.swift" ~> 0.15.0
github "weichsel/ZIPFoundation" ~> 0.9.0
5 changes: 2 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ let package = Package(
path: "Sources/Shared",
exclude: [
// Support for ZIPFoundation is not yet achieved.
"Toolkit/Archive/ZIPFoundation.swift",
"Toolkit/ZIP/ZIPFoundation.swift",
],
resources: [
.process("Resources"),
Expand All @@ -63,7 +63,6 @@ let package = Package(
dependencies: [
"CryptoSwift",
"Fuzi",
.product(name: "ReadiumGCDWebServer", package: "GCDWebServer"),
"Zip",
"ReadiumShared",
],
Expand Down Expand Up @@ -134,7 +133,7 @@ let package = Package(
]
),
// These tests require a R2LCPClient.framework to run.
// FIXME: Find a solution to run the tests with GitHub action.
// TODO: Find a solution to run the tests with GitHub action.
// .testTarget(
// name: "ReadiumLCPTests",
// dependencies: ["ReadiumLCP"],
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ This toolkit is a modular project, which follows the [Readium Architecture](http

<!-- https://swiftversion.net/ -->

| Readium | iOS | Swift compiler | Xcode |
|-----------|------|----------------|--------|
| `develop` | 13.0 | 5.9 | 15.0.1 |
| 3.0.0 | 13.0 | 5.9 | 15.0.1 |
| 2.5.1 | 11.0 | 5.6.1 | 13.4 |
| 2.5.0 | 10.0 | 5.6.1 | 13.4 |
| 2.4.0 | 10.0 | 5.3.2 | 12.4 |
| Readium | iOS | Swift compiler | Xcode |
|-----------|------|----------------|-------|
| `develop` | 13.0 | 5.10 | 15.4 |
| 3.0.0 | 13.0 | 5.10 | 15.4 |
| 2.5.1 | 11.0 | 5.6.1 | 13.4 |
| 2.5.0 | 10.0 | 5.6.1 | 13.4 |
| 2.4.0 | 10.0 | 5.3.2 | 12.4 |

## Using Readium

Expand Down
141 changes: 94 additions & 47 deletions Sources/Adapters/GCDWebServer/GCDHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import Foundation
import ReadiumGCDWebServer
import ReadiumInternal
import ReadiumShared
import UIKit

Expand All @@ -19,7 +20,8 @@ public enum GCDHTTPServerError: Error {
/// Implementation of `HTTPServer` using ReadiumGCDWebServer under the hood.
public class GCDHTTPServer: HTTPServer, Loggable {
/// Shared instance of the HTTP server.
public static let shared = GCDHTTPServer()
@available(*, unavailable, message: "Create your own shared instance")
public static var shared: GCDHTTPServer { fatalError() }

/// The actual underlying HTTP server instance.
private let server = ReadiumGCDWebServer()
Expand All @@ -30,6 +32,8 @@ public class GCDHTTPServer: HTTPServer, Loggable {
/// Mapping between endpoints and resource transformers.
private var transformers: [HTTPURL: [ResourceTransformer]] = [:]

private let assetRetriever: AssetRetriever

private enum State {
case stopped
case started(port: UInt, baseURL: HTTPURL)
Expand All @@ -47,7 +51,12 @@ public class GCDHTTPServer: HTTPServer, Loggable {
/// Creates a new instance of the HTTP server.
///
/// - Parameter logLevel: See `ReadiumGCDWebServer.setLogLevel`.
public init(logLevel: Int = 3) {
public init(
assetRetriever: AssetRetriever,
logLevel: Int = 3
) {
self.assetRetriever = assetRetriever

ReadiumGCDWebServer.setLogLevel(Int32(logLevel))

NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
Expand Down Expand Up @@ -85,31 +94,40 @@ public class GCDHTTPServer: HTTPServer, Loggable {
}

private func handle(request: ReadiumGCDWebServerRequest, completion: @escaping ReadiumGCDWebServerCompletionBlock) {
responseResource(for: request) { httpServerRequest, resource, failureHandler in
let response: ReadiumGCDWebServerResponse
switch resource.length {
case let .success(length):
response = ResourceResponse(
resource: resource,
length: length,
range: request.hasByteRange() ? request.byteRange : nil
)
case let .failure(error):
self.log(.error, error)
failureHandler?(httpServerRequest, error)
response = ReadiumGCDWebServerErrorResponse(
statusCode: error.httpStatusCode,
error: error
)
}
responseResource(for: request) { httpServerRequest, httpServerResponse, failureHandler in
Task {
let response: ReadiumGCDWebServerResponse
let resource = httpServerResponse.resource

func fail(_ error: ReadError) -> ReadiumGCDWebServerResponse {
self.log(.error, error)
failureHandler?(httpServerRequest, error)
return ReadiumGCDWebServerErrorResponse(
statusCode: 500,
error: error
)
}

switch await resource.length() {
case let .success(length):
response = await ResourceResponse(
resource: httpServerResponse.resource,
length: length,
range: request.hasByteRange() ? request.byteRange : nil,
mediaType: httpServerResponse.mediaType(using: self.assetRetriever)
)
case let .failure(error):
response = fail(error)
}

completion(response) // goes back to ReadiumGCDWebServerConnection.m
completion(response) // goes back to ReadiumGCDWebServerConnection.m
}
}
}

private func responseResource(
for request: ReadiumGCDWebServerRequest,
completion: @escaping (HTTPServerRequest, Resource, HTTPRequestHandler.OnFailure?) -> Void
completion: @escaping (HTTPServerRequest, HTTPServerResponse, HTTPRequestHandler.OnFailure?) -> Void
) {
let completion = { request, resource, failureHandler in
// Escape the queue to avoid deadlocks if something is using the
Expand All @@ -124,52 +142,40 @@ public class GCDHTTPServer: HTTPServer, Loggable {
fatalError("Expected an HTTP URL")
}

func transform(resource: Resource, at endpoint: HTTPURL) -> Resource {
func transform(resource: Resource, request: HTTPServerRequest, at endpoint: HTTPURL) -> Resource {
guard let transformers = transformers[endpoint], !transformers.isEmpty else {
return resource
}
let href = request.href?.anyURL ?? request.url.anyURL
var resource = resource
for transformer in transformers {
resource = transformer(resource)
resource = transformer(href, resource)
}
return resource
}

let pathWithoutAnchor = url.removingQuery().removingFragment()

for (endpoint, handler) in handlers {
let request: HTTPServerRequest
if endpoint.isEquivalentTo(pathWithoutAnchor) {
let request = HTTPServerRequest(url: url, href: nil)
let resource = handler.onRequest(request)
completion(
request,
transform(resource: resource, at: endpoint),
handler.onFailure
)
return

request = HTTPServerRequest(url: url, href: nil)
} else if let href = endpoint.relativize(url) {
let request = HTTPServerRequest(
url: url,
href: href
)
let resource = handler.onRequest(request)
completion(
request,
transform(resource: resource, at: endpoint),
handler.onFailure
)
return
request = HTTPServerRequest(url: url, href: href)
} else {
continue
}

var response = handler.onRequest(request)
response.resource = transform(resource: response.resource, request: request, at: endpoint)
completion(request, response, handler.onFailure)
return
}

log(.warning, "Resource not found for request \(request)")
completion(
HTTPServerRequest(url: url, href: nil),
FailureResource(
link: Link(href: request.url.absoluteString),
error: .notFound(nil)
),
HTTPServerResponse(error: .notFound),
nil
)
}
Expand Down Expand Up @@ -320,3 +326,44 @@ public class GCDHTTPServer: HTTPServer, Loggable {
return true
}
}

private extension Resource {
func length() async -> ReadResult<UInt64> {
await estimatedLength()
.flatMap { length in
if let length = length {
return .success(length)
} else {
return await read().map { UInt64($0.count) }
}
}
}
}

private extension HTTPServerResponse {
func mediaType(using assetRetriever: AssetRetriever) async -> MediaType {
if let mediaType = mediaType {
return mediaType
}

if let properties = try? await resource.properties().get() {
if let mediaType = properties.mediaType {
return mediaType
}
if
let filename = properties.filename,
let uti = UTI.findFrom(mediaTypes: [], fileExtensions: [URL(fileURLWithPath: filename).pathExtension]),
let type = uti.preferredTag(withClass: .mediaType),
let mediaType = MediaType(type)
{
return mediaType
}
}

if let mediaType = try? await assetRetriever.sniffFormat(of: resource).get().mediaType {
return mediaType
}

return .binary
}
}
Loading

0 comments on commit a014d59

Please sign in to comment.