Better error display in UI

This commit is contained in:
Igor Katson 2023-12-01 11:28:35 +00:00
parent 414b2c5f65
commit 4078eacf1d
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
7 changed files with 92 additions and 51 deletions

View file

@ -23,6 +23,7 @@ use crate::http_api_error::{ApiError, ApiErrorExt};
use crate::peer_connection::PeerConnectionOptions; use crate::peer_connection::PeerConnectionOptions;
use crate::session::{ use crate::session::{
AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, TorrentId, AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, TorrentId,
SUPPORTED_SCHEMES,
}; };
use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot}; use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot};
use crate::torrent_state::stats::{LiveStats, TorrentStats}; use crate::torrent_state::stats::{LiveStats, TorrentStats};
@ -88,10 +89,29 @@ impl HttpApi {
Query(params): Query<TorrentAddQueryParams>, Query(params): Query<TorrentAddQueryParams>,
data: Bytes, data: Bytes,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
let is_url = params.is_url;
let opts = params.into_add_torrent_options(); let opts = params.into_add_torrent_options();
let add = match String::from_utf8(data.to_vec()) { let data = data.to_vec();
Ok(s) => AddTorrent::Url(s.into()), let add = match is_url {
Err(e) => AddTorrent::TorrentFileBytes(e.into_bytes().into()), 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()),
}; };
state.api_add_torrent(add, Some(opts)).await.map(axum::Json) state.api_add_torrent(add, Some(opts)).await.map(axum::Json)
} }
@ -366,6 +386,8 @@ pub struct TorrentAddQueryParams {
pub peer_connect_timeout: Option<u64>, pub peer_connect_timeout: Option<u64>,
pub peer_read_write_timeout: Option<u64>, pub peer_read_write_timeout: Option<u64>,
pub initial_peers: Option<InitialPeers>, pub initial_peers: Option<InitialPeers>,
// Will force interpreting the content as a URL.
pub is_url: Option<bool>,
pub list_only: Option<bool>, pub list_only: Option<bool>,
} }

View file

@ -118,7 +118,7 @@ impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind { match &self.kind {
ApiErrorKind::TorrentNotFound(idx) => write!(f, "torrent {idx} not found"), ApiErrorKind::TorrentNotFound(idx) => write!(f, "torrent {idx} not found"),
ApiErrorKind::Other(err) => write!(f, "{err:#}"), ApiErrorKind::Other(err) => write!(f, "{err:?}"),
ApiErrorKind::DhtDisabled => write!(f, "DHT is disabled"), ApiErrorKind::DhtDisabled => write!(f, "DHT is disabled"),
} }
} }

View file

@ -150,14 +150,14 @@ pub struct Session {
async fn torrent_from_url(url: &str) -> anyhow::Result<TorrentMetaV1Owned> { async fn torrent_from_url(url: &str) -> anyhow::Result<TorrentMetaV1Owned> {
let response = reqwest::get(url) let response = reqwest::get(url)
.await .await
.with_context(|| format!("error downloading torrent metadata from {url}"))?; .context("error downloading torrent metadata")?;
if !response.status().is_success() { if !response.status().is_success() {
anyhow::bail!("GET {} returned {}", url, response.status()) anyhow::bail!("GET {} returned {}", url, response.status())
} }
let b = response let b = response
.bytes() .bytes()
.await .await
.with_context(|| format!("error reading repsonse body from {url}"))?; .with_context(|| format!("error reading response body from {url}"))?;
torrent_from_bytes(&b).context("error decoding torrent") torrent_from_bytes(&b).context("error decoding torrent")
} }

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
"src": "assets/logo.svg" "src": "assets/logo.svg"
}, },
"index.html": { "index.html": {
"file": "assets/index-75fed916.js", "file": "assets/index-3dee43e7.js",
"isEntry": true, "isEntry": true,
"src": "index.html" "src": "index.html"
} }

View file

@ -167,6 +167,9 @@ export const API = {
if (opts.initialPeers) { if (opts.initialPeers) {
url += `&initial_peers=${opts.initialPeers.join(',')}`; url += `&initial_peers=${opts.initialPeers.join(',')}`;
} }
if (typeof data === 'string') {
url += '&is_url=true';
}
return makeRequest('POST', url, data) return makeRequest('POST', url, data)
}, },

View file

@ -356,8 +356,8 @@ const ErrorDetails = (props: { details: ErrorDetails }) => {
return null; return null;
} }
return <> return <>
{details.status && <strong>{details.status} {details.statusText}: </strong>} <p>{details.status && <strong>{details.status} {details.statusText}</strong>}</p>
{details.text} <pre>{details.text}</pre>
</> </>
} }
@ -438,7 +438,13 @@ const MagnetInput = () => {
}; };
return ( return (
<UploadButton variant='primary' buttonText="Add Torrent from Magnet / URL" onClick={onClick} data={magnet} resetData={() => setMagnet(null)} /> <UploadButton
variant='primary'
buttonText="Add Torrent from Magnet / URL"
onClick={onClick}
data={magnet}
resetData={() => setMagnet(null)}
/>
); );
}; };
@ -463,7 +469,13 @@ const FileInput = () => {
return ( return (
<> <>
<input type="file" ref={inputRef} accept=".torrent" onChange={onFileChange} className='d-none' /> <input type="file" ref={inputRef} accept=".torrent" onChange={onFileChange} className='d-none' />
<UploadButton variant='secondary' buttonText="Upload .torrent File" onClick={onClick} data={file} resetData={reset} /> <UploadButton
variant='secondary'
buttonText="Upload .torrent File"
onClick={onClick}
data={file}
resetData={reset}
/>
</> </>
); );
}; };
@ -474,7 +486,7 @@ const FileSelectionModal = (props: {
listTorrentResponse: AddTorrentResponse, listTorrentResponse: AddTorrentResponse,
listTorrentError: Error, listTorrentError: Error,
listTorrentLoading: boolean, listTorrentLoading: boolean,
data: string | File data: string | File,
}) => { }) => {
let { show, onHide, listTorrentResponse, listTorrentError, listTorrentLoading, data } = props; let { show, onHide, listTorrentResponse, listTorrentError, listTorrentLoading, data } = props;
@ -516,46 +528,50 @@ const FileSelectionModal = (props: {
).finally(() => setUploading(false)); ).finally(() => setUploading(false));
}; };
const getBody = () => {
if (listTorrentLoading) {
return <Spinner />;
} else if (listTorrentError) {
return <ErrorComponent error={listTorrentError}></ErrorComponent>;
} else if (listTorrentResponse) {
return <Form>
<fieldset className='mb-5'>
<legend>Pick the files to download</legend>
{listTorrentResponse.details.files.map((file, index) => (
<Form.Group key={index} controlId={`check-${index}`}>
<Form.Check
type="checkbox"
label={`${file.name} (${formatBytes(file.length)})`}
checked={selectedFiles.includes(index)}
onChange={() => handleToggleFile(index)}>
</Form.Check>
</Form.Group>
))}
</fieldset>
<fieldset>
<legend>Other options</legend>
<Form.Group controlId='unpopular-torrent'>
<Form.Check
type="checkbox"
label="Increase timeouts"
checked={unpopularTorrent}
onChange={() => setUnpopularTorrent(!unpopularTorrent)}>
</Form.Check>
<small id="emailHelp" className="form-text text-muted">This might be useful for unpopular torrents with few peers. It will slow down fast torrents though.</small>
</Form.Group>
</fieldset>
</Form >
}
};
return ( return (
<Modal show={show} onHide={clear} size='lg'> <Modal show={show} onHide={clear} size='lg'>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>Add torrent</Modal.Title> <Modal.Title>Add torrent</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<Form> {getBody()}
<fieldset className='mb-5'>
<legend>Pick the files to download</legend>
{listTorrentLoading ? <Spinner />
: listTorrentError ? <ErrorComponent error={listTorrentError}></ErrorComponent> :
<>
{listTorrentResponse?.details.files.map((file, index) => (
<Form.Group key={index} controlId={`check-${index}`}>
<Form.Check
type="checkbox"
label={`${file.name} (${formatBytes(file.length)})`}
checked={selectedFiles.includes(index)}
onChange={() => handleToggleFile(index)}>
</Form.Check>
</Form.Group>
))}
</>
}
</fieldset>
<fieldset>
<legend>Other options</legend>
<Form.Group controlId='unpopular-torrent'>
<Form.Check
type="checkbox"
label="Increase timeouts"
checked={unpopularTorrent}
onChange={() => setUnpopularTorrent(!unpopularTorrent)}>
</Form.Check>
<small id="emailHelp" className="form-text text-muted">This might be useful for unpopular torrents with few peers.</small>
</Form.Group>
</fieldset>
</Form>
<ErrorComponent error={uploadError} /> <ErrorComponent error={uploadError} />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>