feat(server): support one shot URLs (#46)

* 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 <orhunparmaksiz@gmail.com>
This commit is contained in:
Rahul Garg 2023-05-28 14:09:14 -06:00 committed by GitHub
parent 9d153ad907
commit bd88146430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 134 additions and 25 deletions

View File

@ -32,7 +32,7 @@ The public instance is available at [https://rustypaste.shuttleapp.rs](https://r
- supports expiring links - supports expiring links
- auto-expiration of files (optional) - auto-expiration of files (optional)
- auto-deletion of expired 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 - guesses MIME types
- supports overriding and blacklisting - supports overriding and blacklisting
- supports forcing to download via `?download=true` - supports forcing to download via `?download=true`
@ -142,12 +142,18 @@ $ curl -F "file=@x.txt" -H "expire:10min" "<server_address>"
(supported units: `ns`, `us`, `ms`, `sec`, `min`, `hours`, `days`, `weeks`, `months`, `years`) (supported units: `ns`, `us`, `ms`, `sec`, `min`, `hours`, `days`, `weeks`, `months`, `years`)
#### One shot #### One shot files
```sh ```sh
$ curl -F "oneshot=@x.txt" "<server_address>" $ curl -F "oneshot=@x.txt" "<server_address>"
``` ```
#### One shot URLs
```sh
$ curl -F "oneshot_url=https://example.com" "<server_address>"
```
#### URL shortening #### URL shortening
```sh ```sh

View File

@ -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

View File

@ -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
}

View File

@ -52,7 +52,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
// Create necessary directories. // Create necessary directories.
fs::create_dir_all(&server_config.upload_path)?; 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))?; fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?;
} }

View File

@ -23,6 +23,8 @@ pub enum PasteType {
Oneshot, Oneshot,
/// A file that only contains an URL. /// A file that only contains an URL.
Url, Url,
/// A oneshot url.
OneshotUrl,
} }
impl<'a> TryFrom<&'a ContentDisposition> for PasteType { impl<'a> TryFrom<&'a ContentDisposition> for PasteType {
@ -34,6 +36,8 @@ impl<'a> TryFrom<&'a ContentDisposition> for PasteType {
Ok(Self::RemoteFile) Ok(Self::RemoteFile)
} else if content_disposition.has_form_field("oneshot") { } else if content_disposition.has_form_field("oneshot") {
Ok(Self::Oneshot) Ok(Self::Oneshot)
} else if content_disposition.has_form_field("oneshot_url") {
Ok(Self::OneshotUrl)
} else if content_disposition.has_form_field("url") { } else if content_disposition.has_form_field("url") {
Ok(Self::Url) Ok(Self::Url)
} else { } else {
@ -49,6 +53,7 @@ impl PasteType {
Self::File | Self::RemoteFile => String::new(), Self::File | Self::RemoteFile => String::new(),
Self::Oneshot => String::from("oneshot"), Self::Oneshot => String::from("oneshot"),
Self::Url => String::from("url"), Self::Url => String::from("url"),
Self::OneshotUrl => String::from("oneshot_url"),
} }
} }
@ -220,8 +225,9 @@ impl Paste {
.paste .paste
.random_url .random_url
.generate() .generate()
.unwrap_or_else(|| PasteType::Url.get_dir()); .unwrap_or_else(|| self.type_.get_dir());
let mut path = PasteType::Url let mut path = self
.type_
.get_path(&config.server.upload_path) .get_path(&config.server.upload_path)
.join(&file_name); .join(&file_name);
if let Some(timestamp) = expiry_date { if let Some(timestamp) = expiry_date {

View File

@ -62,7 +62,7 @@ async fn serve(
let mut path = util::glob_match_file(path)?; let mut path = util::glob_match_file(path)?;
let mut paste_type = PasteType::File; let mut paste_type = PasteType::File;
if !path.exists() || path.is_dir() { 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 = type_.get_path(&config.server.upload_path).join(&*file);
let alt_path = util::glob_match_file(alt_path)?; let alt_path = util::glob_match_file(alt_path)?;
if alt_path.exists() if alt_path.exists()
@ -105,6 +105,16 @@ async fn serve(
PasteType::Url => Ok(HttpResponse::Found() PasteType::Url => Ok(HttpResponse::Found()
.append_header(("Location", fs::read_to_string(&path)?)) .append_header(("Location", fs::read_to_string(&path)?))
.finish()), .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 if paste_type != PasteType::Oneshot
&& paste_type != PasteType::RemoteFile && paste_type != PasteType::RemoteFile
&& paste_type != PasteType::OneshotUrl
&& expiry_date.is_none() && expiry_date.is_none()
&& !config && !config
.read() .read()
@ -241,7 +252,7 @@ async fn upload(
.store_remote_file(expiry_date, &client, &config) .store_remote_file(expiry_date, &client, &config)
.await? .await?
} }
PasteType::Url => { PasteType::Url | PasteType::OneshotUrl => {
let config = config let config = config
.read() .read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
@ -759,4 +770,56 @@ mod tests {
Ok(()) 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(())
}
} }

View File

@ -55,24 +55,29 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
/// ///
/// Fail-safe, omits errors. /// Fail-safe, omits errors.
pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> { pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
[PasteType::File, PasteType::Oneshot, PasteType::Url] [
.into_iter() PasteType::File,
.filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok()) PasteType::Oneshot,
.flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>()) PasteType::Url,
.filter(|path| { PasteType::OneshotUrl,
if let Some(extension) = path ]
.extension() .into_iter()
.and_then(|v| v.to_str()) .filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok())
.and_then(|v| v.parse().ok()) .flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>())
{ .filter(|path| {
get_system_time() if let Some(extension) = path
.map(|system_time| system_time > Duration::from_millis(extension)) .extension()
.unwrap_or(false) .and_then(|v| v.to_str())
} else { .and_then(|v| v.parse().ok())
false {
} get_system_time()
}) .map(|system_time| system_time > Duration::from_millis(extension))
.collect() .unwrap_or(false)
} else {
false
}
})
.collect()
} }
/// Returns the SHA256 digest of the given input. /// Returns the SHA256 digest of the given input.