diff --git a/Sources/App/Controllers/PackageController+routes.swift b/Sources/App/Controllers/PackageController+routes.swift index eb476959b..c9b3444d7 100644 --- a/Sources/App/Controllers/PackageController+routes.swift +++ b/Sources/App/Controllers/PackageController+routes.swift @@ -58,13 +58,7 @@ enum PackageController { } static func documentation(req: Request, route: DocRoute) async throws -> Response { - let res: ClientResponse - do { - res = try await awsResponse(client: req.client, route: route) - } catch { - print(error) - throw error - } + let res = try await awsResponse(client: req.client, route: route) switch route.fragment { case .documentation, .tutorials: @@ -78,7 +72,7 @@ enum PackageController { documentationMetadata: documentationMetadata ) - case .css, .data, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings: + case .css, .data, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings, .svgImages, .svgImg: return try await res.encodeResponse( status: .ok, headers: req.headers @@ -452,8 +446,8 @@ extension PackageController { let path = route.path switch route.fragment { - case .css, .data, .documentation, .images, .img, .index, .js, .tutorials: - return URI(string: "\(baseURL)/\(route.fragment)/\(path)") + case .css, .data, .documentation, .images, .img, .index, .js, .tutorials, .svgImages, .svgImg: + return URI(string: "\(baseURL)/\(route.fragment.urlFragment)/\(path)") case .faviconIco, .faviconSvg, .themeSettings: return path.isEmpty ? URI(string: "\(baseURL)/\(route.fragment)") diff --git a/Sources/App/Core/DocRoute.swift b/Sources/App/Core/DocRoute.swift index b336b7a7e..4b603330b 100644 --- a/Sources/App/Core/DocRoute.swift +++ b/Sources/App/Core/DocRoute.swift @@ -35,6 +35,8 @@ struct DocRoute: Equatable { case linkablePaths = "linkable-paths.json" case themeSettings = "theme-settings.json" case tutorials + case svgImages + case svgImg var contentType: String { switch self { @@ -48,18 +50,31 @@ struct DocRoute: Equatable { return "text/html; charset=utf-8" case .js: return "application/javascript" + case .svgImages, .svgImg: + return "image/svg+xml" } } var requiresArchive: Bool { switch self { - case .css, .data, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings, .tutorials: + case .css, .data, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings, .tutorials, .svgImages, .svgImg: return false case .documentation: return true } } + var urlFragment: String { + switch self { + case .css, .data, .documentation, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings, .tutorials: + return rawValue + case .svgImages: + return "images" + case .svgImg: + return "img" + } + } + var description: String { rawValue } } } diff --git a/Sources/App/routes+documentation.swift b/Sources/App/routes+documentation.swift index 8a2883747..1576957ad 100644 --- a/Sources/App/routes+documentation.swift +++ b/Sources/App/routes+documentation.swift @@ -59,11 +59,13 @@ func docRoutes(_ app: Application) throws { return try await PackageController.documentation(req: $0, route: route) }.excludeFromOpenAPI() app.get(":owner", ":repository", ":reference", "images", "**") { - let route = try await $0.getDocRoute(fragment: .images) + let fragment: DocRoute.Fragment = $0.parameters.hasSuffix(".svg", caseInsensitive: true) ? .svgImages : .images + let route = try await $0.getDocRoute(fragment: fragment) return try await PackageController.documentation(req: $0, route: route) }.excludeFromOpenAPI() app.get(":owner", ":repository", ":reference", "img", "**") { - let route = try await $0.getDocRoute(fragment: .img) + let fragment: DocRoute.Fragment = $0.parameters.hasSuffix(".svg", caseInsensitive: true) ? .svgImg : .img + let route = try await $0.getDocRoute(fragment: fragment) return try await PackageController.documentation(req: $0, route: route) }.excludeFromOpenAPI() app.get(":owner", ":repository", ":reference", "index", "**") { @@ -109,10 +111,18 @@ private extension Parameters { // AND THE FIX // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/pull/3039 return ([archive].compactMap { $0 } + getCatchall()).map { $0.lowercased() } - case .css, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings: + case .css, .faviconIco, .faviconSvg, .images, .img, .index, .js, .linkablePaths, .themeSettings, .svgImages, .svgImg: return getCatchall() } } + + func hasSuffix(_ suffix: String, caseInsensitive: Bool) -> Bool { + if caseInsensitive { + return getCatchall().last?.lowercased().hasSuffix(suffix.lowercased()) ?? false + } else { + return getCatchall().last?.hasSuffix(suffix) ?? false + } + } } diff --git a/Tests/AppTests/PackageController+routesTests.swift b/Tests/AppTests/PackageController+routesTests.swift index f4d578f5d..638863ea3 100644 --- a/Tests/AppTests/PackageController+routesTests.swift +++ b/Tests/AppTests/PackageController+routesTests.swift @@ -343,6 +343,22 @@ class PackageController_routesTests: SnapshotTestCase { try PackageController.awsDocumentationURL(route: .init(owner: "Foo", repository: "Bar", docVersion: .reference("1.2.3"), fragment: .data, pathElements: ["path"])).string, "http://docs-bucket.s3-website.us-east-2.amazonaws.com/foo/bar/1.2.3/data/path" ) + XCTAssertEqual( + try PackageController.awsDocumentationURL(route: .init(owner: "Foo", repository: "Bar", docVersion: .reference("1.2.3"), fragment: .images, pathElements: ["path"])).string, + "http://docs-bucket.s3-website.us-east-2.amazonaws.com/foo/bar/1.2.3/images/path" + ) + XCTAssertEqual( + try PackageController.awsDocumentationURL(route: .init(owner: "Foo", repository: "Bar", docVersion: .reference("1.2.3"), fragment: .img, pathElements: ["path"])).string, + "http://docs-bucket.s3-website.us-east-2.amazonaws.com/foo/bar/1.2.3/img/path" + ) + XCTAssertEqual( + try PackageController.awsDocumentationURL(route: .init(owner: "Foo", repository: "Bar", docVersion: .reference("1.2.3"), fragment: .svgImages, pathElements: ["path"])).string, + "http://docs-bucket.s3-website.us-east-2.amazonaws.com/foo/bar/1.2.3/images/path" + ) + XCTAssertEqual( + try PackageController.awsDocumentationURL(route: .init(owner: "Foo", repository: "Bar", docVersion: .reference("1.2.3"), fragment: .svgImg, pathElements: ["path"])).string, + "http://docs-bucket.s3-website.us-east-2.amazonaws.com/foo/bar/1.2.3/img/path" + ) XCTAssertEqual( try PackageController.awsDocumentationURL(route: .init(owner: "Foo", repository: "Bar", docVersion: .reference("1.2.3"), fragment: .js, pathElements: ["path"])).string, "http://docs-bucket.s3-website.us-east-2.amazonaws.com/foo/bar/1.2.3/js/path" @@ -427,6 +443,27 @@ class PackageController_routesTests: SnapshotTestCase { "/owner/repo/canonical-ref/documentation/archive/symbol:$-%") } + func test_documentation_routes_contentType() async throws { + try await app.test(.GET, "/owner/package/main/images/foo/bar.jpeg") { res async in + XCTAssertEqual(res.headers.contentType, .init(type: "application", subType: "octet-stream")) + } + try await app.test(.GET, "/owner/package/main/images/foo/bar.svg") { res async in + XCTAssertEqual(res.headers.contentType, .init(type: "image", subType: "svg+xml")) + } + try await app.test(.GET, "/owner/package/main/images/foo/bar.SVG") { res async in + XCTAssertEqual(res.headers.contentType, .init(type: "image", subType: "svg+xml")) + } + try await app.test(.GET, "/owner/package/main/img/foo/bar.jpeg") { res async in + XCTAssertEqual(res.headers.contentType, .init(type: "application", subType: "octet-stream")) + } + try await app.test(.GET, "/owner/package/main/img/foo/bar.svg") { res async in + XCTAssertEqual(res.headers.contentType, .init(type: "image", subType: "svg+xml")) + } + try await app.test(.GET, "/owner/package/main/img/foo/bar.SVG") { res async in + XCTAssertEqual(res.headers.contentType, .init(type: "image", subType: "svg+xml")) + } + } + func test_documentation_routes_redirect() async throws { // Test the redirect documentation routes without any reference: // /owner/package/documentation + various path elements