From f78e7a153890c7279f6ae709305f4aae8cb1ae92 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 7 Jul 2022 04:58:35 +0000 Subject: [PATCH] add server feature --- Cargo.lock | 1 + Cargo.toml | 4 ++- src/error.rs | 2 ++ src/main.rs | 22 ++++++++++++++ src/opthelper.rs | 24 +++++++++++++++ src/opts.rs | 59 +++++++++++++++++++++++++++++++------ src/server/mod.rs | 4 +++ src/server/service.rs | 68 +++++++++++++++++++++++++++++++++++++++++++ src/server/traits.rs | 23 +++++++++++++++ src/settings_list.rs | 17 +++++++++++ 10 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 src/server/mod.rs create mode 100644 src/server/service.rs create mode 100644 src/server/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 47ed32f..645dde8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1271,6 +1271,7 @@ dependencies = [ "html_parser", "http", "http-content-range", + "hyper", "indicatif", "int-enum", "itertools", diff --git a/Cargo.toml b/Cargo.toml index c291216..0cb64bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ gettext = "0.4" html_parser = "0.6.3" http = "0.2" http-content-range = "0.1" +hyper = { version="0.14", features = ["server"], optional = true } indicatif = "0.17.0-rc.11" int-enum = "0.4" itertools = "0.10" @@ -41,9 +42,10 @@ bindgen = { version = "0.60", optional = true } cmake = { version = "0.1", optional = true } [features] -all = ["exif", "ugoira"] +all = ["exif", "ugoira", "server"] avdict = ["bindgen", "cmake", "flagset"] exif = ["bindgen", "c_fixed_string", "cmake", "link-cplusplus", "utf16string"] +server = ["hyper"] ugoira = ["avdict", "bindgen", "cmake", "link-cplusplus"] [profile.release-with-debug] diff --git a/src/error.rs b/src/error.rs index 7673b3b..967506c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,8 @@ pub enum PixivDownloaderError { JoinError(JoinError), #[cfg(feature = "ugoira")] UgoiraError(UgoiraError), + #[cfg(feature = "server")] + Hyper(hyper::Error), } impl From<&str> for PixivDownloaderError { diff --git a/src/main.rs b/src/main.rs index d5eb115..6adac00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,8 @@ mod parser; mod pixiv_link; mod pixiv_web; mod retry_interval; +#[cfg(feature = "server")] +mod server; mod settings; mod settings_list; #[cfg(feature = "ugoira")] @@ -126,6 +128,26 @@ impl Main { Command::Download => { return self.download().await; } + #[cfg(feature = "server")] + Command::Server => { + let addr = get_helper().server(); + match server::service::start_server(&addr) { + Ok(server) => { + println!("Listening on http://{}", addr); + match server.await { + Ok(_) => {} + Err(e) => { + println!("{}", e); + } + } + } + Err(e) => { + println!("{} {}", gettext("Failed to start the server:"), e); + return 1; + } + } + return 0; + } Command::None => { return 0; } diff --git a/src/opthelper.rs b/src/opthelper.rs index 740e803..13fbb39 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -12,7 +12,15 @@ use crate::opts::CommandOpts; use crate::retry_interval::parse_retry_interval_from_json; use crate::settings::SettingStore; use std::convert::TryFrom; +#[cfg(feature = "server")] +use std::net::IpAddr; +#[cfg(feature = "server")] +use std::net::Ipv4Addr; +#[cfg(feature = "server")] +use std::net::SocketAddr; use std::ops::Deref; +#[cfg(feature = "server")] +use std::str::FromStr; use std::sync::Arc; use std::sync::RwLock; use std::sync::RwLockReadGuard; @@ -221,6 +229,22 @@ impl OptHelper { self.default_retry_interval.clone() } + #[cfg(feature = "server")] + /// Return the server + pub fn server(&self) -> SocketAddr { + match self.opt.get_ref().server { + Some(server) => { + return server; + } + None => {} + } + if self.settings.get_ref().have("server") { + let v = self.settings.get_ref().get("server").unwrap(); + return SocketAddr::from_str(v.as_str().unwrap()).unwrap(); + } + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) + } + /// Return whether to use data from webpage first. pub fn use_webpage(&self) -> bool { if self.opt.get_ref().use_webpage { diff --git a/src/opts.rs b/src/opts.rs index d102166..e8a3cf9 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -9,6 +9,8 @@ use getopts::HasArg; use getopts::Options; use std::convert::TryFrom; use std::env; +#[cfg(feature = "server")] +use std::net::SocketAddr; use std::num::ParseIntError; use std::num::TryFromIntError; use std::str::FromStr; @@ -21,6 +23,9 @@ pub enum Command { Config, /// Download an artwork Download, + #[cfg(feature = "server")] + /// Run as a server + Server, /// Already handled when parsing options, just need return 0. None, } @@ -84,6 +89,9 @@ pub struct CommandOpts { pub max_threads: Option, /// The size of the each part when downloading file. pub part_size: Option, + #[cfg(feature = "server")] + /// Server listen address + pub server: Option, } impl CommandOpts { @@ -110,9 +118,26 @@ impl CommandOpts { download_part_retry: None, max_threads: None, part_size: None, + #[cfg(feature = "server")] + server: None, } } + pub fn new_with_command + ?Sized>(cmd: &S) -> Option { + let cmd = cmd.as_ref(); + if cmd == "download" || cmd == "d" { + return Some(CommandOpts::new(Command::Download)); + } + if cmd == "config" { + return Some(CommandOpts::new(Command::Config)); + } + #[cfg(feature = "server")] + if cmd == "server" || cmd == "s" { + return Some(CommandOpts::new(Command::Server)); + } + None + } + pub fn config(&self) -> Option { if self._config.is_some() { if check_file_exists(&self._config.as_ref().unwrap()) { @@ -138,8 +163,9 @@ impl CommandOpts { } } +#[allow(unused_mut)] pub fn print_usage(prog: &str, opts: &Options) { - let brief = format!( + let mut brief = format!( "{} {} download/d [options] [] {} {} config fix [options] {} @@ -152,6 +178,15 @@ pub fn print_usage(prog: &str, opts: &Options) { prog, gettext("Print all available settings"), ); + #[cfg(feature = "server")] + { + brief += format!( + "\n{} server/s [options] [address] {}", + prog, + gettext("Run as a server") + ) + .as_str(); + } println!("{}", opts.usage(brief.as_str())); } @@ -368,14 +403,7 @@ pub fn parse_cmd() -> Option { print_usage(&argv[0], &opts); return Some(CommandOpts::new(Command::None)); } - let cmd = &result.free[0]; - let mut re = if cmd == "download" || cmd == "d" { - Some(CommandOpts::new(Command::Download)) - } else if cmd == "config" { - Some(CommandOpts::new(Command::Config)) - } else { - None - }; + let mut re = CommandOpts::new_with_command(&result.free[0]); if re.is_none() { println!("{}", gettext("Unknown command.")); print_usage(&argv[0], &opts); @@ -419,6 +447,19 @@ pub fn parse_cmd() -> Option { return None; } } + #[cfg(feature = "server")] + Command::Server => { + if result.free.len() >= 2 { + let address = &result.free[1]; + match SocketAddr::from_str(address) { + Ok(address) => re.as_mut().unwrap().server = Some(address), + Err(e) => { + println!("{} {}", gettext("Failed to parse address:"), e); + return None; + } + } + } + } Command::None => {} } if result.opt_present("config") { diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..a0a2255 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,4 @@ +/// Services +pub mod service; +/// Traits +pub mod traits; diff --git a/src/server/service.rs b/src/server/service.rs new file mode 100644 index 0000000..660a0b2 --- /dev/null +++ b/src/server/service.rs @@ -0,0 +1,68 @@ +use crate::error::PixivDownloaderError; +use hyper::server::conn::AddrIncoming; +use hyper::server::Server; +use hyper::service::Service; +use hyper::Body; +use hyper::Request; +use hyper::Response; +use std::future::Future; +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; + +pub struct PixivDownloaderSvc { + _unused: [u8; 0], +} + +impl PixivDownloaderSvc { + pub fn new() -> Self { + Self { _unused: [] } + } +} + +impl Service> for PixivDownloaderSvc { + type Response = Response; + type Error = PixivDownloaderError; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + Box::pin(async { Ok(Response::builder().body(Body::from("hello world")).unwrap()) }) + } +} + +pub struct PixivDownloaderMakeSvc { + _unused: [u8; 0], +} + +impl PixivDownloaderMakeSvc { + pub fn new() -> Self { + Self { _unused: [] } + } +} + +impl Service for PixivDownloaderMakeSvc { + type Response = PixivDownloaderSvc; + type Error = hyper::Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: T) -> Self::Future { + let fut = async move { Ok(PixivDownloaderSvc::new()) }; + Box::pin(fut) + } +} + +/// Start the server +pub fn start_server( + addr: &SocketAddr, +) -> Result, hyper::Error> { + Ok(Server::try_bind(addr)?.serve(PixivDownloaderMakeSvc::new())) +} diff --git a/src/server/traits.rs b/src/server/traits.rs new file mode 100644 index 0000000..734d2c9 --- /dev/null +++ b/src/server/traits.rs @@ -0,0 +1,23 @@ +use crate::error::PixivDownloaderError; +use hyper::Body; +use hyper::Request; +use hyper::Response; +use json::JsonValue; + +pub trait ResponseFor { + fn response(&self, res: Request) -> Result, PixivDownloaderError>; +} + +pub trait ResponseJsonFor { + fn response_json(&self, res: Request) -> Result; +} + +impl ResponseFor for T +where + T: ResponseJsonFor, +{ + fn response(&self, res: Request) -> Result, PixivDownloaderError> { + let re = self.response_json(res)?; + Ok(Response::new(Body::from(re.to_string()))) + } +} diff --git a/src/settings_list.rs b/src/settings_list.rs index 38d7ccc..f3bd24d 100644 --- a/src/settings_list.rs +++ b/src/settings_list.rs @@ -8,6 +8,10 @@ use crate::opt::author_name_filter::check_author_name_filters; use crate::opt::proxy::check_proxy; use crate::opt::size::parse_u32_size; use json::JsonValue; +#[cfg(feature = "server")] +use std::net::SocketAddr; +#[cfg(feature = "server")] +use std::str::FromStr; pub fn get_settings_list() -> Vec { vec![ @@ -30,6 +34,8 @@ pub fn get_settings_list() -> Vec { SettingDes::new("max-threads", gettext("The maximun threads when downloading file."), JsonValueType::Number, Some(check_u64)).unwrap(), SettingDes::new("part-size", gettext("The size of the each part when downloading file."), JsonValueType::Number, Some(check_parse_size_u32)).unwrap(), SettingDes::new("proxy", gettext("Proxy settings."), JsonValueType::Array, Some(check_proxy)).unwrap(), + #[cfg(feature = "server")] + SettingDes::new("server", gettext("Server address."), JsonValueType::Str, Some(check_socket_addr)).unwrap(), ] } @@ -38,6 +44,17 @@ fn check_i64(obj: &JsonValue) -> bool { r.is_some() } +#[cfg(feature = "server")] +fn check_socket_addr(obj: &JsonValue) -> bool { + match obj.as_str() { + Some(s) => match SocketAddr::from_str(s) { + Ok(_) => true, + Err(_) => false, + } + None => false, + } +} + fn check_u64(obj: &JsonValue) -> bool { let r = obj.as_u64(); r.is_some()