feat(server): allow to override filename when using random_url (#233)
* feat(server): allow to override filename when using random_url * docs(README): remove line from features * refactor(header): make const private
This commit is contained in:
parent
8e6393c6f4
commit
db971e6434
|
@ -52,6 +52,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
||||||
- [Paste file from remote URL](#paste-file-from-remote-url)
|
- [Paste file from remote URL](#paste-file-from-remote-url)
|
||||||
- [Cleaning up expired files](#cleaning-up-expired-files)
|
- [Cleaning up expired files](#cleaning-up-expired-files)
|
||||||
- [Delete file from server](#delete-file-from-server)
|
- [Delete file from server](#delete-file-from-server)
|
||||||
|
- [Override the filename when using `random_url`](#override-the-filename-when-using-random_url)
|
||||||
- [Server](#server)
|
- [Server](#server)
|
||||||
- [List endpoint](#list-endpoint)
|
- [List endpoint](#list-endpoint)
|
||||||
- [HTML Form](#html-form)
|
- [HTML Form](#html-form)
|
||||||
|
@ -254,6 +255,14 @@ $ curl -H "Authorization: <auth_token>" -X DELETE "<server_address>/file.txt"
|
||||||
|
|
||||||
> The `DELETE` endpoint will not be exposed and will return `404` error if `delete_tokens` are not set.
|
> The `DELETE` endpoint will not be exposed and will return `404` error if `delete_tokens` are not set.
|
||||||
|
|
||||||
|
#### Override the filename when using `random_url`
|
||||||
|
|
||||||
|
The generation of a random filename can be overridden by sending a header called `filename`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -F "file=@x.txt" -H "filename: <file_name>" "<server_address>"
|
||||||
|
```
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
To start the server:
|
To start the server:
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
[server]
|
||||||
|
address = "127.0.0.1:8000"
|
||||||
|
max_content_length = "10MB"
|
||||||
|
upload_path = "./upload"
|
||||||
|
|
||||||
|
[paste]
|
||||||
|
random_url = { type = "alphanumeric", length = "4", suffix_mode = true }
|
||||||
|
default_extension = "txt"
|
||||||
|
duplicate_files = true
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
content="test data"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
echo "$content" > file
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
|
||||||
|
test "$file_url" = "http://localhost:8000/fn_from_header.txt"
|
||||||
|
test "$content" = "$(cat upload/fn_from_header.txt)"
|
||||||
|
test "$content" = "$(curl -s $file_url)"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
rm file
|
||||||
|
rm -r upload
|
||||||
|
}
|
|
@ -7,6 +7,9 @@ use std::time::Duration;
|
||||||
/// Custom HTTP header for expiry dates.
|
/// Custom HTTP header for expiry dates.
|
||||||
pub const EXPIRE: &str = "expire";
|
pub const EXPIRE: &str = "expire";
|
||||||
|
|
||||||
|
/// Custom HTTP header to override filename.
|
||||||
|
const FILENAME: &str = "filename";
|
||||||
|
|
||||||
/// Parses the expiry date from the [`custom HTTP header`](EXPIRE).
|
/// Parses the expiry date from the [`custom HTTP header`](EXPIRE).
|
||||||
pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u128>, ActixError> {
|
pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u128>, ActixError> {
|
||||||
if let Some(expire_time) = headers.get(EXPIRE).and_then(|v| v.to_str().ok()) {
|
if let Some(expire_time) = headers.get(EXPIRE).and_then(|v| v.to_str().ok()) {
|
||||||
|
@ -18,6 +21,15 @@ pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the filename from the header.
|
||||||
|
pub fn parse_header_filename(headers: &HeaderMap) -> Result<Option<String>, ActixError> {
|
||||||
|
if let Some(file_name) = headers.get(FILENAME).and_then(|v| v.to_str().ok()) {
|
||||||
|
Ok(Some(file_name.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrapper for Actix content disposition header.
|
/// Wrapper for Actix content disposition header.
|
||||||
///
|
///
|
||||||
/// Aims to parse the file data from multipart body.
|
/// Aims to parse the file data from multipart body.
|
||||||
|
|
62
src/paste.rs
62
src/paste.rs
|
@ -88,6 +88,7 @@ impl Paste {
|
||||||
/// - If `file_name` does not have an extension, it is replaced with [`default_extension`].
|
/// - If `file_name` does not have an extension, it is replaced with [`default_extension`].
|
||||||
/// - If `file_name` is "-", it is replaced with "stdin".
|
/// - If `file_name` is "-", it is replaced with "stdin".
|
||||||
/// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string.
|
/// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string.
|
||||||
|
/// - If `header_filename` is set, it will override the filename.
|
||||||
///
|
///
|
||||||
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
||||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||||
|
@ -95,6 +96,7 @@ impl Paste {
|
||||||
&self,
|
&self,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
expiry_date: Option<u128>,
|
expiry_date: Option<u128>,
|
||||||
|
header_filename: Option<String>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> IoResult<String> {
|
) -> IoResult<String> {
|
||||||
let file_type = infer::get(&self.data);
|
let file_type = infer::get(&self.data);
|
||||||
|
@ -166,6 +168,10 @@ impl Paste {
|
||||||
}
|
}
|
||||||
path.set_file_name(file_name);
|
path.set_file_name(file_name);
|
||||||
path.set_extension(extension);
|
path.set_extension(extension);
|
||||||
|
if let Some(header_filename) = header_filename {
|
||||||
|
file_name = header_filename;
|
||||||
|
path.set_file_name(file_name);
|
||||||
|
}
|
||||||
let file_name = path
|
let file_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|v| v.to_string_lossy())
|
.map(|v| v.to_string_lossy())
|
||||||
|
@ -235,7 +241,7 @@ impl Paste {
|
||||||
.to_string());
|
.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(self.store_file(file_name, expiry_date, &config)?)
|
Ok(self.store_file(file_name, expiry_date, None, &config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes an URL to a file in upload directory.
|
/// Writes an URL to a file in upload directory.
|
||||||
|
@ -295,7 +301,7 @@ mod tests {
|
||||||
data: vec![65, 66, 67],
|
data: vec![65, 66, 67],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("test.txt", None, &config)?;
|
let file_name = paste.store_file("test.txt", None, None, &config)?;
|
||||||
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some("txt"),
|
Some("txt"),
|
||||||
|
@ -315,7 +321,7 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
|
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||||
assert!(file_name.ends_with(".tar.gz"));
|
assert!(file_name.ends_with(".tar.gz"));
|
||||||
assert!(file_name.starts_with("foo."));
|
assert!(file_name.starts_with("foo."));
|
||||||
|
@ -331,7 +337,7 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file(".foo.tar.gz", None, &config)?;
|
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||||
assert!(file_name.ends_with(".tar.gz"));
|
assert!(file_name.ends_with(".tar.gz"));
|
||||||
assert!(file_name.starts_with(".foo."));
|
assert!(file_name.starts_with(".foo."));
|
||||||
|
@ -347,7 +353,7 @@ mod tests {
|
||||||
data: vec![116, 101, 115, 115, 117, 115],
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
|
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||||
assert!(file_name.ends_with(".tar.gz"));
|
assert!(file_name.ends_with(".tar.gz"));
|
||||||
fs::remove_file(file_name)?;
|
fs::remove_file(file_name)?;
|
||||||
|
@ -358,7 +364,7 @@ mod tests {
|
||||||
data: vec![120, 121, 122],
|
data: vec![120, 121, 122],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file(".foo", None, &config)?;
|
let file_name = paste.store_file(".foo", None, None, &config)?;
|
||||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||||
assert_eq!(".foo.txt", file_name);
|
assert_eq!(".foo.txt", file_name);
|
||||||
fs::remove_file(file_name)?;
|
fs::remove_file(file_name)?;
|
||||||
|
@ -373,7 +379,7 @@ mod tests {
|
||||||
data: vec![120, 121, 122],
|
data: vec![120, 121, 122],
|
||||||
type_: PasteType::File,
|
type_: PasteType::File,
|
||||||
};
|
};
|
||||||
let file_name = paste.store_file("random", None, &config)?;
|
let file_name = paste.store_file("random", None, None, &config)?;
|
||||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some("bin"),
|
Some("bin"),
|
||||||
|
@ -383,6 +389,46 @@ mod tests {
|
||||||
);
|
);
|
||||||
fs::remove_file(file_name)?;
|
fs::remove_file(file_name)?;
|
||||||
|
|
||||||
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
|
length: Some(4),
|
||||||
|
type_: RandomURLType::Alphanumeric,
|
||||||
|
suffix_mode: Some(true),
|
||||||
|
..RandomURLConfig::default()
|
||||||
|
});
|
||||||
|
let paste = Paste {
|
||||||
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
|
type_: PasteType::File,
|
||||||
|
};
|
||||||
|
let file_name = paste.store_file(
|
||||||
|
"filename.txt",
|
||||||
|
None,
|
||||||
|
Some("fn_from_header.txt".to_string()),
|
||||||
|
&config,
|
||||||
|
)?;
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||||
|
assert_eq!("fn_from_header.txt", file_name);
|
||||||
|
fs::remove_file(file_name)?;
|
||||||
|
|
||||||
|
config.paste.random_url = Some(RandomURLConfig {
|
||||||
|
length: Some(4),
|
||||||
|
type_: RandomURLType::Alphanumeric,
|
||||||
|
suffix_mode: Some(true),
|
||||||
|
..RandomURLConfig::default()
|
||||||
|
});
|
||||||
|
let paste = Paste {
|
||||||
|
data: vec![116, 101, 115, 115, 117, 115],
|
||||||
|
type_: PasteType::File,
|
||||||
|
};
|
||||||
|
let file_name = paste.store_file(
|
||||||
|
"filename.txt",
|
||||||
|
None,
|
||||||
|
Some("fn_from_header".to_string()),
|
||||||
|
&config,
|
||||||
|
)?;
|
||||||
|
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||||
|
assert_eq!("fn_from_header", file_name);
|
||||||
|
fs::remove_file(file_name)?;
|
||||||
|
|
||||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||||
fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?;
|
fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?;
|
||||||
}
|
}
|
||||||
|
@ -393,7 +439,7 @@ mod tests {
|
||||||
type_: PasteType::Oneshot,
|
type_: PasteType::Oneshot,
|
||||||
};
|
};
|
||||||
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
||||||
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
|
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
|
||||||
let file_path = PasteType::Oneshot
|
let file_path = PasteType::Oneshot
|
||||||
.get_path(&config.server.upload_path)
|
.get_path(&config.server.upload_path)
|
||||||
.join(format!("{file_name}.{expiry_date}"));
|
.join(format!("{file_name}.{expiry_date}"));
|
||||||
|
|
|
@ -225,6 +225,7 @@ async fn upload(
|
||||||
}
|
}
|
||||||
let mut urls: Vec<String> = Vec::new();
|
let mut urls: Vec<String> = Vec::new();
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
|
let header_filename = header::parse_header_filename(request.headers())?;
|
||||||
let mut field = item?;
|
let mut field = item?;
|
||||||
let content = ContentDisposition::from(field.content_disposition().clone());
|
let content = ContentDisposition::from(field.content_disposition().clone());
|
||||||
if let Ok(paste_type) = PasteType::try_from(&content) {
|
if let Ok(paste_type) = PasteType::try_from(&content) {
|
||||||
|
@ -274,7 +275,12 @@ async fn upload(
|
||||||
let config = config
|
let config = config
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||||
paste.store_file(content.get_file_name()?, expiry_date, &config)?
|
paste.store_file(
|
||||||
|
content.get_file_name()?,
|
||||||
|
expiry_date,
|
||||||
|
header_filename,
|
||||||
|
&config,
|
||||||
|
)?
|
||||||
}
|
}
|
||||||
PasteType::RemoteFile => {
|
PasteType::RemoteFile => {
|
||||||
paste
|
paste
|
||||||
|
@ -853,6 +859,56 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_upload_file_override_filename() -> Result<(), Error> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.server.upload_path = env::current_dir()?;
|
||||||
|
|
||||||
|
let app = test::init_service(
|
||||||
|
App::new()
|
||||||
|
.app_data(Data::new(RwLock::new(config)))
|
||||||
|
.app_data(Data::new(Client::default()))
|
||||||
|
.configure(configure_routes),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let file_name = "test_file.txt";
|
||||||
|
let header_filename = "fn_from_header.txt";
|
||||||
|
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||||
|
let response = test::call_service(
|
||||||
|
&app,
|
||||||
|
get_multipart_request(×tamp, "file", file_name)
|
||||||
|
.insert_header((
|
||||||
|
header::HeaderName::from_static("filename"),
|
||||||
|
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||||
|
))
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
|
assert_body(
|
||||||
|
response.into_body(),
|
||||||
|
&format!("http://localhost:8080/{header_filename}\n"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let serve_request = TestRequest::get()
|
||||||
|
.uri(&format!("/{header_filename}"))
|
||||||
|
.to_request();
|
||||||
|
let response = test::call_service(&app, serve_request).await;
|
||||||
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
|
assert_body(response.into_body(), ×tamp).await?;
|
||||||
|
|
||||||
|
fs::remove_file(header_filename)?;
|
||||||
|
let serve_request = TestRequest::get()
|
||||||
|
.uri(&format!("/{header_filename}"))
|
||||||
|
.to_request();
|
||||||
|
let response = test::call_service(&app, serve_request).await;
|
||||||
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
||||||
|
|
Loading…
Reference in New Issue