2023-11-24 14:08:02 +00:00
use anyhow ::Context ;
2023-11-20 19:52:48 +00:00
use axum ::body ::Bytes ;
2024-08-05 09:30:26 +02:00
use axum ::extract ::{ Path , Query , State } ;
2024-07-20 10:08:10 +02:00
use axum ::response ::{ IntoResponse , Redirect } ;
2023-11-24 14:19:39 +00:00
use axum ::routing ::{ get , post } ;
2024-02-27 08:14:39 +00:00
use futures ::future ::BoxFuture ;
use futures ::{ FutureExt , TryStreamExt } ;
2024-04-24 20:56:58 +01:00
use http ::{ HeaderMap , HeaderValue , StatusCode } ;
2023-11-22 17:19:35 +00:00
use itertools ::Itertools ;
2023-12-02 15:19:05 +00:00
2021-10-10 11:39:25 +01:00
use serde ::{ Deserialize , Serialize } ;
2024-04-24 19:10:17 +01:00
use std ::io ::SeekFrom ;
2021-10-10 09:57:21 +01:00
use std ::net ::SocketAddr ;
2023-12-01 09:30:23 +00:00
use std ::str ::FromStr ;
2023-11-30 16:05:48 +00:00
use std ::time ::Duration ;
2024-04-24 19:10:17 +01:00
use tokio ::io ::AsyncSeekExt ;
2024-04-29 18:26:36 +01:00
use tracing ::{ debug , info , trace } ;
2022-12-04 12:53:55 +00:00
2022-12-08 23:56:14 +00:00
use axum ::Router ;
2021-06-30 10:14:33 +01:00
2023-12-02 15:19:05 +00:00
use crate ::api ::Api ;
2023-11-30 16:05:48 +00:00
use crate ::peer_connection ::PeerConnectionOptions ;
2023-12-08 19:47:48 +00:00
use crate ::session ::{ AddTorrent , AddTorrentOptions , SUPPORTED_SCHEMES } ;
2023-12-02 15:19:05 +00:00
use crate ::torrent_state ::peer ::stats ::snapshot ::PeerStatsFilter ;
2023-12-08 19:47:48 +00:00
type ApiState = Api ;
2023-12-02 15:19:05 +00:00
use crate ::api ::Result ;
2024-08-12 23:50:55 +01:00
use crate ::{ ApiError , ListOnlyResponse , ManagedTorrent } ;
2021-07-08 23:03:58 +01:00
2023-12-03 12:14:50 +00:00
/// An HTTP server for the API.
2022-12-08 15:40:29 +00:00
pub struct HttpApi {
2023-12-02 15:19:05 +00:00
inner : ApiState ,
2023-12-08 19:47:48 +00:00
opts : HttpApiOptions ,
}
#[ derive(Debug, Default) ]
pub struct HttpApiOptions {
pub read_only : bool ,
2022-12-08 09:28:01 +00:00
}
2022-12-08 15:40:29 +00:00
impl HttpApi {
2023-12-08 19:47:48 +00:00
pub fn new ( api : Api , opts : Option < HttpApiOptions > ) -> Self {
2022-12-08 09:28:01 +00:00
Self {
2023-12-08 19:47:48 +00:00
inner : api ,
opts : opts . unwrap_or_default ( ) ,
2022-12-08 09:28:01 +00:00
}
}
2022-12-08 11:06:29 +00:00
2023-12-03 12:14:50 +00:00
/// Run the HTTP server forever on the given address.
/// If read_only is passed, no state-modifying methods will be exposed.
2024-02-26 23:08:47 +00:00
#[ inline(never) ]
2024-02-27 08:14:39 +00:00
pub fn make_http_api_and_run ( self , addr : SocketAddr ) -> BoxFuture < 'static , anyhow ::Result < ( ) > > {
2022-12-08 15:40:29 +00:00
let state = self . inner ;
2022-12-08 23:56:14 +00:00
async fn api_root ( ) -> impl IntoResponse {
axum ::Json ( serde_json ::json! ( {
" apis " : {
" GET / " : " list all available APIs " ,
" GET /dht/stats " : " DHT stats " ,
" GET /dht/table " : " DHT routing table " ,
" GET /torrents " : " List torrents (default torrent is 0) " ,
" GET /torrents/{index} " : " Torrent details " ,
" GET /torrents/{index}/haves " : " The bitfield of have pieces " ,
2023-11-25 01:24:57 +00:00
" GET /torrents/{index}/stats/v1 " : " Torrent stats " ,
2023-11-20 13:55:42 +00:00
" GET /torrents/{index}/peer_stats " : " Per peer stats " ,
2023-11-25 01:24:57 +00:00
" POST /torrents/{index}/pause " : " Pause torrent " ,
" POST /torrents/{index}/start " : " Resume torrent " ,
" POST /torrents/{index}/forget " : " Forget about the torrent, keep the files " ,
" POST /torrents/{index}/delete " : " Forget about the torrent, remove the files " ,
2024-03-30 20:36:56 +00:00
" POST /torrents/{index}/update_only_files " : " Change the selection of files to download. You need to POST json of the following form { \" only_files \" : [0, 1, 2]} " ,
2023-11-21 12:56:07 +00:00
" POST /torrents " : " Add a torrent here. magnet: or http:// or a local file. " ,
2023-11-25 01:24:57 +00:00
" POST /rust_log " : " Set RUST_LOG to this post launch (for debugging) " ,
2023-11-21 12:56:07 +00:00
" GET /web/ " : " Web UI " ,
2022-12-08 23:56:14 +00:00
} ,
" server " : " rqbit " ,
2023-12-07 12:19:35 +00:00
" version " : env ! ( " CARGO_PKG_VERSION " ) ,
2022-12-08 23:56:14 +00:00
} ) )
}
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 > ,
2023-11-20 19:52:48 +00:00
data : Bytes ,
2022-12-08 23:56:14 +00:00
) -> Result < impl IntoResponse > {
2023-12-01 11:28:35 +00:00
let is_url = params . is_url ;
2022-12-08 23:56:14 +00:00
let opts = params . into_add_torrent_options ( ) ;
2023-12-01 11:28:35 +00:00
let data = data . to_vec ( ) ;
let add = match is_url {
Some ( true ) = > AddTorrent ::Url (
String ::from_utf8 ( data )
. context ( " invalid utf-8 for passed URL " ) ?
. into ( ) ,
) ,
Some ( false ) = > AddTorrent ::TorrentFileBytes ( data . into ( ) ) ,
// Guess the format.
None if SUPPORTED_SCHEMES
. iter ( )
. any ( | s | data . starts_with ( s . as_bytes ( ) ) ) = >
{
AddTorrent ::Url (
String ::from_utf8 ( data )
. context ( " invalid utf-8 for passed URL " ) ?
. into ( ) ,
)
}
_ = > AddTorrent ::TorrentFileBytes ( data . into ( ) ) ,
2023-11-20 19:52:48 +00:00
} ;
state . api_add_torrent ( add , Some ( opts ) ) . await . map ( axum ::Json )
2022-12-08 23:56:14 +00:00
}
async fn torrent_details (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_torrent_details ( idx ) . map ( axum ::Json )
}
2024-08-09 12:13:09 +01:00
fn torrent_playlist_items ( handle : & ManagedTorrent ) -> Result < Vec < ( usize , String ) > > {
let mut playlist_items = handle
. info ( )
. info
. iter_filenames_and_lengths ( ) ?
2024-08-04 13:36:39 +02:00
. enumerate ( )
2024-08-09 12:13:09 +01:00
. filter_map ( | ( file_idx , ( filename , _ ) ) | {
let filename = filename . to_vec ( ) . ok ( ) ? . join ( " / " ) ;
let is_playable = mime_guess ::from_path ( & filename )
2024-08-04 13:36:39 +02:00
. first ( )
. map ( | mime | {
mime . type_ ( ) = = mime_guess ::mime ::VIDEO
| | mime . type_ ( ) = = mime_guess ::mime ::AUDIO
} )
. unwrap_or ( false ) ;
if is_playable {
2024-08-09 12:13:09 +01:00
let filename = urlencoding ::encode ( & filename ) ;
Some ( ( file_idx , filename . into_owned ( ) ) )
2024-08-04 13:36:39 +02:00
} else {
None
}
2024-08-09 08:31:53 +02:00
} )
. collect ::< Vec < _ > > ( ) ;
2024-08-09 21:38:31 +02:00
playlist_items . sort_by ( | left , right | left . 1. cmp ( & right . 1 ) ) ;
2024-08-09 12:13:09 +01:00
Ok ( playlist_items )
}
fn get_host ( headers : & HeaderMap ) -> Result < & str > {
Ok ( headers
. get ( " host " )
. ok_or_else ( | | {
ApiError ::new_from_text ( StatusCode ::BAD_REQUEST , " Missing host header " )
} ) ?
. to_str ( )
. context ( " hostname is not string " ) ? )
}
fn build_playlist_content (
host : & str ,
it : impl IntoIterator < Item = ( usize , usize , String ) > ,
) -> impl IntoResponse {
let body = it
2024-08-09 08:31:53 +02:00
. into_iter ( )
2024-08-09 12:13:09 +01:00
. map ( | ( torrent_idx , file_idx , filename ) | {
format! ( " http:// {host} /torrents/ {torrent_idx} /stream/ {file_idx} / {filename} " )
2024-08-09 08:31:53 +02:00
} )
. join ( " \r \n " ) ;
2024-08-09 12:13:09 +01:00
(
2024-08-10 13:30:02 +01:00
[
( " Content-Type " , " application/mpegurl; charset=utf-8 " ) ,
(
" Content-Disposition " ,
" attachment; filename= \" rqbit-playlist.m3u8 \" " ,
) ,
] ,
2024-08-09 12:13:09 +01:00
body ,
)
}
2024-08-12 23:50:55 +01:00
async fn resolve_magnet (
State ( state ) : State < ApiState > ,
url : String ,
) -> Result < impl IntoResponse > {
let added = state
. session ( )
. add_torrent (
AddTorrent ::from_url ( & url ) ,
Some ( AddTorrentOptions {
list_only : true ,
.. Default ::default ( )
} ) ,
)
. await ? ;
2024-08-12 23:59:23 +01:00
let ( info , content ) = match added {
crate ::AddTorrentResponse ::AlreadyManaged ( _ , handle ) = > (
handle . info ( ) . info . clone ( ) ,
2024-08-13 06:43:52 +01:00
handle . info ( ) . torrent_bytes . clone ( ) ,
2024-08-12 23:59:23 +01:00
) ,
crate ::AddTorrentResponse ::ListOnly ( ListOnlyResponse {
info ,
torrent_bytes ,
..
2024-08-13 06:43:52 +01:00
} ) = > ( info , torrent_bytes ) ,
2024-08-12 23:50:55 +01:00
crate ::AddTorrentResponse ::Added ( _ , _ ) = > {
return Err ( ApiError ::new_from_text (
StatusCode ::INTERNAL_SERVER_ERROR ,
" bug: torrent was added to session, but shouldn't have been " ,
) )
}
} ;
2024-08-12 23:59:23 +01:00
let mut headers = HeaderMap ::new ( ) ;
headers . insert (
" Content-Type " ,
HeaderValue ::from_static ( " application/x-bittorrent " ) ,
) ;
if let Some ( name ) = info . name . as_ref ( ) {
2024-08-13 06:43:52 +01:00
if let Ok ( name ) = std ::str ::from_utf8 ( name ) {
2024-08-12 23:59:23 +01:00
if let Ok ( h ) =
HeaderValue ::from_str ( & format! ( " attachment; filename= \" {} .torrent \" " , name ) )
{
headers . insert ( " Content-Disposition " , h ) ;
}
}
}
Ok ( ( headers , content ) )
2024-08-12 23:50:55 +01:00
}
2024-08-09 12:13:09 +01:00
async fn torrent_playlist (
State ( state ) : State < ApiState > ,
headers : HeaderMap ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
let host = get_host ( & headers ) ? ;
let playlist_items = torrent_playlist_items ( & * state . mgr_handle ( idx ) ? ) ? ;
Ok ( build_playlist_content (
host ,
playlist_items
. into_iter ( )
. map ( move | ( file_idx , filename ) | ( idx , file_idx , filename ) ) ,
) )
}
async fn global_playlist (
State ( state ) : State < ApiState > ,
headers : HeaderMap ,
) -> Result < impl IntoResponse > {
let host = get_host ( & headers ) ? ;
let all_items = state . session ( ) . with_torrents ( | torrents | {
torrents
. filter_map ( | ( torrent_idx , handle ) | {
torrent_playlist_items ( handle )
. map ( move | items | {
items . into_iter ( ) . map ( move | ( file_idx , filename ) | {
( torrent_idx , file_idx , filename )
} )
} )
. ok ( )
} )
. flatten ( )
. collect ::< Vec < _ > > ( )
} ) ;
Ok ( build_playlist_content ( host , all_items ) )
2024-08-04 13:36:39 +02:00
}
2022-12-08 23:56:14 +00:00
async fn torrent_haves (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_dump_haves ( idx )
}
2023-11-24 15:04:36 +00:00
async fn torrent_stats_v0 (
2022-12-08 23:56:14 +00:00
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
2023-11-24 15:04:36 +00:00
state . api_stats_v0 ( idx ) . map ( axum ::Json )
}
async fn torrent_stats_v1 (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_stats_v1 ( idx ) . map ( axum ::Json )
2022-12-08 23:56:14 +00:00
}
2022-12-08 09:28:01 +00:00
2023-11-20 13:55:42 +00:00
async fn peer_stats (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
Query ( filter ) : Query < PeerStatsFilter > ,
) -> Result < impl IntoResponse > {
state . api_peer_stats ( idx , filter ) . map ( axum ::Json )
}
2024-04-24 19:10:17 +01:00
async fn torrent_stream_file (
State ( state ) : State < ApiState > ,
Path ( ( idx , file_id ) ) : Path < ( usize , usize ) > ,
headers : http ::HeaderMap ,
) -> Result < impl IntoResponse > {
let mut stream = state . api_stream ( idx , file_id ) ? ;
2024-04-24 20:56:58 +01:00
let mut status = StatusCode ::OK ;
let mut output_headers = HeaderMap ::new ( ) ;
output_headers . insert ( " Accept-Ranges " , HeaderValue ::from_static ( " bytes " ) ) ;
2024-07-27 11:44:15 +02:00
if let Ok ( mime ) = state . torrent_file_mime_type ( idx , file_id ) {
output_headers . insert (
http ::header ::CONTENT_TYPE ,
HeaderValue ::from_str ( mime ) . context ( " bug - invalid MIME " ) ? ,
) ;
}
2024-04-29 18:26:36 +01:00
let range_header = headers . get ( http ::header ::RANGE ) ;
trace! ( torrent_id = idx , file_id = file_id , range = ? range_header , " request for HTTP stream " ) ;
2024-07-20 20:03:14 +02:00
if let Some ( range ) = range_header {
2024-04-24 19:10:17 +01:00
let offset : Option < u64 > = range
. to_str ( )
. ok ( )
. and_then ( | s | s . strip_prefix ( " bytes= " ) )
. and_then ( | s | s . strip_suffix ( '-' ) )
. and_then ( | s | s . parse ( ) . ok ( ) ) ;
if let Some ( offset ) = offset {
2024-04-24 20:56:58 +01:00
status = StatusCode ::PARTIAL_CONTENT ;
2024-04-24 19:10:17 +01:00
stream
. seek ( SeekFrom ::Start ( offset ) )
. await
. context ( " error seeking " ) ? ;
2024-04-24 20:56:58 +01:00
output_headers . insert (
http ::header ::CONTENT_LENGTH ,
HeaderValue ::from_str ( & format! ( " {} " , stream . len ( ) - stream . position ( ) ) )
. context ( " bug " ) ? ,
) ;
output_headers . insert (
http ::header ::CONTENT_RANGE ,
HeaderValue ::from_str ( & format! (
" bytes {}-{}/{} " ,
stream . position ( ) ,
stream . len ( ) . saturating_sub ( 1 ) ,
stream . len ( )
) )
. context ( " bug " ) ? ,
) ;
2024-04-24 19:10:17 +01:00
}
2024-07-28 12:15:56 +02:00
} else {
output_headers . insert (
http ::header ::CONTENT_LENGTH ,
HeaderValue ::from_str ( & format! ( " {} " , stream . len ( ) ) ) . context ( " bug " ) ? ,
) ;
2024-04-24 19:10:17 +01:00
}
let s = tokio_util ::io ::ReaderStream ::new ( stream ) ;
2024-04-24 20:56:58 +01:00
Ok ( ( status , ( output_headers , axum ::body ::Body ::from_stream ( s ) ) ) )
2024-04-24 19:10:17 +01:00
}
2023-11-24 14:19:39 +00:00
async fn torrent_action_pause (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
2023-11-24 18:28:46 +00:00
state . api_torrent_action_pause ( idx ) . map ( axum ::Json )
2023-11-24 14:19:39 +00:00
}
async fn torrent_action_start (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
2023-11-24 18:28:46 +00:00
state . api_torrent_action_start ( idx ) . map ( axum ::Json )
}
async fn torrent_action_forget (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_torrent_action_forget ( idx ) . map ( axum ::Json )
}
async fn torrent_action_delete (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_torrent_action_delete ( idx ) . map ( axum ::Json )
2023-11-24 14:19:39 +00:00
}
2024-03-30 17:55:43 +00:00
#[ derive(Deserialize) ]
struct UpdateOnlyFilesRequest {
only_files : Vec < usize > ,
}
async fn torrent_action_update_only_files (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
axum ::Json ( req ) : axum ::Json < UpdateOnlyFilesRequest > ,
) -> Result < impl IntoResponse > {
state
. api_torrent_action_update_only_files ( idx , & req . only_files . into_iter ( ) . collect ( ) )
. map ( axum ::Json )
}
2023-11-25 01:24:57 +00:00
async fn set_rust_log (
State ( state ) : State < ApiState > ,
new_value : String ,
) -> Result < impl IntoResponse > {
state . api_set_rust_log ( new_value ) . map ( axum ::Json )
}
2023-12-08 19:47:48 +00:00
async fn stream_logs ( State ( state ) : State < ApiState > ) -> Result < impl IntoResponse > {
2023-12-09 14:03:42 +00:00
let s = state . api_log_lines_stream ( ) ? . map_err ( | e | {
debug! ( error = % e , " stream_logs " ) ;
e
} ) ;
2023-12-08 19:47:48 +00:00
Ok ( axum ::body ::Body ::from_stream ( s ) )
}
2023-11-20 20:15:40 +00:00
let mut app = Router ::new ( )
2022-12-08 23:56:14 +00:00
. route ( " / " , get ( api_root ) )
2023-12-08 19:47:48 +00:00
. route ( " /stream_logs " , get ( stream_logs ) )
2023-11-25 11:21:45 +00:00
. route ( " /rust_log " , post ( set_rust_log ) )
2022-12-08 23:56:14 +00:00
. route ( " /dht/stats " , get ( dht_stats ) )
. route ( " /dht/table " , get ( dht_table ) )
2023-11-25 11:21:45 +00:00
. route ( " /torrents " , get ( torrents_list ) )
2022-12-08 23:56:14 +00:00
. route ( " /torrents/:id " , get ( torrent_details ) )
. route ( " /torrents/:id/haves " , get ( torrent_haves ) )
2023-11-24 15:04:36 +00:00
. route ( " /torrents/:id/stats " , get ( torrent_stats_v0 ) )
. route ( " /torrents/:id/stats/v1 " , get ( torrent_stats_v1 ) )
2024-04-24 19:10:17 +01:00
. route ( " /torrents/:id/peer_stats " , get ( peer_stats ) )
2024-04-29 19:01:04 +01:00
. route ( " /torrents/:id/stream/:file_id " , get ( torrent_stream_file ) )
2024-08-04 13:36:39 +02:00
. route ( " /torrents/:id/playlist " , get ( torrent_playlist ) )
2024-08-09 12:13:09 +01:00
. route ( " /torrents/playlist " , get ( global_playlist ) )
2024-08-12 23:50:55 +01:00
. route ( " /torrents/resolve_magnet " , post ( resolve_magnet ) )
2024-04-29 19:01:04 +01:00
. route (
" /torrents/:id/stream/:file_id/*filename " ,
get ( torrent_stream_file ) ,
) ;
2023-11-25 11:21:45 +00:00
2023-12-08 19:47:48 +00:00
if ! self . opts . read_only {
2023-11-25 11:21:45 +00:00
app = app
. route ( " /torrents " , post ( torrents_post ) )
. route ( " /torrents/:id/pause " , post ( torrent_action_pause ) )
. route ( " /torrents/:id/start " , post ( torrent_action_start ) )
. route ( " /torrents/:id/forget " , post ( torrent_action_forget ) )
2024-03-30 17:55:43 +00:00
. route ( " /torrents/:id/delete " , post ( torrent_action_delete ) )
. route (
" /torrents/:id/update_only_files " ,
post ( torrent_action_update_only_files ) ,
) ;
2023-11-25 11:21:45 +00:00
}
2023-11-20 20:15:40 +00:00
#[ cfg(feature = " webui " ) ]
{
let webui_router = Router ::new ( )
. route (
" / " ,
get ( | | async {
(
[ ( " Content-Type " , " text/html " ) ] ,
2023-11-21 03:43:32 +00:00
include_str! ( " ../webui/dist/index.html " ) ,
2023-11-20 20:15:40 +00:00
)
} ) ,
)
. route (
2023-11-27 17:21:45 +00:00
" /assets/index.js " ,
2023-11-20 20:15:40 +00:00
get ( | | async {
(
[ ( " Content-Type " , " application/javascript " ) ] ,
2023-11-27 17:21:45 +00:00
include_str! ( " ../webui/dist/assets/index.js " ) ,
)
} ) ,
)
2023-12-14 10:37:29 +00:00
. route (
" /assets/index.css " ,
get ( | | async {
(
[ ( " Content-Type " , " text/css " ) ] ,
include_str! ( " ../webui/dist/assets/index.css " ) ,
)
} ) ,
)
2023-11-27 17:21:45 +00:00
. route (
" /assets/logo.svg " ,
get ( | | async {
(
[ ( " Content-Type " , " image/svg+xml " ) ] ,
include_str! ( " ../webui/dist/assets/logo.svg " ) ,
2023-11-20 20:15:40 +00:00
)
} ) ,
) ;
2023-12-08 19:47:48 +00:00
app = app . nest ( " /web/ " , webui_router ) ;
2024-07-20 10:08:10 +02:00
app = app . route ( " /web " , get ( | | async { Redirect ::permanent ( " /web/ " ) } ) )
2023-12-08 19:47:48 +00:00
}
2023-11-20 22:10:01 +00:00
2023-12-17 10:25:56 +00:00
let cors_layer = {
2023-12-08 19:47:48 +00:00
use tower_http ::cors ::{ AllowHeaders , AllowOrigin } ;
2023-12-17 10:25:56 +00:00
const ALLOWED_ORIGINS : [ & [ u8 ] ; 4 ] = [
// Webui-dev
b " http://localhost:3031 " ,
b " http://127.0.0.1:3031 " ,
// Tauri dev
b " http://localhost:1420 " ,
// Tauri prod
b " tauri://localhost " ,
] ;
2023-12-08 19:47:48 +00:00
tower_http ::cors ::CorsLayer ::default ( )
2023-12-17 10:25:56 +00:00
. allow_origin ( AllowOrigin ::predicate ( | v , _ | {
ALLOWED_ORIGINS . contains ( & v . as_bytes ( ) )
} ) )
2023-12-08 19:47:48 +00:00
. allow_headers ( AllowHeaders ::any ( ) )
} ;
2023-11-20 20:15:40 +00:00
let app = app
2023-12-09 00:26:14 +00:00
. layer ( cors_layer )
2023-11-20 19:52:48 +00:00
. layer ( tower_http ::trace ::TraceLayer ::new_for_http ( ) )
2023-11-20 20:15:40 +00:00
. with_state ( state )
. into_make_service ( ) ;
2022-12-08 09:28:01 +00:00
2023-12-09 00:26:14 +00:00
info! ( % addr , " starting HTTP server " ) ;
2023-12-01 11:56:07 +00:00
use tokio ::net ::TcpListener ;
2024-02-26 22:52:53 +00:00
async move {
let listener = TcpListener ::bind ( & addr )
. await
. with_context ( | | format! ( " error binding to {addr} " ) ) ? ;
axum ::serve ( listener , app ) . await ? ;
Ok ( ( ) )
}
. boxed ( )
2022-12-08 09:28:01 +00:00
}
}
2023-12-03 12:14:50 +00:00
pub ( crate ) struct OnlyFiles ( Vec < usize > ) ;
pub ( crate ) struct InitialPeers ( pub Vec < SocketAddr > ) ;
2023-12-02 12:48:03 +00:00
#[ derive(Serialize, Deserialize, Default) ]
2023-12-03 12:14:50 +00:00
pub ( crate ) struct TorrentAddQueryParams {
2023-12-02 12:48:03 +00:00
pub overwrite : Option < bool > ,
pub output_folder : Option < String > ,
pub sub_folder : Option < String > ,
pub only_files_regex : Option < String > ,
pub only_files : Option < OnlyFiles > ,
pub peer_connect_timeout : Option < u64 > ,
pub peer_read_write_timeout : Option < u64 > ,
pub initial_peers : Option < InitialPeers > ,
// Will force interpreting the content as a URL.
pub is_url : Option < bool > ,
pub list_only : Option < bool > ,
}
2023-11-22 17:19:35 +00:00
impl Serialize for OnlyFiles {
fn serialize < S > ( & self , serializer : S ) -> core ::result ::Result < S ::Ok , S ::Error >
where
S : serde ::Serializer ,
{
let s = self . 0. iter ( ) . map ( | id | id . to_string ( ) ) . join ( " , " ) ;
s . serialize ( serializer )
}
}
impl < ' de > Deserialize < ' de > for OnlyFiles {
fn deserialize < D > ( deserializer : D ) -> core ::result ::Result < Self , D ::Error >
where
D : serde ::Deserializer < ' de > ,
{
use serde ::de ::Error ;
let s = String ::deserialize ( deserializer ) ? ;
let list = s
. split ( ',' )
. try_fold ( Vec ::< usize > ::new ( ) , | mut acc , c | match c . parse ( ) {
Ok ( i ) = > {
acc . push ( i ) ;
Ok ( acc )
}
Err ( _ ) = > Err ( D ::Error ::custom ( format! (
" only_files: failed to parse {:?} as integer " ,
c
) ) ) ,
} ) ? ;
if list . is_empty ( ) {
return Err ( D ::Error ::custom (
" only_files: should contain at least one file id " ,
) ) ;
}
Ok ( OnlyFiles ( list ) )
2023-11-22 15:26:24 +00:00
}
}
2023-12-01 09:30:23 +00:00
impl < ' de > Deserialize < ' de > for InitialPeers {
fn deserialize < D > ( deserializer : D ) -> std ::prelude ::v1 ::Result < Self , D ::Error >
where
D : serde ::Deserializer < ' de > ,
{
use serde ::de ::Error ;
let string = String ::deserialize ( deserializer ) ? ;
let mut addrs = Vec ::new ( ) ;
2023-12-01 10:28:20 +00:00
for addr_str in string . split ( ',' ) . filter ( | s | ! s . is_empty ( ) ) {
2023-12-01 09:30:23 +00:00
addrs . push ( SocketAddr ::from_str ( addr_str ) . map_err ( D ::Error ::custom ) ? ) ;
}
Ok ( InitialPeers ( addrs ) )
}
}
impl Serialize for InitialPeers {
fn serialize < S > ( & self , serializer : S ) -> std ::prelude ::v1 ::Result < S ::Ok , S ::Error >
where
S : serde ::Serializer ,
{
self . 0
. iter ( )
. map ( | s | s . to_string ( ) )
. join ( " , " )
. serialize ( serializer )
}
}
2022-12-08 15:40:29 +00:00
impl TorrentAddQueryParams {
2023-12-02 12:48:03 +00:00
pub fn into_add_torrent_options ( self ) -> AddTorrentOptions {
2022-12-08 15:40:29 +00:00
AddTorrentOptions {
overwrite : self . overwrite . unwrap_or ( false ) ,
only_files_regex : self . only_files_regex ,
2023-11-22 17:19:35 +00:00
only_files : self . only_files . map ( | o | o . 0 ) ,
2022-12-08 15:40:29 +00:00
output_folder : self . output_folder ,
sub_folder : self . sub_folder ,
list_only : self . list_only . unwrap_or ( false ) ,
2023-12-01 09:30:23 +00:00
initial_peers : self . initial_peers . map ( | i | i . 0 ) ,
2023-11-30 16:05:48 +00:00
peer_opts : Some ( PeerConnectionOptions {
connect_timeout : self . peer_connect_timeout . map ( Duration ::from_secs ) ,
read_write_timeout : self . peer_read_write_timeout . map ( Duration ::from_secs ) ,
.. Default ::default ( )
} ) ,
2022-12-08 15:40:29 +00:00
.. Default ::default ( )
}
}
}