1use crate::config::Config;
2use crate::file::Directory;
3use crate::header::ContentDisposition;
4use crate::util;
5use actix_web::{error, Error};
6use awc::Client;
7use std::fs::{self, File};
8use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult, Write};
9use std::path::{Path, PathBuf};
10use std::str;
11use std::sync::RwLock;
12use std::{
13 convert::{TryFrom, TryInto},
14 ops::Add,
15};
16use url::Url;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum PasteType {
21 File,
23 RemoteFile,
25 Oneshot,
27 Url,
29 OneshotUrl,
31}
32
33impl<'a> TryFrom<&'a ContentDisposition> for PasteType {
34 type Error = ();
35 fn try_from(content_disposition: &'a ContentDisposition) -> Result<Self, Self::Error> {
36 if content_disposition.has_form_field("file") {
37 Ok(Self::File)
38 } else if content_disposition.has_form_field("remote") {
39 Ok(Self::RemoteFile)
40 } else if content_disposition.has_form_field("oneshot") {
41 Ok(Self::Oneshot)
42 } else if content_disposition.has_form_field("oneshot_url") {
43 Ok(Self::OneshotUrl)
44 } else if content_disposition.has_form_field("url") {
45 Ok(Self::Url)
46 } else {
47 Err(())
48 }
49 }
50}
51
52impl PasteType {
53 pub fn get_dir(&self) -> String {
55 match self {
56 Self::File | Self::RemoteFile => String::new(),
57 Self::Oneshot => String::from("oneshot"),
58 Self::Url => String::from("url"),
59 Self::OneshotUrl => String::from("oneshot_url"),
60 }
61 }
62
63 pub fn get_path(&self, path: &Path) -> IoResult<PathBuf> {
65 let dir = self.get_dir();
66 if dir.is_empty() {
67 Ok(path.to_path_buf())
68 } else {
69 util::safe_path_join(path, Path::new(&dir))
70 }
71 }
72
73 pub fn is_oneshot(&self) -> bool {
75 self == &Self::Oneshot
76 }
77}
78
79#[derive(Debug)]
81pub struct Paste {
82 pub data: Vec<u8>,
84 pub type_: PasteType,
86}
87
88impl Paste {
89 pub fn store_file(
99 &self,
100 file_name: &str,
101 expiry_date: Option<u128>,
102 header_filename: Option<String>,
103 config: &Config,
104 ) -> Result<String, Error> {
105 let file_type = infer::get(&self.data);
106 if let Some(file_type) = file_type {
107 for mime_type in &config.paste.mime_blacklist {
108 if mime_type == file_type.mime_type() {
109 return Err(error::ErrorUnsupportedMediaType(
110 "this file type is not permitted",
111 ));
112 }
113 }
114 }
115
116 if let Some(max_dir_size) = config.server.max_upload_dir_size {
117 let file_size = u64::try_from(self.data.len()).unwrap_or_default();
118 let upload_dir = self.type_.get_path(&config.server.upload_path)?;
119 let current_size_of_upload_dir = util::get_dir_size(&upload_dir).map_err(|e| {
120 error::ErrorInternalServerError(format!("could not get directory size: {e}"))
121 })?;
122 let expected_size_of_upload_dir = current_size_of_upload_dir.add(file_size);
123 if expected_size_of_upload_dir > max_dir_size {
124 return Err(error::ErrorInsufficientStorage(
125 "upload directory size limit exceeded",
126 ));
127 }
128 }
129
130 let mut file_name = match PathBuf::from(file_name)
131 .file_name()
132 .and_then(|v| v.to_str())
133 {
134 Some("-") => String::from("stdin"),
135 Some(".") => String::from("file"),
136 Some(v) => v.to_string(),
137 None => String::from("file"),
138 };
139 if let Some(handle_spaces_config) = config.server.handle_spaces {
140 file_name = handle_spaces_config.process_filename(&file_name);
141 }
142
143 let mut path =
144 util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
145 let mut parts: Vec<&str> = file_name.split('.').collect();
146 let mut dotfile = false;
147 let mut lower_bound = 1;
148 let mut file_name = match parts[0] {
149 "" => {
150 dotfile = true;
152 lower_bound = 2;
153 format!(".{}", parts[1])
155 }
156 _ => parts[0].to_string(),
157 };
158 let mut extension = if parts.len() > lower_bound {
159 parts.remove(0);
161 if dotfile {
162 parts.remove(0);
164 }
165 parts.join(".")
166 } else {
167 file_type
168 .map(|t| t.extension())
169 .unwrap_or(&config.paste.default_extension)
170 .to_string()
171 };
172 if let Some(random_url) = &config.paste.random_url {
173 if let Some(random_text) = random_url.generate() {
174 if let Some(suffix_mode) = random_url.suffix_mode {
175 if suffix_mode {
176 extension = format!("{}.{}", random_text, extension);
177 } else {
178 file_name = random_text;
179 }
180 } else {
181 file_name = random_text;
182 }
183 }
184 }
185 path.set_file_name(file_name);
186 path.set_extension(extension);
187 if let Some(header_filename) = header_filename {
188 file_name = header_filename;
189 path.set_file_name(file_name);
190 }
191 let file_name = path
192 .file_name()
193 .map(|v| v.to_string_lossy())
194 .unwrap_or_default()
195 .to_string();
196 let file_path = util::glob_match_file(path.clone())
197 .map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
198 if file_path.is_file() && file_path.exists() {
199 return Err(error::ErrorConflict("file already exists\n"));
200 }
201 if let Some(timestamp) = expiry_date {
202 path.set_file_name(format!("{file_name}.{timestamp}"));
203 }
204 let mut buffer = File::create(&path)?;
205 buffer.write_all(&self.data)?;
206 Ok(file_name)
207 }
208
209 pub async fn store_remote_file(
218 &mut self,
219 expiry_date: Option<u128>,
220 client: &Client,
221 config: &RwLock<Config>,
222 ) -> Result<String, Error> {
223 let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?;
224 let url = Url::parse(data).map_err(error::ErrorBadRequest)?;
225 let file_name = url
226 .path_segments()
227 .and_then(|segments| segments.last())
228 .and_then(|name| if name.is_empty() { None } else { Some(name) })
229 .unwrap_or("file");
230 let mut response = client
231 .get(url.as_str())
232 .send()
233 .await
234 .map_err(error::ErrorInternalServerError)?;
235 let payload_limit = config
236 .read()
237 .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
238 .server
239 .max_content_length
240 .try_into()
241 .map_err(error::ErrorInternalServerError)?;
242 let bytes = response
243 .body()
244 .limit(payload_limit)
245 .await
246 .map_err(error::ErrorInternalServerError)?
247 .to_vec();
248 let config = config
249 .read()
250 .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
251 let bytes_checksum = util::sha256_digest(&*bytes)?;
252 self.data = bytes;
253 if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() {
254 if let Some(file) =
255 Directory::try_from(config.server.upload_path.as_path())?.get_file(bytes_checksum)
256 {
257 return Ok(file
258 .path
259 .file_name()
260 .map(|v| v.to_string_lossy())
261 .unwrap_or_default()
262 .to_string());
263 }
264 }
265 self.store_file(file_name, expiry_date, None, &config)
266 }
267
268 #[allow(deprecated)]
275 pub fn store_url(
276 &self,
277 expiry_date: Option<u128>,
278 header_filename: Option<String>,
279 config: &Config,
280 ) -> IoResult<String> {
281 let data = str::from_utf8(&self.data)
282 .map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
283 let url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
284 let mut file_name = self.type_.get_dir();
285 if let Some(random_url) = &config.paste.random_url {
286 if let Some(random_text) = random_url.generate() {
287 file_name = random_text;
288 }
289 }
290 if let Some(header_filename) = header_filename {
291 file_name = header_filename;
292 }
293 let mut path =
294 util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
295 if let Some(timestamp) = expiry_date {
296 path.set_file_name(format!("{file_name}.{timestamp}"));
297 }
298 fs::write(&path, url.to_string())?;
299 Ok(file_name)
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::random::{RandomURLConfig, RandomURLType};
307 use crate::util;
308 use actix_web::web::Data;
309 use awc::ClientBuilder;
310 use byte_unit::Byte;
311 use std::env;
312 use std::str::FromStr;
313 use std::time::Duration;
314
315 #[actix_rt::test]
316 #[allow(deprecated)]
317 async fn test_paste_data() -> Result<(), Error> {
318 let mut config = Config::default();
319 config.server.upload_path = env::current_dir()?;
320 config.paste.random_url = Some(RandomURLConfig {
321 enabled: Some(true),
322 words: Some(3),
323 separator: Some(String::from("_")),
324 type_: RandomURLType::PetName,
325 ..RandomURLConfig::default()
326 });
327 let paste = Paste {
328 data: vec![65, 66, 67],
329 type_: PasteType::File,
330 };
331 let file_name = paste.store_file("test.txt", None, None, &config)?;
332 assert_eq!("ABC", fs::read_to_string(&file_name)?);
333 assert_eq!(
334 Some("txt"),
335 PathBuf::from(&file_name)
336 .extension()
337 .and_then(|v| v.to_str())
338 );
339 fs::remove_file(file_name)?;
340
341 config.paste.random_url = Some(RandomURLConfig {
342 length: Some(4),
343 type_: RandomURLType::Alphanumeric,
344 suffix_mode: Some(true),
345 ..RandomURLConfig::default()
346 });
347 let paste = Paste {
348 data: vec![116, 101, 115, 115, 117, 115],
349 type_: PasteType::File,
350 };
351 let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
352 assert_eq!("tessus", fs::read_to_string(&file_name)?);
353 assert!(file_name.ends_with(".tar.gz"));
354 assert!(file_name.starts_with("foo."));
355 fs::remove_file(file_name)?;
356
357 config.paste.random_url = Some(RandomURLConfig {
358 length: Some(4),
359 type_: RandomURLType::Alphanumeric,
360 suffix_mode: Some(true),
361 ..RandomURLConfig::default()
362 });
363 let paste = Paste {
364 data: vec![116, 101, 115, 115, 117, 115],
365 type_: PasteType::File,
366 };
367 let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
368 assert_eq!("tessus", fs::read_to_string(&file_name)?);
369 assert!(file_name.ends_with(".tar.gz"));
370 assert!(file_name.starts_with(".foo."));
371 fs::remove_file(file_name)?;
372
373 config.paste.random_url = Some(RandomURLConfig {
374 length: Some(4),
375 type_: RandomURLType::Alphanumeric,
376 suffix_mode: Some(false),
377 ..RandomURLConfig::default()
378 });
379 let paste = Paste {
380 data: vec![116, 101, 115, 115, 117, 115],
381 type_: PasteType::File,
382 };
383 let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
384 assert_eq!("tessus", fs::read_to_string(&file_name)?);
385 assert!(file_name.ends_with(".tar.gz"));
386 fs::remove_file(file_name)?;
387
388 config.paste.default_extension = String::from("txt");
389 config.paste.random_url = None;
390 let paste = Paste {
391 data: vec![120, 121, 122],
392 type_: PasteType::File,
393 };
394 let file_name = paste.store_file(".foo", None, None, &config)?;
395 assert_eq!("xyz", fs::read_to_string(&file_name)?);
396 assert_eq!(".foo.txt", file_name);
397 fs::remove_file(file_name)?;
398
399 config.paste.default_extension = String::from("bin");
400 config.paste.random_url = Some(RandomURLConfig {
401 length: Some(10),
402 type_: RandomURLType::Alphanumeric,
403 ..RandomURLConfig::default()
404 });
405 let paste = Paste {
406 data: vec![120, 121, 122],
407 type_: PasteType::File,
408 };
409 let file_name = paste.store_file("random", None, None, &config)?;
410 assert_eq!("xyz", fs::read_to_string(&file_name)?);
411 assert_eq!(
412 Some("bin"),
413 PathBuf::from(&file_name)
414 .extension()
415 .and_then(|v| v.to_str())
416 );
417 fs::remove_file(file_name)?;
418
419 config.paste.random_url = Some(RandomURLConfig {
420 length: Some(4),
421 type_: RandomURLType::Alphanumeric,
422 suffix_mode: Some(true),
423 ..RandomURLConfig::default()
424 });
425 let paste = Paste {
426 data: vec![116, 101, 115, 115, 117, 115],
427 type_: PasteType::File,
428 };
429 let file_name = paste.store_file(
430 "filename.txt",
431 None,
432 Some("fn_from_header.txt".to_string()),
433 &config,
434 )?;
435 assert_eq!("tessus", fs::read_to_string(&file_name)?);
436 assert_eq!("fn_from_header.txt", file_name);
437 fs::remove_file(file_name)?;
438
439 config.paste.random_url = Some(RandomURLConfig {
440 length: Some(4),
441 type_: RandomURLType::Alphanumeric,
442 suffix_mode: Some(true),
443 ..RandomURLConfig::default()
444 });
445 let paste = Paste {
446 data: vec![116, 101, 115, 115, 117, 115],
447 type_: PasteType::File,
448 };
449 let file_name = paste.store_file(
450 "filename.txt",
451 None,
452 Some("fn_from_header".to_string()),
453 &config,
454 )?;
455 assert_eq!("tessus", fs::read_to_string(&file_name)?);
456 assert_eq!("fn_from_header", file_name);
457 fs::remove_file(file_name)?;
458
459 for paste_type in &[PasteType::Url, PasteType::Oneshot] {
460 fs::create_dir_all(
461 paste_type
462 .get_path(&config.server.upload_path)
463 .expect("Bad upload path"),
464 )?;
465 }
466
467 config.paste.random_url = None;
468 let paste = Paste {
469 data: vec![116, 101, 115, 116],
470 type_: PasteType::Oneshot,
471 };
472 let expiry_date = util::get_system_time()?.as_millis() + 100;
473 let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
474 let file_path = PasteType::Oneshot
475 .get_path(&config.server.upload_path)
476 .expect("Bad upload path")
477 .join(format!("{file_name}.{expiry_date}"));
478 assert_eq!("test", fs::read_to_string(&file_path)?);
479 fs::remove_file(file_path)?;
480
481 config.paste.random_url = Some(RandomURLConfig {
482 enabled: Some(true),
483 ..RandomURLConfig::default()
484 });
485 let url = String::from("https://orhun.dev/");
486 let paste = Paste {
487 data: url.as_bytes().to_vec(),
488 type_: PasteType::Url,
489 };
490 let file_name = paste.store_url(None, None, &config)?;
491 let file_path = PasteType::Url
492 .get_path(&config.server.upload_path)
493 .expect("Bad upload path")
494 .join(&file_name);
495 assert_eq!(url, fs::read_to_string(&file_path)?);
496 fs::remove_file(file_path)?;
497
498 let url = String::from("testurl.com");
499 let paste = Paste {
500 data: url.as_bytes().to_vec(),
501 type_: PasteType::Url,
502 };
503 assert!(paste.store_url(None, None, &config).is_err());
504
505 let url = String::from("https://orhun.dev/");
506 let paste = Paste {
507 data: url.as_bytes().to_vec(),
508 type_: PasteType::Url,
509 };
510 let prepared_result = paste.store_url(None, Some("prepared-name".to_string()), &config)?;
511 let file_path = PasteType::Url
512 .get_path(&config.server.upload_path)
513 .expect("Bad upload path")
514 .join(&prepared_result);
515 assert_eq!(prepared_result, "prepared-name");
516 assert_eq!(url, fs::read_to_string(&file_path)?);
517 fs::remove_file(file_path)?;
518
519 config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte");
520 let url = String::from("https://raw.githubusercontent.com/orhun/rustypaste/refs/heads/master/img/rp_test_3b5eeeee7a7326cd6141f54820e6356a0e9d1dd4021407cb1d5e9de9f034ed2f.png");
521 let mut paste = Paste {
522 data: url.as_bytes().to_vec(),
523 type_: PasteType::RemoteFile,
524 };
525 let client_data = Data::new(
526 ClientBuilder::new()
527 .timeout(Duration::from_secs(30))
528 .finish(),
529 );
530 let file_name = paste
531 .store_remote_file(None, &client_data, &RwLock::new(config.clone()))
532 .await?;
533 let file_path = PasteType::RemoteFile
534 .get_path(&config.server.upload_path)
535 .expect("Bad upload path")
536 .join(file_name);
537 assert_eq!(
538 "3b5eeeee7a7326cd6141f54820e6356a0e9d1dd4021407cb1d5e9de9f034ed2f",
539 util::sha256_digest(&*paste.data)?
540 );
541 fs::remove_file(file_path)?;
542
543 for paste_type in &[PasteType::Url, PasteType::Oneshot] {
544 fs::remove_dir(
545 paste_type
546 .get_path(&config.server.upload_path)
547 .expect("Bad upload path"),
548 )?;
549 }
550
551 Ok(())
552 }
553}