1use std::path::PathBuf;
2use std::time::Duration;
3
4use regex::Regex;
5use structopt::StructOpt;
6
7use crate::dezoomer::Dezoomer;
8
9use super::{auto, stdin_line, Vec2d, ZoomError};
10
11#[derive(StructOpt, Debug)]
12#[structopt(author, about)]
13pub struct Arguments {
14 pub input_uri: Option<String>,
16
17 #[structopt(parse(from_os_str))]
19 pub outfile: Option<PathBuf>,
20
21 #[structopt(short, long, default_value = "auto")]
23 dezoomer: String,
24
25 #[structopt(short, long)]
27 pub largest: bool,
28
29 #[structopt(short = "w", long = "max-width")]
32 max_width: Option<u32>,
33
34 #[structopt(short = "h", long = "max-height")]
37 max_height: Option<u32>,
38
39 #[structopt(short = "n", long = "parallelism", default_value = "16")]
42 pub parallelism: usize,
43
44 #[structopt(short = "r", long = "retries", default_value = "1")]
50 pub retries: usize,
51
52 #[structopt(long, default_value = "2s", parse(try_from_str = parse_duration))]
57 pub retry_delay: Duration,
58
59 #[structopt(long, default_value = "20")]
64 pub compression: u8,
65
66 #[structopt(
71 short = "H",
72 long = "header",
73 parse(try_from_str = parse_header),
74 number_of_values = 1
75 )]
76 pub headers: Vec<(String, String)>,
77
78 #[structopt(long, default_value = "32")]
80 pub max_idle_per_host: usize,
81
82 #[structopt(long)]
84 pub accept_invalid_certs: bool,
85
86 #[structopt(long, default_value = "30s", parse(try_from_str = parse_duration))]
89 pub timeout: Duration,
90
91 #[structopt(long = "connect-timeout", default_value = "6s", parse(try_from_str = parse_duration))]
93 pub connect_timeout: Duration,
94
95 #[structopt(long, default_value = "warn")]
97 pub logging: String,
98
99 #[structopt(short = "c", long = "tile-cache")]
103 pub tile_storage_folder: Option<PathBuf>,
104}
105
106impl Default for Arguments {
107 fn default() -> Self {
108 Arguments {
109 input_uri: None,
110 outfile: None,
111 dezoomer: "auto".to_string(),
112 largest: false,
113 max_width: None,
114 max_height: None,
115 parallelism: 16,
116 retries: 1,
117 compression: 20,
118 retry_delay: Duration::from_secs(2),
119 headers: vec![],
120 max_idle_per_host: 32,
121 accept_invalid_certs: false,
122 timeout: Duration::from_secs(30),
123 connect_timeout: Duration::from_secs(6),
124 logging: "warn".to_string(),
125 tile_storage_folder: None
126 }
127 }
128}
129
130impl Arguments {
131 pub fn choose_input_uri(&self) -> Result<String, ZoomError> {
132 match &self.input_uri {
133 Some(uri) => Ok(uri.clone()),
134 None => {
135 println!("Enter an URL or a path to a tiles.yaml file: ");
136 stdin_line()
137 }
138 }
139 }
140 pub fn find_dezoomer(&self) -> Result<Box<dyn Dezoomer>, ZoomError> {
141 auto::all_dezoomers(true)
142 .into_iter()
143 .find(|d| d.name() == self.dezoomer)
144 .ok_or_else(|| ZoomError::NoSuchDezoomer {
145 name: self.dezoomer.clone(),
146 })
147 }
148 pub fn best_size<I: Iterator<Item = Vec2d>>(&self, sizes: I) -> Option<Vec2d> {
149 if self.largest {
150 sizes.max_by_key(|s| s.area())
151 } else if self.max_width.is_some() || self.max_height.is_some() {
152 sizes
153 .filter(|s| {
154 self.max_width.map(|w| s.x <= w).unwrap_or(true)
155 && self.max_height.map(|h| s.y <= h).unwrap_or(true)
156 })
157 .max_by_key(|s| s.area())
158 } else {
159 None
160 }
161 }
162
163 pub fn headers(&self) -> impl Iterator<Item = (&String, &String)> {
164 self.headers.iter().map(|(k, v)| (k, v))
165 }
166}
167
168fn parse_header(s: &str) -> Result<(String, String), &'static str> {
169 let vals: Vec<&str> = s.splitn(2, ':').map(str::trim).collect();
170 if let [key, value] = vals[..] {
171 Ok((key.into(), value.into()))
172 } else {
173 Err("Invalid header format. Expected 'Name: Value'")
174 }
175}
176
177fn parse_duration(s: &str) -> Result<Duration, &'static str> {
178 let err_msg = "Invalid duration. \
179 A duration is a number followed by a unit, such as '10ms' or '5s'";
180 let re = Regex::new(r"^(\d+)\s*(min|s|ms|ns)$").unwrap();
181 let caps = re.captures(s).ok_or(err_msg)?;
182 let val: u64 = caps[1].parse().map_err(|_| err_msg)?;
183 match &caps[2] {
184 "min" => Ok(Duration::from_secs(60 * val)),
185 "s" => Ok(Duration::from_secs(val)),
186 "ms" => Ok(Duration::from_millis(val)),
187 "ns" => Ok(Duration::from_nanos(val)),
188 _ => Err(err_msg)
189 }
190}
191
192
193#[test]
194fn test_headers_and_input() -> Result<(), structopt::clap::Error> {
195 let args: Arguments = StructOpt::from_iter_safe(
196 [
197 "dezoomify-rs",
198 "--header",
199 "Referer: http://test.com",
200 "--header",
201 "User-Agent: custom",
202 "--header",
203 "A:B",
204 "input-url",
205 ]
206 .iter(),
207 )?;
208 assert_eq!(args.input_uri, Some("input-url".into()));
209 assert_eq!(
210 args.headers,
211 vec![
212 ("Referer".into(), "http://test.com".into()),
213 ("User-Agent".into(), "custom".into()),
214 ("A".into(), "B".into()),
215 ]
216 );
217 Ok(())
218}
219
220#[test]
221fn test_parse_duration() {
222 assert_eq!(parse_duration("2s"), Ok(Duration::from_secs(2)));
223 assert_eq!(parse_duration("29 s"), Ok(Duration::from_secs(29)));
224 assert_eq!(parse_duration("2min"), Ok(Duration::from_secs(120)));
225 assert_eq!(parse_duration("1000 ms"), Ok(Duration::from_secs(1)));
226 assert!(parse_duration("1 2 ms").is_err());
227 assert!(parse_duration("1 s s").is_err());
228 assert!(parse_duration("ms").is_err());
229 assert!(parse_duration("1j").is_err());
230 assert!(parse_duration("").is_err());
231}