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:
parent
9d153ad907
commit
bd88146430
10
README.md
10
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" "<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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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))?;
|
||||
}
|
||||
|
||||
|
|
10
src/paste.rs
10
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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
41
src/util.rs
41
src/util.rs
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue