diff --git a/r2-shared-swift.xcodeproj/project.pbxproj b/r2-shared-swift.xcodeproj/project.pbxproj index 0ecd3a7b..7e45b672 100644 --- a/r2-shared-swift.xcodeproj/project.pbxproj +++ b/r2-shared-swift.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ CA2AE322221C1DCB008BD18F /* Loggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2AE31F221C1DCB008BD18F /* Loggable.swift */; }; CA40C07A21FF25F80069A50E /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA40C07921FF25F80069A50E /* JSON.swift */; }; CA50B86E22B2A1CF003AFF24 /* R2LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50B86D22B2A1CF003AFF24 /* R2LocalizedString.swift */; }; + CA94291622BCD08C00305CDB /* ResourcesServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA94291522BCD08B00305CDB /* ResourcesServer.swift */; }; CA9A40D1221B0AA200531EA1 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9A40D0221B0AA200531EA1 /* Either.swift */; }; CA9E6BA12239823300ECF6E4 /* WP+Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9E6BA02239823300ECF6E4 /* WP+Deprecated.swift */; }; CA9E6BA4223A657900ECF6E4 /* JSONEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9E6BA3223A657900ECF6E4 /* JSONEquatable.swift */; }; @@ -67,6 +68,7 @@ CACD75F32236B0A5004F20CA /* PublicationCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACD75F22236B0A5004F20CA /* PublicationCollection.swift */; }; CACD75F52236B2AF004F20CA /* PublicationCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACD75F42236B2AF004F20CA /* PublicationCollectionTests.swift */; }; CAD178AD22B3A75C004E6812 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CAD178AF22B3A75C004E6812 /* Localizable.strings */; }; + CADD69E222C3B17500A4CADF /* DocumentTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADD69E122C3B17500A4CADF /* DocumentTypes.swift */; }; CAF4EAE42237ABC700A17DA1 /* MediaOverlayNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E7D4041F4EC69100DF166D /* MediaOverlayNode.swift */; }; CAF4EAE52237ABC700A17DA1 /* MediaOverlays.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E7D4051F4EC69100DF166D /* MediaOverlays.swift */; }; CAF4EAE72237AD6000A17DA1 /* UserProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF39DF220E3895200A560F3 /* UserProperties.swift */; }; @@ -134,6 +136,7 @@ CA50B86D22B2A1CF003AFF24 /* R2LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = R2LocalizedString.swift; sourceTree = ""; }; CA6161EA21FB257700D2CFE3 /* r2-shared-swiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "r2-shared-swiftTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; CA6161EE21FB257700D2CFE3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CA94291522BCD08B00305CDB /* ResourcesServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesServer.swift; sourceTree = ""; }; CA9A40D0221B0AA200531EA1 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; CA9E6BA02239823300ECF6E4 /* WP+Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WP+Deprecated.swift"; sourceTree = ""; }; CA9E6BA3223A657900ECF6E4 /* JSONEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEquatable.swift; sourceTree = ""; }; @@ -158,6 +161,7 @@ CACD75F22236B0A5004F20CA /* PublicationCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationCollection.swift; sourceTree = ""; }; CACD75F42236B2AF004F20CA /* PublicationCollectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationCollectionTests.swift; sourceTree = ""; }; CAD178AE22B3A75C004E6812 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + CADD69E122C3B17500A4CADF /* DocumentTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTypes.swift; sourceTree = ""; }; F3B1879F1FA33D4D00BB46BF /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; F3B187A11FA33DFA00BB46BF /* Facet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Facet.swift; sourceTree = ""; }; F3B187A31FA33E1D00BB46BF /* OpdsMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpdsMetadata.swift; sourceTree = ""; }; @@ -200,6 +204,8 @@ 57470F7D20ED0D1A000CDCA3 /* DownloadSession.swift */, CA9E6BA3223A657900ECF6E4 /* JSONEquatable.swift */, CA50B86D22B2A1CF003AFF24 /* R2LocalizedString.swift */, + CA94291522BCD08B00305CDB /* ResourcesServer.swift */, + CADD69E122C3B17500A4CADF /* DocumentTypes.swift */, ); path = Toolkit; sourceTree = ""; @@ -611,6 +617,7 @@ CABBB2F82237C99E004EB039 /* OPDSAcquisition.swift in Sources */, CAF4EAE72237AD6000A17DA1 /* UserProperties.swift in Sources */, CA176387223A898B00959FEB /* EPUBProperties.swift in Sources */, + CA94291622BCD08C00305CDB /* ResourcesServer.swift in Sources */, CA16A8702232F7CD00E66255 /* Link.swift in Sources */, CA176389223A8D0300959FEB /* EPUBMetadata.swift in Sources */, F3E7D4191F4EC69100DF166D /* RootFile.swift in Sources */, @@ -643,6 +650,7 @@ CAC34F0A221C61BD002C452E /* DRM.swift in Sources */, CABBB2FC22394450004EB039 /* ContentLayout.swift in Sources */, CABBB2F62237BF61004EB039 /* OPDSPrice.swift in Sources */, + CADD69E222C3B17500A4CADF /* DocumentTypes.swift in Sources */, CA16A8762233065F00E66255 /* Publication+JSON.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/r2-shared-swift/Publication/Publication.swift b/r2-shared-swift/Publication/Publication.swift index 6009b701..61ffc878 100644 --- a/r2-shared-swift/Publication/Publication.swift +++ b/r2-shared-swift/Publication/Publication.swift @@ -123,21 +123,25 @@ public class Publication: WebPublication, Loggable { /// Generates an URL to a publication's `Link`. public func url(to link: Link?) -> URL? { - guard let link = link, let baseURL = self.baseURL else { + guard let link = link else { return nil } - // Remove trailing "/" before appending the href (href are absolute, hence starting with a "/", but relative to the publication). - let trimmedBaseURLString = baseURL.absoluteString.trimmingCharacters(in: ["/"]) - - return URL(string: trimmedBaseURLString)? - .appendingPathComponent(link.href) + if let url = URL(string: link.href), url.scheme != nil { + return url + } else { + var href = link.href + if href.hasPrefix("/") { + href = String(href.dropFirst()) + } + return baseURL.map { $0.appendingPathComponent(href) } + } } public enum Format: Equatable, Hashable { /// Formats natively supported by Readium. - case cbz, epub, pdf + case cbz, epub, pdf, webpub /// Default value when the format is not specified. case unknown @@ -149,7 +153,7 @@ public class Publication: WebPublication, Loggable { } self.init(mimetypes: [mimetype]) } - + /// Finds the format from a list of possible mimetypes or fallback on a file extension. public init(mimetypes: [String] = [], fileExtension: String? = nil) { self = { @@ -161,6 +165,8 @@ public class Publication: WebPublication, Loggable { return .cbz case "application/pdf", "application/pdf+lcp": return .pdf + case "application/webpub+json", "application/audiobook+json": + return .webpub default: break } @@ -173,12 +179,14 @@ public class Publication: WebPublication, Loggable { return .cbz case "pdf", "lcpdf": return .pdf + case "json": + return .webpub default: return .unknown } }() } - + /// Finds the format of the publication at the given url. /// Uses the format declared as exported UTIs in the app's Info.plist, or fallbacks on the file extension. /// @@ -207,6 +215,11 @@ public class Publication: WebPublication, Loggable { fileExtension: file.pathExtension ) } + + @available(*, deprecated, renamed: "init(file:)") + public init(url: URL) { + self.init(file: url) + } } diff --git a/r2-shared-swift/Toolkit/DocumentTypes.swift b/r2-shared-swift/Toolkit/DocumentTypes.swift new file mode 100644 index 00000000..6a016cdd --- /dev/null +++ b/r2-shared-swift/Toolkit/DocumentTypes.swift @@ -0,0 +1,56 @@ +// +// DocumentTypes.swift +// r2-shared-swift +// +// Created by Mickaël Menu on 26.06.19. +// +// Copyright 2019 Readium Foundation. All rights reserved. +// Use of this source code is governed by a BSD-style license which is detailed +// in the LICENSE file present in the project repository where this source code is maintained. +// + +import CoreServices +import Foundation + +public final class DocumentTypes { + + // Extracts supported Document Types from the main bundle's Info.plist. + private static let types = (Bundle.main.object(forInfoDictionaryKey: "CFBundleDocumentTypes") as? [[String: Any]] ?? []) + + /// Supported UTI. + public static let utis: [String] = types + .flatMap { $0["LSItemContentTypes"] as? [String] ?? [] } + + /// Supported document content types. + public static let contentTypes: [String] = utis + .compactMap { UTTypeCopyPreferredTagWithClass($0 as CFString, kUTTagClassMIMEType)?.takeRetainedValue() as String? } + + /// Supported document extensions. + public static let extensions: [String] = types + .flatMap { $0["CFBundleTypeExtensions"] as? [String] ?? [] } + .map { $0.lowercased() } + + /// Returns the content type for the given URL. + public static func contentType(for url: URL?) -> String? { + return contentType(forExtension: url?.pathExtension) + } + + /// Returns the content type for the given document extension. + public static func contentType(forExtension ext: String?) -> String? { + guard let ext = ext else { + return nil + } + return (UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)?.takeUnretainedValue()) + .flatMap { UTTypeCopyPreferredTagWithClass($0, kUTTagClassMIMEType)?.takeRetainedValue() as String? } + } + + /// Returns the document extension for given content type. + public static func `extension`(forContentType contentType: String?) -> String? { + guard let contentType = contentType else { + return nil + } + return (UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, contentType as CFString, nil)?.takeUnretainedValue()) + .flatMap { UTTypeCopyPreferredTagWithClass($0, kUTTagClassFilenameExtension)?.takeRetainedValue() as String? } + } + +} diff --git a/r2-shared-swift/Toolkit/ResourcesServer.swift b/r2-shared-swift/Toolkit/ResourcesServer.swift new file mode 100644 index 00000000..78b42472 --- /dev/null +++ b/r2-shared-swift/Toolkit/ResourcesServer.swift @@ -0,0 +1,30 @@ +// +// ResourcesServer.swift +// r2-shared-swift +// +// Created by Mickaël Menu on 21.06.19. +// +// Copyright 2019 Readium Foundation. All rights reserved. +// Use of this source code is governed by a BSD-style license which is detailed +// in the LICENSE file present in the project repository where this source code is maintained. +// + +import Foundation + +public enum ResourcesServerError: Error { + case fileNotFound + case invalidPath + case serverFailure +} + +public protocol ResourcesServer { + + /// Serves the local file URL at the given absolute path on the server. + /// If the given URL is a directory, then all the files in the directory are served. + /// Subsequent calls with the same served path overwrite each other. + /// + /// Returns: The URL to access the file on the server. + @discardableResult + func serve(_ url: URL, at path: String) throws -> URL + +}