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
|
- 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
|
||||||
|
|
|
@ -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.
|
// 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))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
src/paste.rs
10
src/paste.rs
|
@ -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 {
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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.
|
/// 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.
|
||||||
|
|
Loading…
Reference in New Issue