Working utoipa setup

This commit is contained in:
Antoine Gersant 2025-01-13 22:15:59 -08:00
parent 1b142b1855
commit 2e2ddf017b
32 changed files with 55 additions and 1550 deletions

View file

@ -43,6 +43,7 @@
### API
- API version is now 8.0.
- Documentation is now served under `/docs` instead of `/swagger` (eg. `http://localhost:5050/docs`)
- Clients are now expected to send their preferred API major version in a `Accept-Version` header. Omitting this currently defaults to `7`, but will become an error in future Polaris releases. Support for API version 7 will be removed entirely in a future release.
- Most API responses now support gzip compression.
- The response format of the `/browse`, `/flatten`, `/get_playlist`, `/search/<query>` endpoints has been modified to accomodate large lists.

53
Cargo.lock generated
View file

@ -1851,7 +1851,11 @@ dependencies = [
"ureq",
"utoipa",
"utoipa-axum",
<<<<<<< HEAD
"utoipa-swagger-ui",
=======
"utoipa-scalar",
>>>>>>> 7653e4b (Working utoipa setup)
"winres",
]
@ -2711,6 +2715,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
<<<<<<< HEAD
]
[[package]]
@ -2722,6 +2727,8 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
=======
>>>>>>> 7653e4b (Working utoipa setup)
]
[[package]]
@ -3106,6 +3113,7 @@ dependencies = [
]
[[package]]
<<<<<<< HEAD
name = "utoipa-swagger-ui"
version = "8.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -3131,6 +3139,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
[[package]]
=======
name = "utoipa-scalar"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088e93bf19f6bd06e0aacb02ca432b3c5a449c4aec2e4aa9fc333a667f2b2c55"
dependencies = [
"axum",
"serde",
"serde_json",
"utoipa",
]
[[package]]
>>>>>>> 7653e4b (Working utoipa setup)
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -3527,37 +3549,6 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "zip"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap",
"memchr",
"thiserror 2.0.11",
"zopfli",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]
[[package]]
name = "zune-core"
version = "0.4.12"

View file

@ -66,8 +66,8 @@ trie-rs = { version = "0.4.2", features = ["serde"] }
unicase = "2.7.0"
ureq = { version = "2.10.0", default-features = false, features = ["tls"] }
utoipa = { version = "5.3", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "8.1", features = ["axum", "vendored"] }
utoipa-axum = { version = "0.1" }
utoipa-scalar = { version = "0.2", features = ["axum"] }
[dependencies.axum]
version = "0.8.1"

View file

@ -40,7 +40,7 @@ Password: `demo_password`
### API Documentation
The Polaris server API is documented via [Swagger](https://demo.polaris.stream/swagger/). Every installation of Polaris distributes this documentation, with the ability to use the `Try it out` buttons. To access it, simply open http://localhost:5050/swagger/ in your browser on the machine running Polaris.
The Polaris server API is documented via [OpenAPI](https://demo.polaris.stream/docs/). Every installation of Polaris distributes this interactive documentation. To access it, simply open http://localhost:5050/docs/ in your browser on the machine running Polaris.
## Credits & License Information

View file

@ -13,10 +13,9 @@ Polaris supports a few command line arguments which are useful during developmen
- `-c some/config.toml` sets the location of the configuration file. This is useful to preconfigure users and music directories.
- `--data some/path` sets the folder Polaris will use to store runtime data such as playlists, collection index and auth secrets.
- `-w some/path/to/web/dir` lets you point to the directory to be served as the web interface. You can find a suitable directory in your Polaris install (under `/web`), or from the [latest polaris-web release](https://github.com/agersant/polaris-web/releases/latest/download/web.zip).
- `-s some/path/to/swagger/dir` lets you point to the directory to be served as the swagger API documentation. You'll probably want to point this to the `/docs/swagger` directory of the polaris repository.
- `-f` (on Linux) makes Polaris not fork into a separate process.
Putting it all together, a typical command to compile and run the program would be: `cargo run -- -w web -s docs/swagger -c test-config.toml`
Putting it all together, a typical command to compile and run the program would be: `cargo run -- -w web -c test-config.toml`
While Polaris is running, access the web UI at [http://localhost:5050](http://localhost:5050).

Binary file not shown.

Before

(image error) Size: 665 B

Binary file not shown.

Before

(image error) Size: 628 B

View file

@ -1,60 +0,0 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Polaris Swagger UI</title>
<link rel="stylesheet" type="text/css" href="swagger-ui.css">
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="swagger-ui-bundle.js"> </script>
<script src="swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "polaris-api.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View file

@ -1,67 +0,0 @@
<!doctype html>
<html lang="en-US">
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"swagger-ui.css","sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,7 @@ echo "Creating output directory"
mkdir -p release/tmp/polaris
echo "Copying package files"
cp -r web docs/swagger src migrations test-data build.rs Cargo.toml Cargo.lock rust-toolchain res/unix/Makefile release/tmp/polaris
cp -r web src migrations test-data build.rs Cargo.toml Cargo.lock rust-toolchain res/unix/Makefile release/tmp/polaris
echo "Creating tarball"
tar -zc -C release/tmp -f release/polaris.tar.gz polaris

View file

@ -49,7 +49,6 @@
<ComponentRef Id="ProgramMenuDir" />
<ComponentRef Id="CleanupExtraData" />
<ComponentGroupRef Id="WebUI" />
<ComponentGroupRef Id="SwaggerUI" />
</Feature>
<Icon Id="polaris.exe" SourceFile="polaris.exe" />
<Property Id="ARPPRODUCTICON" Value="polaris.exe" />

View file

@ -8,7 +8,6 @@ if (!(Test-Path env:POLARIS_VERSION)) {
# And remove the code setting these as defaults in `service/mod.rs`
# $script:INSTALL_DIR = "%LOCALAPPDATA%\Permafrost\Polaris"
# $env:POLARIS_WEB_DIR = "$INSTALL_DIR\web"
# $env:POLARIS_SWAGGER_DIR = "$INSTALL_DIR\swagger"
# $env:POLARIS_DB_DIR = "$INSTALL_DIR"
# $env:POLARIS_LOG_DIR = "$INSTALL_DIR"
# $env:POLARIS_CACHE_DIR = "$INSTALL_DIR"
@ -29,7 +28,6 @@ Copy-Item .\res\windows\installer\dialog.bmp .\release\tmp\
Copy-Item .\target\release\polaris.exe .\release\tmp\
Copy-Item .\target\release\polaris-cli.exe .\release\tmp\
Copy-Item .\web .\release\tmp\web -recurse
Copy-Item .\docs\swagger .\release\tmp\swagger -recurse
""
"Inserting version number in installer config"
@ -41,15 +39,13 @@ $wxs.Save('.\res\windows\installer\installer.wxs')
"Creating installer"
$heat_exe = Join-Path $env:WIX bin\heat.exe
& $heat_exe dir .\release\tmp\web\ -ag -g1 -dr AppDataPolaris -cg WebUI -sfrag -var wix.WebUIDir -out .\release\tmp\web_ui_fragment.wxs
& $heat_exe dir .\release\tmp\swagger\ -ag -g1 -dr AppDataPolaris -cg SwaggerUI -sfrag -var wix.SwaggerUIDir -out .\release\tmp\swagger_ui_fragment.wxs
$candle_exe = Join-Path $env:WIX bin\candle.exe
& $candle_exe -wx -ext WixUtilExtension -arch x64 -out .\release\tmp\web_ui_fragment.wixobj .\release\tmp\web_ui_fragment.wxs
& $candle_exe -wx -ext WixUtilExtension -arch x64 -out .\release\tmp\swagger_ui_fragment.wixobj .\release\tmp\swagger_ui_fragment.wxs
& $candle_exe -wx -ext WixUtilExtension -arch x64 -out .\release\tmp\installer.wixobj .\res\windows\installer\installer.wxs
$light_exe = Join-Path $env:WIX bin\light.exe
& $light_exe -dWebUIDir=".\release\tmp\web" -dSwaggerUIDir=".\release\tmp\swagger" -wx -ext WixUtilExtension -ext WixUIExtension -spdb -sw1076 -sice:ICE38 -sice:ICE64 -out .\release\polaris.msi .\release\tmp\installer.wixobj .\release\tmp\web_ui_fragment.wixobj .\release\tmp\swagger_ui_fragment.wixobj
& $light_exe -dWebUIDir=".\release\tmp\web" -wx -ext WixUtilExtension -ext WixUIExtension -spdb -sw1076 -sice:ICE38 -sice:ICE64 -out .\release\polaris.msi .\release\tmp\installer.wixobj .\release\tmp\web_ui_fragment.wixobj
"Cleaning up"
Remove-Item -Recurse .\release\tmp

View file

@ -154,7 +154,6 @@ pub enum Error {
pub struct App {
pub port: u16,
pub web_dir_path: PathBuf,
pub swagger_dir_path: PathBuf,
pub ddns_manager: ddns::Manager,
pub scanner: scanner::Scanner,
pub index_manager: index::Manager,
@ -172,9 +171,6 @@ impl App {
fs::create_dir_all(&paths.web_dir_path)
.map_err(|e| Error::Io(paths.web_dir_path.clone(), e))?;
fs::create_dir_all(&paths.swagger_dir_path)
.map_err(|e| Error::Io(paths.swagger_dir_path.clone(), e))?;
let peaks_dir_path = paths.cache_dir_path.join("peaks");
fs::create_dir_all(&peaks_dir_path).map_err(|e| Error::Io(peaks_dir_path.clone(), e))?;
@ -198,7 +194,6 @@ impl App {
let app = Self {
port,
web_dir_path: paths.web_dir_path,
swagger_dir_path: paths.swagger_dir_path,
ddns_manager,
scanner,
index_manager,

View file

@ -135,7 +135,6 @@ fn main() -> Result<(), Error> {
if !cli_options.foreground {
info!("Pid file location is {:#?}", paths.pid_file_path);
}
info!("Swagger files location is {:#?}", paths.swagger_dir_path);
info!("Web client files location is {:#?}", paths.web_dir_path);
async_main(cli_options, paths)

View file

@ -12,7 +12,6 @@ pub struct CLIOptions {
pub cache_dir_path: Option<PathBuf>,
pub data_dir_path: Option<PathBuf>,
pub web_dir_path: Option<PathBuf>,
pub swagger_dir_path: Option<PathBuf>,
pub port: Option<u16>,
pub log_level: Option<LevelFilter>,
}
@ -45,7 +44,6 @@ impl Manager {
cache_dir_path: matches.opt_str("cache").map(PathBuf::from),
data_dir_path: matches.opt_str("data").map(PathBuf::from),
web_dir_path: matches.opt_str("w").map(PathBuf::from),
swagger_dir_path: matches.opt_str("s").map(PathBuf::from),
port: matches.opt_str("p").and_then(|p| p.parse().ok()),
log_level: matches.opt_str("log-level").and_then(|l| l.parse().ok()),
})
@ -62,7 +60,6 @@ fn get_options() -> getopts::Options {
options.optopt("p", "port", "set polaris to run on a custom port", "PORT");
options.optopt("d", "database", "set the path to index database", "FILE");
options.optopt("w", "web", "set the path to web client files", "DIRECTORY");
options.optopt("s", "swagger", "set the path to swagger files", "DIRECTORY");
options.optopt(
"",
"cache",

View file

@ -10,7 +10,6 @@ pub struct Paths {
pub log_file_path: Option<PathBuf>,
#[cfg(unix)]
pub pid_file_path: PathBuf,
pub swagger_dir_path: PathBuf,
pub web_dir_path: PathBuf,
}
@ -26,7 +25,6 @@ impl Default for Paths {
db_file_path: [".", "db.sqlite"].iter().collect(),
log_file_path: Some([".", "polaris.log"].iter().collect()),
pid_file_path: [".", "polaris.pid"].iter().collect(),
swagger_dir_path: [".", "docs", "swagger"].iter().collect(),
web_dir_path: [".", "web"].iter().collect(),
}
}
@ -44,7 +42,6 @@ impl Default for Paths {
data_dir_path: install_directory.clone(),
db_file_path: install_directory.join("db.sqlite"),
log_file_path: Some(install_directory.join("polaris.log")),
swagger_dir_path: install_directory.join("swagger"),
web_dir_path: install_directory.join("web"),
}
}
@ -76,9 +73,6 @@ impl Paths {
.map(PathBuf::from)
.map(|p| p.join("polaris.pid"))
.unwrap_or(defaults.pid_file_path),
swagger_dir_path: option_env!("POLARIS_SWAGGER_DIR")
.map(PathBuf::from)
.unwrap_or(defaults.swagger_dir_path),
web_dir_path: option_env!("POLARIS_WEB_DIR")
.map(PathBuf::from)
.unwrap_or(defaults.web_dir_path),
@ -103,9 +97,6 @@ impl Paths {
if let Some(path) = &cli_options.pid_file_path {
path.clone_into(&mut paths.pid_file_path);
}
if let Some(path) = &cli_options.swagger_dir_path {
path.clone_into(&mut paths.swagger_dir_path);
}
if let Some(path) = &cli_options.web_dir_path {
path.clone_into(&mut paths.web_dir_path);
}

View file

@ -9,7 +9,7 @@ use tower_http::{
};
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_swagger_ui::SwaggerUi;
use utoipa_scalar::{Scalar, Servable};
mod api;
mod auth;
@ -21,20 +21,18 @@ mod version;
pub mod test;
pub fn make_router(app: App) -> NormalizePath<Router> {
let swagger = ServeDir::new(&app.swagger_dir_path);
let static_files = Router::new()
.fallback_service(ServeDir::new(&app.web_dir_path))
.layer(CompressionLayer::new());
let (open_api_router, open_api) =
OpenApiRouter::with_openapi(doc::ApiDoc::openapi()).split_for_parts();
let (open_api_router, open_api) = OpenApiRouter::with_openapi(doc::ApiDoc::openapi())
.nest("/api", api::router())
.split_for_parts();
let router = open_api_router
.merge(SwaggerUi::new("/swagger-ui"))
.nest("/api", api::router())
.with_state(app.clone())
.nest("/", static_files)
.merge(Scalar::with_url("/docs", open_api))
.fallback_service(static_files)
.layer(logger::LogLayer::new());
NormalizePathLayer::trim_trailing_slash().layer(router)

View file

@ -4,13 +4,14 @@ use axum::{
extract::{DefaultBodyLimit, Path, Query, State},
response::{IntoResponse, Response},
routing::{delete, get, post, put},
Json, Router,
Json,
};
use axum_extra::headers::Range;
use axum_extra::TypedHeader;
use axum_range::{KnownSize, Ranged};
use regex::Regex;
use tower_http::{compression::CompressionLayer, CompressionLevel};
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::{
app::{auth, config, ddns, index, peaks, playlist, scanner, thumbnail, App},
@ -22,11 +23,11 @@ use crate::{
use super::auth::{AdminRights, Auth};
pub fn router() -> Router<App> {
Router::new()
pub fn router() -> OpenApiRouter<App> {
OpenApiRouter::new()
// Basic
.route("/version", get(get_version))
.route("/initial_setup", get(get_initial_setup))
.routes(routes!(get_version))
.routes(routes!(get_initial_setup))
.route("/auth", post(post_auth))
// Configuration
.route("/settings", get(get_settings))
@ -77,6 +78,13 @@ pub fn router() -> Router<App> {
.route("/audio/{*path}", get(get_audio))
}
#[utoipa::path(
get,
path = "/version",
responses(
(status = 200, body = dto::Version),
),
)]
async fn get_version() -> Json<dto::Version> {
let current_version = dto::Version {
major: API_MAJOR_VERSION,

View file

@ -29,7 +29,6 @@ impl TestService for AxumTestService {
#[cfg(unix)]
pid_file_path: output_dir.join("polaris.pid"),
log_file_path: None,
swagger_dir_path: ["docs", "swagger"].iter().collect(),
web_dir_path: ["test-data", "web"].iter().collect(),
};

View file

@ -4,7 +4,7 @@ use utoipa::ToSchema;
use crate::app::{config, index, peaks, playlist, scanner, thumbnail};
use std::{collections::HashMap, convert::From, path::PathBuf, time::UNIX_EPOCH};
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, ToSchema)]
pub struct Version {
pub major: i32,
pub minor: i32,

View file

@ -14,11 +14,11 @@ mod admin;
mod auth;
mod browser;
mod collection;
mod docs;
mod media;
mod playlist;
mod search;
mod settings;
mod swagger;
mod user;
mod web;

View file

@ -4,17 +4,17 @@ use crate::server::test::{add_trailing_slash, protocol, ServiceType, TestService
use crate::test_name;
#[tokio::test]
async fn can_get_swagger_index() {
async fn can_get_docs_index() {
let mut service = ServiceType::new(&test_name!()).await;
let request = protocol::swagger_index();
let request = protocol::docs_index();
let response = service.fetch(&request).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn can_get_swagger_index_with_trailing_slash() {
async fn can_get_docs_index_with_trailing_slash() {
let mut service = ServiceType::new(&test_name!()).await;
let mut request = protocol::swagger_index();
let mut request = protocol::docs_index();
add_trailing_slash(&mut request);
let response = service.fetch(&request).await;
assert_eq!(response.status(), StatusCode::OK);

View file

@ -32,10 +32,10 @@ pub fn web_index() -> Request<()> {
.unwrap()
}
pub fn swagger_index() -> Request<()> {
pub fn docs_index() -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/swagger")
.uri("/docs")
.body(())
.unwrap()
}