From bd881464307e9d0d3182e86e2b2ae6a07d6894a8 Mon Sep 17 00:00:00 2001 From: Rahul Garg <3199183+vihu@users.noreply.github.com> Date: Sun, 28 May 2023 14:09:14 -0600 Subject: [PATCH] feat(server): support one shot URLs (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for OneshotUrl * Review cmt: Update README * docs(oneshot): update documentation about oneshot URLs * Review cmt: revert fmt * test(fixtures): add fixture test for oneshot URLs --------- Co-authored-by: Orhun Parmaksız --- README.md | 10 +++- fixtures/test-oneshot-url/config.toml | 9 ++++ fixtures/test-oneshot-url/test.sh | 20 ++++++++ src/main.rs | 2 +- src/paste.rs | 10 +++- src/server.rs | 67 ++++++++++++++++++++++++++- src/util.rs | 41 +++++++++------- 7 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 fixtures/test-oneshot-url/config.toml create mode 100755 fixtures/test-oneshot-url/test.sh diff --git a/README.md b/README.md index f50cc62..ca459f6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The public instance is available at [https://rustypaste.shuttleapp.rs](https://r - supports expiring links - auto-expiration of files (optional) - auto-deletion of expired files (optional) - - supports one shot links (can only be viewed once) + - supports one shot links/URLs (can only be viewed once) - guesses MIME types - supports overriding and blacklisting - supports forcing to download via `?download=true` @@ -142,12 +142,18 @@ $ curl -F "file=@x.txt" -H "expire:10min" "" (supported units: `ns`, `us`, `ms`, `sec`, `min`, `hours`, `days`, `weeks`, `months`, `years`) -#### One shot +#### One shot files ```sh $ curl -F "oneshot=@x.txt" "" ``` +#### One shot URLs + +```sh +$ curl -F "oneshot_url=https://example.com" "" +``` + #### URL shortening ```sh diff --git a/fixtures/test-oneshot-url/config.toml b/fixtures/test-oneshot-url/config.toml new file mode 100644 index 0000000..bef300e --- /dev/null +++ b/fixtures/test-oneshot-url/config.toml @@ -0,0 +1,9 @@ +[server] +address="127.0.0.1:8000" +max_content_length="10MB" +upload_path="./upload" + +[paste] +random_url = { enabled = false, type = "petname", words = 2, separator = "-" } +default_extension = "txt" +duplicate_files = false diff --git a/fixtures/test-oneshot-url/test.sh b/fixtures/test-oneshot-url/test.sh new file mode 100755 index 0000000..e11faf4 --- /dev/null +++ b/fixtures/test-oneshot-url/test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +url="https://orhun.dev/" + +setup() { + :; +} + +run_test() { + file_url=$(curl -s -F "oneshot_url=$url" localhost:8000) + test "$url" = $(curl -Ls -w %{url_effective} -o /dev/null "$file_url") + test "$url" = "$(cat upload/oneshot_url/oneshot_url.*)" + + result="$(curl -s $file_url)" + test "file is not found or expired :(" = "$result" +} + +teardown() { + rm -r upload +} diff --git a/src/main.rs b/src/main.rs index 57782b9..d3f8248 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data>, ServerConfig, // Create necessary directories. fs::create_dir_all(&server_config.upload_path)?; - for paste_type in &[PasteType::Url, PasteType::Oneshot] { + for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] { fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?; } diff --git a/src/paste.rs b/src/paste.rs index 9a2aee8..0f8f322 100644 --- a/src/paste.rs +++ b/src/paste.rs @@ -23,6 +23,8 @@ pub enum PasteType { Oneshot, /// A file that only contains an URL. Url, + /// A oneshot url. + OneshotUrl, } impl<'a> TryFrom<&'a ContentDisposition> for PasteType { @@ -34,6 +36,8 @@ impl<'a> TryFrom<&'a ContentDisposition> for PasteType { Ok(Self::RemoteFile) } else if content_disposition.has_form_field("oneshot") { Ok(Self::Oneshot) + } else if content_disposition.has_form_field("oneshot_url") { + Ok(Self::OneshotUrl) } else if content_disposition.has_form_field("url") { Ok(Self::Url) } else { @@ -49,6 +53,7 @@ impl PasteType { Self::File | Self::RemoteFile => String::new(), Self::Oneshot => String::from("oneshot"), Self::Url => String::from("url"), + Self::OneshotUrl => String::from("oneshot_url"), } } @@ -220,8 +225,9 @@ impl Paste { .paste .random_url .generate() - .unwrap_or_else(|| PasteType::Url.get_dir()); - let mut path = PasteType::Url + .unwrap_or_else(|| self.type_.get_dir()); + let mut path = self + .type_ .get_path(&config.server.upload_path) .join(&file_name); if let Some(timestamp) = expiry_date { diff --git a/src/server.rs b/src/server.rs index 9acd355..860b65a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -62,7 +62,7 @@ async fn serve( let mut path = util::glob_match_file(path)?; let mut paste_type = PasteType::File; if !path.exists() || path.is_dir() { - for type_ in &[PasteType::Url, PasteType::Oneshot] { + for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] { let alt_path = type_.get_path(&config.server.upload_path).join(&*file); let alt_path = util::glob_match_file(alt_path)?; if alt_path.exists() @@ -105,6 +105,16 @@ async fn serve( PasteType::Url => Ok(HttpResponse::Found() .append_header(("Location", fs::read_to_string(&path)?)) .finish()), + PasteType::OneshotUrl => { + let resp = HttpResponse::Found() + .append_header(("Location", fs::read_to_string(&path)?)) + .finish(); + fs::rename( + &path, + path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())), + )?; + Ok(resp) + } } } @@ -199,6 +209,7 @@ async fn upload( } if paste_type != PasteType::Oneshot && paste_type != PasteType::RemoteFile + && paste_type != PasteType::OneshotUrl && expiry_date.is_none() && !config .read() @@ -241,7 +252,7 @@ async fn upload( .store_remote_file(expiry_date, &client, &config) .await? } - PasteType::Url => { + PasteType::Url | PasteType::OneshotUrl => { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; @@ -759,4 +770,56 @@ mod tests { Ok(()) } + + #[actix_web::test] + async fn test_upload_oneshot_url() -> Result<(), Error> { + let mut config = Config::default(); + config.server.upload_path = env::current_dir()?; + config.server.max_content_length = Byte::from_bytes(100); + + let oneshot_url_suffix = "oneshot_url"; + + let app = test::init_service( + App::new() + .app_data(Data::new(RwLock::new(config.clone()))) + .app_data(Data::new(Client::default())) + .configure(configure_routes), + ) + .await; + + let url_upload_path = PasteType::OneshotUrl.get_path(&config.server.upload_path); + fs::create_dir_all(&url_upload_path)?; + + let response = test::call_service( + &app, + get_multipart_request( + env!("CARGO_PKG_HOMEPAGE"), + oneshot_url_suffix, + oneshot_url_suffix, + ) + .to_request(), + ) + .await; + assert_eq!(StatusCode::OK, response.status()); + assert_body( + response, + &format!("http://localhost:8080/{}\n", oneshot_url_suffix), + ) + .await?; + + // Make the oneshot_url request, ensure it is found. + let serve_request = TestRequest::with_uri(&format!("/{}", oneshot_url_suffix)).to_request(); + let response = test::call_service(&app, serve_request).await; + assert_eq!(StatusCode::FOUND, response.status()); + + // Make the same request again, and ensure that the oneshot_url is not found. + let serve_request = TestRequest::with_uri(&format!("/{}", oneshot_url_suffix)).to_request(); + let response = test::call_service(&app, serve_request).await; + assert_eq!(StatusCode::NOT_FOUND, response.status()); + + // Cleanup + fs::remove_dir_all(url_upload_path)?; + + Ok(()) + } } diff --git a/src/util.rs b/src/util.rs index d4126e2..ebc0235 100644 --- a/src/util.rs +++ b/src/util.rs @@ -55,24 +55,29 @@ pub fn glob_match_file(mut path: PathBuf) -> Result { /// /// Fail-safe, omits errors. pub fn get_expired_files(base_path: &Path) -> Vec { - [PasteType::File, PasteType::Oneshot, PasteType::Url] - .into_iter() - .filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok()) - .flat_map(|glob| glob.filter_map(|v| v.ok()).collect::>()) - .filter(|path| { - if let Some(extension) = path - .extension() - .and_then(|v| v.to_str()) - .and_then(|v| v.parse().ok()) - { - get_system_time() - .map(|system_time| system_time > Duration::from_millis(extension)) - .unwrap_or(false) - } else { - false - } - }) - .collect() + [ + PasteType::File, + PasteType::Oneshot, + PasteType::Url, + PasteType::OneshotUrl, + ] + .into_iter() + .filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok()) + .flat_map(|glob| glob.filter_map(|v| v.ok()).collect::>()) + .filter(|path| { + if let Some(extension) = path + .extension() + .and_then(|v| v.to_str()) + .and_then(|v| v.parse().ok()) + { + get_system_time() + .map(|system_time| system_time > Duration::from_millis(extension)) + .unwrap_or(false) + } else { + false + } + }) + .collect() } /// Returns the SHA256 digest of the given input.