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