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
- 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" "<server_address>"
(supported units: `ns`, `us`, `ms`, `sec`, `min`, `hours`, `days`, `weeks`, `months`, `years`)
#### One shot
#### One shot files
```sh
$ curl -F "oneshot=@x.txt" "<server_address>"
```
#### One shot URLs
```sh
$ curl -F "oneshot_url=https://example.com" "<server_address>"
```
#### URL shortening
```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.
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))?;
}

View File

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

View File

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

View File

@ -55,24 +55,29 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
///
/// Fail-safe, omits errors.
pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
[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::<Vec<PathBuf>>())
.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::<Vec<PathBuf>>())
.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.