1use crate::config::Config;
2use crate::error::{Error, Result};
3use indicatif::{ProgressBar, ProgressStyle};
4use multipart::client::lazy::Multipart;
5use serde::Deserialize;
6use std::io::{Read, Result as IoResult, Write};
7use std::time::Duration;
8use ureq::Error as UreqError;
9use ureq::{Agent, AgentBuilder};
10use url::Url;
11
12const DEFAULT_FILE_NAME: Option<&str> = Some("file");
14
15const EXPIRATION_HEADER: &str = "expire";
17
18const FILENAME_HEADER: &str = "filename";
20
21#[derive(Deserialize, Debug)]
23pub struct ListItem {
24 pub file_name: String,
26 pub file_size: u64,
28 #[serde(default = "creation_date_utc_default")]
30 pub creation_date_utc: String,
31 pub expires_at_utc: Option<String>,
33}
34
35fn creation_date_utc_default() -> String {
36 "info not available".to_string()
37}
38
39#[derive(Debug)]
41pub struct UploadResult<'a, T>(pub &'a str, pub Result<T>);
42
43#[derive(Debug)]
45pub struct UploadTracker<'a, R: Read> {
46 inner: R,
48 progress_bar: &'a ProgressBar,
50 uploaded: usize,
52}
53
54impl<'a, R: Read> UploadTracker<'a, R> {
55 pub fn new(progress_bar: &'a ProgressBar, total: u64, reader: R) -> Result<Self> {
57 progress_bar.set_style(
58 ProgressStyle::default_bar()
59 .template("{msg:.green.bold} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")?
60 .progress_chars("#>-"),
61 );
62 progress_bar.set_length(total);
63 progress_bar.reset_elapsed();
64 Ok(Self {
65 inner: reader,
66 progress_bar,
67 uploaded: 0,
68 })
69 }
70}
71
72impl<R: Read> Read for UploadTracker<'_, R> {
73 fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
74 let bytes_read = self.inner.read(buf)?;
75 self.uploaded += bytes_read;
76 self.progress_bar.set_position(self.uploaded as u64);
77 Ok(bytes_read)
78 }
79}
80
81#[derive(Debug)]
83pub struct Uploader<'a> {
84 client: Agent,
86 config: &'a Config,
88}
89
90impl<'a> Uploader<'a> {
91 pub fn new(config: &'a Config) -> Self {
93 Self {
94 client: AgentBuilder::new()
95 .user_agent(&format!(
96 "{}/{}",
97 env!("CARGO_PKG_NAME"),
98 env!("CARGO_PKG_VERSION")
99 ))
100 .build(),
101 config,
102 }
103 }
104
105 pub fn upload_file(&self, file: &'a str) -> UploadResult<'a, String> {
107 let field = if self.config.paste.oneshot == Some(true) {
108 "oneshot"
109 } else {
110 "file"
111 };
112 let mut multipart = Multipart::new();
113 multipart.add_file(field, file);
114
115 UploadResult(file, self.upload(multipart))
116 }
117
118 pub fn upload_url(&self, url: &'a str) -> UploadResult<'a, String> {
120 let field = if self.config.paste.oneshot == Some(true) {
121 "oneshot_url"
122 } else {
123 "url"
124 };
125
126 if let Err(e) = Url::parse(url) {
127 UploadResult(url, Err(e.into()))
128 } else {
129 let mut multipart = Multipart::new();
130 multipart.add_stream::<_, &[u8], &str>(field, url.as_bytes(), None, None);
131 UploadResult(url, self.upload(multipart))
132 }
133 }
134
135 pub fn upload_remote_url(&self, url: &'a str) -> UploadResult<'a, String> {
137 if let Err(e) = Url::parse(url) {
138 UploadResult(url, Err(e.into()))
139 } else {
140 let mut multipart = Multipart::new();
141 multipart.add_stream::<_, &[u8], &str>("remote", url.as_bytes(), None, None);
142 UploadResult(url, self.upload(multipart))
143 }
144 }
145
146 pub fn upload_stream<S: Read>(&self, stream: S) -> UploadResult<'a, String> {
148 let field = if self.config.paste.oneshot == Some(true) {
149 "oneshot"
150 } else {
151 "file"
152 };
153 let mut multipart = Multipart::new();
154 multipart.add_stream(field, stream, DEFAULT_FILE_NAME, None);
155
156 UploadResult("stream", self.upload(multipart))
157 }
158
159 fn upload(&self, mut multipart: Multipart<'static, '_>) -> Result<String> {
161 let multipart_data = multipart.prepare()?;
162 let mut request = self.client.post(&self.config.server.address).set(
163 "Content-Type",
164 &format!(
165 "multipart/form-data; boundary={}",
166 multipart_data.boundary()
167 ),
168 );
169 if let Some(content_len) = multipart_data.content_len() {
170 request = request.set("Content-Length", &content_len.to_string());
171 }
172 if let Some(auth_token) = &self.config.server.auth_token {
173 request = request.set("Authorization", auth_token);
174 }
175 if let Some(expiration_time) = &self.config.paste.expire {
176 request = request.set(EXPIRATION_HEADER, expiration_time);
177 }
178 if let Some(filename) = &self.config.paste.filename {
179 request = request.set(FILENAME_HEADER, filename);
180 }
181 let progress_bar = ProgressBar::new_spinner();
182 progress_bar.enable_steady_tick(Duration::from_millis(80));
183 progress_bar.set_message("Uploading");
184 let upload_tracker = UploadTracker::new(
185 &progress_bar,
186 multipart_data.content_len().unwrap_or_default(),
187 multipart_data,
188 )?;
189 let result = match request.send(upload_tracker) {
190 Ok(response) => {
191 let status = response.status();
192 let response_text = response.into_string()?;
193 if response_text.lines().count() != 1 {
194 Err(Error::UploadError(format!(
195 "server returned invalid body (status code: {status})"
196 )))
197 } else if status == 200 {
198 Ok(response_text)
199 } else {
200 Err(Error::UploadError(format!(
201 "unknown error (status code: {status})"
202 )))
203 }
204 }
205 Err(UreqError::Status(code, response)) => Err(Error::UploadError(format!(
206 "{} (status code: {})",
207 response.into_string()?.trim(),
208 code
209 ))),
210 Err(e) => Err(Error::RequestError(Box::new(e))),
211 };
212 progress_bar.finish_and_clear();
213 result
214 }
215
216 pub fn delete_file(&self, file: &'a str) -> UploadResult<'a, String> {
218 UploadResult(file, self.delete(file))
219 }
220
221 fn delete(&self, file: &'a str) -> Result<String> {
223 let url = self.retrieve_url(file)?;
224 let mut request = self.client.delete(url.as_str());
225 if let Some(delete_token) = &self.config.server.delete_token {
226 request = request.set("Authorization", delete_token);
227 }
228 let result = match request.call() {
229 Ok(response) => {
230 let status = response.status();
231 let response_text = response.into_string()?;
232 if status == 200 {
233 Ok(response_text)
234 } else {
235 Err(Error::DeleteError(format!(
236 "unknown error (status code: {status})"
237 )))
238 }
239 }
240 Err(UreqError::Status(code, response)) => {
241 if code == 404 {
242 Err(Error::DeleteError(
243 response.into_string()?.trim().to_string(),
244 ))
245 } else {
246 Err(Error::DeleteError(format!(
247 "{} (status code: {})",
248 response.into_string()?.trim(),
249 code
250 )))
251 }
252 }
253 Err(e) => Err(Error::RequestError(Box::new(e))),
254 };
255 result
256 }
257
258 pub fn retrieve_url(&self, endpoint: &str) -> Result<Url> {
260 let mut url = Url::parse(&self.config.server.address)?;
261 if !url.path().to_string().ends_with('/') {
262 url = url.join(&format!("{}/", url.path()))?;
263 }
264 url = url.join(endpoint)?;
265 Ok(url)
266 }
267
268 pub fn retrieve_version(&self) -> Result<String> {
270 let url = self.retrieve_url("version")?;
271 let mut request = self.client.get(url.as_str());
272 if let Some(auth_token) = &self.config.server.auth_token {
273 request = request.set("Authorization", auth_token);
274 }
275 Ok(request
276 .call()
277 .map_err(|e| Error::RequestError(Box::new(e)))?
278 .into_string()?)
279 }
280
281 pub fn retrieve_list<Output: Write>(&self, output: &mut Output, prettify: bool) -> Result<()> {
283 let url = self.retrieve_url("list")?;
284 let mut request = self.client.get(url.as_str());
285 if let Some(auth_token) = &self.config.server.auth_token {
286 request = request.set("Authorization", auth_token);
287 }
288 let response = request
289 .call()
290 .map_err(|e| Error::RequestError(Box::new(e)))?;
291 if !prettify {
292 writeln!(output, "{}", response.into_string()?)?;
293 return Ok(());
294 }
295 let items: Vec<ListItem> = response.into_json()?;
296 if items.is_empty() {
297 writeln!(output, "No files on server :(")?;
298 return Ok(());
299 }
300 let filename_width = items
301 .iter()
302 .map(|v| v.file_name.len())
303 .max()
304 .unwrap_or_default();
305 let mut filesize_width = items
306 .iter()
307 .map(|v| v.file_size)
308 .max()
309 .unwrap_or_default()
310 .to_string()
311 .len();
312 if filesize_width < 4 {
313 filesize_width = 4;
314 }
315 writeln!(
316 output,
317 "{:^filename_width$} | {:^filesize_width$} | {:^19} | {:^19}",
318 "Name", "Size", "Creation (UTC)", "Expiry (UTC)"
319 )?;
320 writeln!(
321 output,
322 "{:-<filename_width$}-|-{:->filesize_width$}-|-{:-<19}-|-{:-<19}",
323 "", "", "", ""
324 )?;
325 items.iter().try_for_each(|file_info| {
326 writeln!(
327 output,
328 "{:<filename_width$} | {:>filesize_width$} | {:<19} | {}",
329 file_info.file_name,
330 file_info.file_size,
331 file_info.creation_date_utc,
332 file_info
333 .expires_at_utc
334 .as_ref()
335 .cloned()
336 .unwrap_or_default()
337 )
338 })?;
339 Ok(())
340 }
341}