HTTP API: moved everything back to functions
This commit is contained in:
parent
1875599706
commit
b52824df6f
2 changed files with 91 additions and 76 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::get;
|
||||||
use buffers::ByteString;
|
use buffers::ByteString;
|
||||||
use dht::{Dht, DhtStats};
|
use dht::{Dht, DhtStats};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
|
|
@ -12,9 +14,9 @@ use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use axum::{routing, Router};
|
use axum::Router;
|
||||||
|
|
||||||
use crate::http_api_error::{ApiError, WithErrorStatus};
|
use crate::http_api_error::{ApiError, ApiErrorExt};
|
||||||
use crate::session::{AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session};
|
use crate::session::{AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session};
|
||||||
use crate::torrent_manager::TorrentManagerHandle;
|
use crate::torrent_manager::TorrentManagerHandle;
|
||||||
use crate::torrent_state::StatsSnapshot;
|
use crate::torrent_state::StatsSnapshot;
|
||||||
|
|
@ -37,80 +39,75 @@ impl HttpApi {
|
||||||
|
|
||||||
pub async fn make_http_api_and_run(self, addr: SocketAddr) -> anyhow::Result<()> {
|
pub async fn make_http_api_and_run(self, addr: SocketAddr) -> anyhow::Result<()> {
|
||||||
let state = self.inner;
|
let state = self.inner;
|
||||||
let api_description_body = serde_json::json!({
|
|
||||||
"apis": {
|
async fn api_root() -> impl IntoResponse {
|
||||||
"GET /": "list all available APIs",
|
axum::Json(serde_json::json!({
|
||||||
"GET /dht/stats": "DHT stats",
|
"apis": {
|
||||||
"GET /dht/table": "DHT routing table",
|
"GET /": "list all available APIs",
|
||||||
"GET /torrents": "List torrents (default torrent is 0)",
|
"GET /dht/stats": "DHT stats",
|
||||||
"GET /torrents/{index}": "Torrent details",
|
"GET /dht/table": "DHT routing table",
|
||||||
"GET /torrents/{index}/haves": "The bitfield of have pieces",
|
"GET /torrents": "List torrents (default torrent is 0)",
|
||||||
"GET /torrents/{index}/stats": "Torrent stats",
|
"GET /torrents/{index}": "Torrent details",
|
||||||
// This is kind of not secure as it just reads any local file that it has access to,
|
"GET /torrents/{index}/haves": "The bitfield of have pieces",
|
||||||
// or any URL, but whatever, ok for our purposes / threat model.
|
"GET /torrents/{index}/stats": "Torrent stats",
|
||||||
"POST /torrents": "Add a torrent here. magnet: or http:// or a local file."
|
// This is kind of not secure as it just reads any local file that it has access to,
|
||||||
},
|
// or any URL, but whatever, ok for our purposes / threat model.
|
||||||
"server": "rqbit",
|
"POST /torrents": "Add a torrent here. magnet: or http:// or a local file."
|
||||||
});
|
},
|
||||||
|
"server": "rqbit",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dht_stats(State(state): State<ApiState>) -> Result<impl IntoResponse> {
|
||||||
|
state.api_dht_stats().map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dht_table(State(state): State<ApiState>) -> Result<impl IntoResponse> {
|
||||||
|
state.api_dht_table().map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrents_list(State(state): State<ApiState>) -> impl IntoResponse {
|
||||||
|
axum::Json(state.api_torrent_list())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrents_post(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Query(params): Query<TorrentAddQueryParams>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
let opts = params.into_add_torrent_options();
|
||||||
|
state.api_add_torrent(url, Some(opts)).await.map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrent_details(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(idx): Path<usize>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
state.api_torrent_details(idx).map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrent_haves(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(idx): Path<usize>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
state.api_dump_haves(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrent_stats(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(idx): Path<usize>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
state.api_stats(idx).map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route("/", get(api_root))
|
||||||
"/",
|
.route("/dht/stats", get(dht_stats))
|
||||||
routing::get(move || async move { axum::Json(api_description_body) }),
|
.route("/dht/table", get(dht_table))
|
||||||
)
|
.route("/torrents", get(torrents_list).post(torrents_post))
|
||||||
.route(
|
.route("/torrents/:id", get(torrent_details))
|
||||||
"/dht/stats",
|
.route("/torrents/:id/haves", get(torrent_haves))
|
||||||
routing::get(|State(state): State<ApiState>| async move {
|
.route("/torrents/:id/stats", get(torrent_stats))
|
||||||
state.api_dht_stats().map(axum::Json)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/dht/table",
|
|
||||||
routing::get(|State(state): State<ApiState>| async move {
|
|
||||||
state.api_dht_table().map(axum::Json)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/torrents",
|
|
||||||
routing::get(|State(state): State<ApiState>| async move {
|
|
||||||
axum::Json(state.api_torrent_list())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/torrents",
|
|
||||||
routing::post(
|
|
||||||
|State(state): State<ApiState>,
|
|
||||||
Query(params): Query<TorrentAddQueryParams>,
|
|
||||||
url: String| async move {
|
|
||||||
let opts = params.into_add_torrent_options();
|
|
||||||
state.api_add_torrent(url, Some(opts)).await.map(axum::Json)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/torrents/:id",
|
|
||||||
routing::get(
|
|
||||||
|State(state): State<ApiState>, Path(idx): Path<usize>| async move {
|
|
||||||
state.api_torrent_details(idx).map(axum::Json)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/torrents/:id/haves",
|
|
||||||
routing::get(
|
|
||||||
|State(state): State<ApiState>, Path(idx): Path<usize>| async move {
|
|
||||||
state.api_dump_haves(idx)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/torrents/:id/stats",
|
|
||||||
routing::get(
|
|
||||||
|State(state): State<ApiState>, Path(idx): Path<usize>| async move {
|
|
||||||
state.api_stats(idx).map(axum::Json)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
log::info!("starting HTTP server on {}", addr);
|
log::info!("starting HTTP server on {}", addr);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use serde::{Serialize, Serializer};
|
||||||
pub struct ApiError {
|
pub struct ApiError {
|
||||||
status: Option<StatusCode>,
|
status: Option<StatusCode>,
|
||||||
kind: ApiErrorKind,
|
kind: ApiErrorKind,
|
||||||
|
plaintext: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiError {
|
impl ApiError {
|
||||||
|
|
@ -14,6 +15,7 @@ impl ApiError {
|
||||||
Self {
|
Self {
|
||||||
status: Some(StatusCode::NOT_FOUND),
|
status: Some(StatusCode::NOT_FOUND),
|
||||||
kind: ApiErrorKind::TorrentNotFound(torrent_id),
|
kind: ApiErrorKind::TorrentNotFound(torrent_id),
|
||||||
|
plaintext: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,6 +23,7 @@ impl ApiError {
|
||||||
Self {
|
Self {
|
||||||
status: Some(StatusCode::NOT_FOUND),
|
status: Some(StatusCode::NOT_FOUND),
|
||||||
kind: ApiErrorKind::DhtDisabled,
|
kind: ApiErrorKind::DhtDisabled,
|
||||||
|
plaintext: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +35,15 @@ impl ApiError {
|
||||||
Self {
|
Self {
|
||||||
status: Some(status),
|
status: Some(status),
|
||||||
kind: self.kind,
|
kind: self.kind,
|
||||||
|
plaintext: self.plaintext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_plaintext_error(self, value: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
status: self.status,
|
||||||
|
kind: self.kind,
|
||||||
|
plaintext: value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +91,7 @@ impl From<anyhow::Error> for ApiError {
|
||||||
Self {
|
Self {
|
||||||
status,
|
status,
|
||||||
kind: ApiErrorKind::Other(value),
|
kind: ApiErrorKind::Other(value),
|
||||||
|
plaintext: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,15 +123,20 @@ impl IntoResponse for ApiError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WithErrorStatus<T> {
|
pub trait ApiErrorExt<T> {
|
||||||
fn with_error_status_code(self, s: StatusCode) -> Result<T, ApiError>;
|
fn with_error_status_code(self, s: StatusCode) -> Result<T, ApiError>;
|
||||||
|
fn with_plaintext_error(self, value: bool) -> Result<T, ApiError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, E> WithErrorStatus<T> for std::result::Result<T, E>
|
impl<T, E> ApiErrorExt<T> for std::result::Result<T, E>
|
||||||
where
|
where
|
||||||
E: Into<ApiError>,
|
E: Into<ApiError>,
|
||||||
{
|
{
|
||||||
fn with_error_status_code(self, s: StatusCode) -> Result<T, ApiError> {
|
fn with_error_status_code(self, s: StatusCode) -> Result<T, ApiError> {
|
||||||
self.map_err(|e| e.into().with_status(s))
|
self.map_err(|e| e.into().with_status(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_plaintext_error(self, value: bool) -> Result<T, ApiError> {
|
||||||
|
self.map_err(|e| e.into().with_plaintext_error(value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue