From 99db087cf17157aa6568760471947e9e14b1bb7d Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 2 Dec 2023 15:35:26 +0000 Subject: [PATCH] Improving typescript types --- Cargo.lock | 118 ++++++++++++++++++++++++ crates/librqbit/Cargo.toml | 1 + crates/librqbit/src/peer_connection.rs | 10 +- crates/librqbit/src/session.rs | 6 +- crates/librqbit/webui/src/api-types.ts | 30 ++++-- crates/librqbit/webui/src/http-api.ts | 25 +++-- crates/librqbit/webui/src/rqbit-web.tsx | 33 ++++--- 7 files changed, 190 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 279b326..b771e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,6 +530,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -544,6 +579,16 @@ dependencies = [ "serde", ] +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -1075,6 +1120,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1093,6 +1144,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1103,6 +1155,7 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -1219,6 +1272,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "serde_with", "sha1", "size_format", "tokio", @@ -1685,6 +1739,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2083,6 +2143,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64", + "chrono", + "hex 0.4.3", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2257,6 +2346,35 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 8d44d57..513c3f8 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -62,6 +62,7 @@ hex = "0.4" backoff = "0.4.0" dashmap = "5.5.3" base64 = "0.21.5" +serde_with = "3.4.0" [dev-dependencies] futures = {version = "0.3"} diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index 9eb5702..6c65b88 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -12,6 +12,8 @@ use peer_binary_protocol::{ serialize_piece_preamble, Handshake, Message, MessageBorrowed, MessageDeserializeError, MessageOwned, PIECE_MESSAGE_DEFAULT_LEN, }; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use tokio::time::timeout; use tracing::trace; @@ -38,10 +40,16 @@ pub enum WriterRequest { Disconnect, } -#[derive(Default, Debug, Copy, Clone)] +#[serde_as] +#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] pub struct PeerConnectionOptions { + #[serde_as(as = "Option")] pub connect_timeout: Option, + + #[serde_as(as = "Option")] pub read_write_timeout: Option, + + #[serde_as(as = "Option")] pub keep_alive_interval: Option, } diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 87ca86f..381dcad 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -21,6 +21,7 @@ use librqbit_core::{ use parking_lot::RwLock; use reqwest::Url; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_with::serde_as; use tracing::{debug, error, error_span, info, warn}; use crate::{ @@ -181,7 +182,8 @@ fn compute_only_files>( Ok(only_files) } -#[derive(Default, Clone)] +#[serde_as] +#[derive(Default, Clone, Serialize, Deserialize)] pub struct AddTorrentOptions { pub paused: bool, pub only_files_regex: Option, @@ -191,9 +193,11 @@ pub struct AddTorrentOptions { pub output_folder: Option, pub sub_folder: Option, pub peer_opts: Option, + #[serde_as(as = "Option")] pub force_tracker_interval: Option, pub initial_peers: Option>, // This is used to restore the session. + #[serde(skip)] pub preferred_id: Option, } diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts index bb61424..f7b6293 100644 --- a/crates/librqbit/webui/src/api-types.ts +++ b/crates/librqbit/webui/src/api-types.ts @@ -104,16 +104,34 @@ export interface ErrorDetails { text: string, }; +export type Duration = number; + +export interface PeerConnectionOptions { + connect_timeout?: Duration | null; + read_write_timeout?: Duration | null; + keep_alive_interval?: Duration | null; +} + +export interface AddTorrentOptions { + paused?: boolean; + only_files_regex?: string | null; + only_files?: number[] | null; + overwrite?: boolean; + list_only?: boolean; + output_folder?: string | null; + sub_folder?: string | null; + peer_opts?: PeerConnectionOptions | null; + force_tracker_interval?: Duration | null; + initial_peers?: string[] | null; // Assuming SocketAddr is equivalent to a string in TypeScript + preferred_id?: number | null; +} + + export interface RqbitAPI { listTorrents: () => Promise, getTorrentDetails: (index: number) => Promise, getTorrentStats: (index: number) => Promise; - uploadTorrent: (data: string | File, opts?: { - listOnly?: boolean, - selectedFiles?: Array, - unpopularTorrent?: boolean, - initialPeers?: Array | null, - }) => Promise; + uploadTorrent: (data: string | File, opts?: AddTorrentOptions) => Promise; pause: (index: number) => Promise; start: (index: number) => Promise; diff --git a/crates/librqbit/webui/src/http-api.ts b/crates/librqbit/webui/src/http-api.ts index a52fc87..8b19ab0 100644 --- a/crates/librqbit/webui/src/http-api.ts +++ b/crates/librqbit/webui/src/http-api.ts @@ -55,25 +55,22 @@ export const API: RqbitAPI = { return makeRequest('GET', `/torrents/${index}/stats/v1`); }, - uploadTorrent: (data: string | File, opts?: { - listOnly?: boolean, - selectedFiles?: Array, - unpopularTorrent?: boolean, - initialPeers?: Array | null, - }): Promise => { - opts = opts || {}; + uploadTorrent: (data, opts): Promise => { let url = '/torrents?&overwrite=true'; - if (opts.listOnly) { + if (opts?.list_only) { url += '&list_only=true'; } - if (opts.selectedFiles != null) { - url += `&only_files=${opts.selectedFiles.join(',')}`; + if (opts?.only_files != null) { + url += `&only_files=${opts.only_files.join(',')}`; } - if (opts.unpopularTorrent) { - url += '&peer_connect_timeout=20&peer_read_write_timeout=60'; + if (opts?.peer_opts?.connect_timeout) { + url += `&peer_connect_timeout=${opts.peer_opts.connect_timeout}`; } - if (opts.initialPeers) { - url += `&initial_peers=${opts.initialPeers.join(',')}`; + if (opts?.peer_opts?.read_write_timeout) { + url += `&peer_read_write_timeout=${opts.peer_opts.read_write_timeout}`; + } + if (opts?.initial_peers) { + url += `&initial_peers=${opts.initial_peers.join(',')}`; } if (typeof data === 'string') { url += '&is_url=true'; diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 3e656e9..f10df2f 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -1,6 +1,6 @@ import { MouseEventHandler, RefObject, createContext, useContext, useEffect, useRef, useState } from 'react'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentId, TorrentStats, ErrorDetails as ApiErrorDetails, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR, RqbitAPI, ErrorDetails, ListTorrentsResponse } from './api-types'; +import { AddTorrentResponse, TorrentDetails, TorrentId, TorrentStats, ErrorDetails as ApiErrorDetails, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR, RqbitAPI, ErrorDetails, ListTorrentsResponse, AddTorrentOptions } from './api-types'; interface Error { text: string, @@ -13,28 +13,28 @@ interface ContextType { } export const APIContext = createContext({ - listTorrents: function (): Promise { + listTorrents: () => { throw new Error('Function not implemented.'); }, - getTorrentDetails: function (index: number): Promise { + getTorrentDetails: () => { throw new Error('Function not implemented.'); }, - getTorrentStats: function (index: number): Promise { + getTorrentStats: () => { throw new Error('Function not implemented.'); }, - uploadTorrent: function (data: string | File, opts?: { listOnly?: boolean | undefined; selectedFiles?: number[] | undefined; unpopularTorrent?: boolean | undefined; initialPeers?: string[] | null | undefined; } | undefined): Promise { + uploadTorrent: () => { throw new Error('Function not implemented.'); }, - pause: function (index: number): Promise { + pause: () => { throw new Error('Function not implemented.'); }, - start: function (index: number): Promise { + start: () => { throw new Error('Function not implemented.'); }, - forget: function (index: number): Promise { + forget: () => { throw new Error('Function not implemented.'); }, - delete: function (index: number): Promise { + delete: () => { throw new Error('Function not implemented.'); } }); @@ -439,7 +439,7 @@ const UploadButton: React.FC<{ let t = setTimeout(async () => { setLoading(true); try { - const response = await API.uploadTorrent(data, { listOnly: true }); + const response = await API.uploadTorrent(data, { list_only: true }); setListTorrentResponse(response); } catch (e) { setListTorrentError({ text: 'Error listing torrent files', details: e as ErrorDetails }); @@ -624,7 +624,18 @@ const FileSelectionModal = (props: { } setUploading(true); let initialPeers = listTorrentResponse.seen_peers ? listTorrentResponse.seen_peers.slice(0, 32) : null; - API.uploadTorrent(data, { selectedFiles, unpopularTorrent, initialPeers }).then(() => { + let opts: AddTorrentOptions = { + overwrite: true, + only_files: selectedFiles, + initial_peers: initialPeers, + }; + if (unpopularTorrent) { + opts.peer_opts = { + connect_timeout: 20, + read_write_timeout: 60, + }; + } + API.uploadTorrent(data, opts).then(() => { onHide(); ctx.refreshTorrents(); },