diff --git a/crates/librqbit/examples/ubuntu.rs b/crates/librqbit/examples/ubuntu.rs index 7c136de..f93d27c 100644 --- a/crates/librqbit/examples/ubuntu.rs +++ b/crates/librqbit/examples/ubuntu.rs @@ -36,12 +36,8 @@ async fn main() -> Result<(), anyhow::Error> { .add_torrent( AddTorrent::from_url(MAGNET_LINK), Some(AddTorrentOptions { - // Set this to true to allow writing on top of existing files. - // If the file is partially downloaded, librqbit will only download the - // missing pieces. - // - // Otherwise it will throw an error that the file exists. - overwrite: false, + // Allow writing on top of existing files. + overwrite: true, ..Default::default() }), ) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index c44e68d..0eb8f5b 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -38,7 +38,11 @@ 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, + read_only: bool, + ) -> anyhow::Result<()> { let state = self.inner; async fn api_root() -> impl IntoResponse { @@ -160,22 +164,26 @@ impl HttpApi { state.api_set_rust_log(new_value).map(axum::Json) } - #[allow(unused_mut)] let mut app = Router::new() .route("/", get(api_root)) + .route("/rust_log", post(set_rust_log)) .route("/dht/stats", get(dht_stats)) .route("/dht/table", get(dht_table)) - .route("/torrents", get(torrents_list).post(torrents_post)) + .route("/torrents", get(torrents_list)) .route("/torrents/:id", get(torrent_details)) .route("/torrents/:id/haves", get(torrent_haves)) .route("/torrents/:id/stats", get(torrent_stats_v0)) .route("/torrents/:id/stats/v1", get(torrent_stats_v1)) - .route("/torrents/:id/peer_stats", get(peer_stats)) - .route("/torrents/:id/pause", post(torrent_action_pause)) - .route("/torrents/:id/start", post(torrent_action_start)) - .route("/torrents/:id/forget", post(torrent_action_forget)) - .route("/torrents/:id/delete", post(torrent_action_delete)) - .route("/rust_log", post(set_rust_log)); + .route("/torrents/:id/peer_stats", get(peer_stats)); + + if !read_only { + 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)) + .route("/torrents/:id/delete", post(torrent_action_delete)); + } #[cfg(feature = "webui")] { diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 0e3cc4e..b8531a9 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -210,6 +210,8 @@ pub struct SessionOptions { pub disable_dht: bool, pub disable_dht_persistence: bool, pub persistence: bool, + // Will default to output_folder/.rqbit-session.json + pub persistence_filename: Option, pub dht_config: Option, pub peer_id: Option, pub peer_opts: Option, @@ -240,7 +242,9 @@ impl Session { Some(dht) }; let peer_opts = opts.peer_opts.unwrap_or_default(); - let session_filename = output_folder.join(".rqbit-session.json"); + let session_filename = opts + .persistence_filename + .unwrap_or_else(|| output_folder.join(".rqbit-session.json")); let session = Arc::new(Self { persistence_filename: session_filename, peer_id, diff --git a/crates/librqbit/src/torrent_state/stats.rs b/crates/librqbit/src/torrent_state/stats.rs index 8940aa7..2d68d8d 100644 --- a/crates/librqbit/src/torrent_state/stats.rs +++ b/crates/librqbit/src/torrent_state/stats.rs @@ -191,7 +191,7 @@ impl Serialize for Speed { } Tmp { mbps: self.mbps, - human_readable: format!("{:?}", self.mbps), + human_readable: format!("{}", self), } .serialize(serializer) } diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 4eed9db..bdcff52 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -1,7 +1,7 @@ import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED } from './api'; +import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR } from './api'; interface Error { text: string, @@ -171,13 +171,16 @@ const TorrentRow: React.FC<{ return `${peer_stats.live} / ${peer_stats.seen}`; } - const formatDownloadSped = () => { + const formatDownloadSpeed = () => { if (finished) { return 'Completed'; } - if (state == STATE_INITIALIZING) { - return 'Checking files'; + switch (state) { + case STATE_PAUSED: return 'Paused'; + case STATE_INITIALIZING: return 'Checking files'; + case STATE_ERROR: return 'Error'; } + return statsResponse.live?.download_speed.human_readable ?? "N/A"; } @@ -206,14 +209,14 @@ const TorrentRow: React.FC<{ {statsResponse ? <> {`${formatBytes(totalBytes)} `} - + - {formatDownloadSped()} + {formatDownloadSpeed()} {getCompletionETA(statsResponse)} {formatPeersString()} @@ -393,7 +396,7 @@ const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => { const response = await API.uploadTorrent(data, { listOnly: true }); setFileList(response.details.files); } catch (e) { - setFileListError({ text: 'Error listing torrent', details: e }); + setFileListError({ text: 'Error uploading torrent', details: e }); } finally { setLoading(false); } diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index dfdab9a..bf96634 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -77,6 +77,13 @@ struct Opts { struct ServerStartOptions { /// The output folder to write to. If not exists, it will be created. output_folder: String, + #[arg( + long = "disable-persistence", + help = "Disable server persistence. It will not read or write its state to disk." + )] + disable_persistence: bool, + #[arg(long = "persistence-filename")] + persistence_filename: Option, } #[derive(Parser)] @@ -275,11 +282,12 @@ fn main() -> anyhow::Result<()> { async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> { let logging_reload_tx = init_logging(&opts); - let sopts = SessionOptions { + let mut sopts = SessionOptions { disable_dht: opts.disable_dht, disable_dht_persistence: opts.disable_dht_persistence, dht_config: None, - persistence: true, + persistence: false, + persistence_filename: None, peer_id: None, peer_opts: Some(PeerConnectionOptions { connect_timeout: Some(opts.peer_connect_timeout), @@ -343,6 +351,9 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> match &opts.subcommand { SubCommand::Server(server_opts) => match &server_opts.subcommand { ServerSubcommand::Start(start_opts) => { + sopts.persistence = !start_opts.disable_persistence; + sopts.persistence_filename = + start_opts.persistence_filename.clone().map(PathBuf::from); let session = Session::new_with_opts( PathBuf::from(&start_opts.output_folder), spawner, @@ -358,7 +369,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> let http_api = HttpApi::new(session, Some(logging_reload_tx)); let http_api_listen_addr = opts.http_api_listen_addr; http_api - .make_http_api_and_run(http_api_listen_addr) + .make_http_api_and_run(http_api_listen_addr, false) .await .context("error starting HTTP API") } @@ -438,7 +449,9 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> spawn( "http_api", error_span!("http_api"), - http_api.clone().make_http_api_and_run(http_api_listen_addr), + http_api + .clone() + .make_http_api_and_run(http_api_listen_addr, true), ); let mut added = false; @@ -504,8 +517,11 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> if download_opts.exit_on_finish { let results = futures::future::join_all( handles.iter().map(|h| h.wait_until_completed()), - ); - results.await; + ) + .await; + if results.iter().any(|r| r.is_err()) { + anyhow::bail!("some downloads failed") + } info!("All downloads completed, exiting"); Ok(()) } else {