Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): add an endpoint for retrieving a list of files #94

Merged
merged 49 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
efa7258
Start
ajbdev Jul 11, 2023
e4a85dc
Wip
ajbdev Jul 11, 2023
c8265cd
Implement path based JSON index
ajbdev Jul 12, 2023
e2d6e23
Remove json_index_path
ajbdev Jul 12, 2023
f27b8b7
Merge branch 'orhun:master' into json-index
ajbdev Jul 13, 2023
7615c33
Return datetime stamp instead of relative time
ajbdev Jul 14, 2023
8179a55
Add file size to list item
ajbdev Jul 14, 2023
c0404b7
Merge branch 'json-index' of github.com:ajbdev/rustypaste into json-i…
ajbdev Jul 14, 2023
a99b91d
Add auth check when retrieving JSON index
ajbdev Jul 17, 2023
13efaed
Make json index path hardcoded
ajbdev Jul 17, 2023
89b9456
Test (currently failing)
ajbdev Jul 18, 2023
6511ac1
Fix test for test_json_list
ajbdev Jul 21, 2023
74b8e67
Clippy fix
ajbdev Jul 21, 2023
c770de4
Revert cargo to original versions with only needed changes
ajbdev Jul 21, 2023
46a5ccf
Add detail about auth guard affecting list route
ajbdev Jul 21, 2023
5e812e8
Change json_index_path to expose_list
ajbdev Jul 24, 2023
202485f
Remove unneeded linebreak
ajbdev Jul 24, 2023
62a7b7e
Remove unnecessary import
ajbdev Jul 24, 2023
9dd8f69
Remove unnecessary space at end of line
ajbdev Jul 24, 2023
4a8795e
Move config check after auth check
ajbdev Jul 24, 2023
fb71afe
Merge branch 'master' into json-index-path
ajbdev Jul 24, 2023
4e9aea0
Use new auth check syntax, add docs to struct, rename test_json_list …
ajbdev Jul 24, 2023
31d1b6a
Replace chrono usage with uts2ts
ajbdev Jul 26, 2023
6a59529
Check list result in test
ajbdev Jul 26, 2023
791ef78
Add example to README
ajbdev Jul 26, 2023
b8ed9bc
Upgrade serde_json to 1.0.103
ajbdev Jul 26, 2023
8c333b7
Add linebreak
ajbdev Jul 26, 2023
a41e3cd
Remove unneeded clone
ajbdev Jul 26, 2023
7231b57
Remove extra nl
ajbdev Jul 26, 2023
c9f076e
Update README.md
ajbdev Jul 26, 2023
e26b367
Update README.md
ajbdev Jul 28, 2023
cae8ce1
Update README.md
ajbdev Jul 28, 2023
9187112
Remove serde_json
ajbdev Jul 28, 2023
b664ea9
Set default config to false for expose_list
ajbdev Jul 28, 2023
28d5328
Apply suggestions from code review
ajbdev Jul 28, 2023
5c46d69
Check that option is value in test_list
ajbdev Jul 28, 2023
60d4582
Update Cargo.toml
ajbdev Jul 30, 2023
88034e7
Update cargo.lock
ajbdev Jul 30, 2023
48ba8f3
Use expect() to check file name
ajbdev Jul 30, 2023
16aebd8
Remove underscore from list item struct
ajbdev Jul 30, 2023
cd01ca2
Keep comma after last line
ajbdev Jul 30, 2023
1f09c1e
refactor(server): rename ListItem fields
orhun Jul 31, 2023
f7c0fc9
test(fixtures): add fixture test for listing files
orhun Jul 31, 2023
e36fef5
test push
tessus Aug 1, 2023
a10275b
remove file again
tessus Aug 1, 2023
c202b82
chop off ts from filename and minor refactor
tessus Aug 2, 2023
f7f477a
update README
tessus Aug 2, 2023
2cd343f
docs(readme): fix capitalization
orhun Aug 7, 2023
b9e15dc
refactor(server): clean up list implementation
orhun Aug 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ shuttle-actix-web = { version = "0.21.0", optional = true }
shuttle-runtime = { version = "0.21.0", optional = true }
shuttle-static-folder = { version = "0.21.0", optional = true }
tokio = { version = "1.29.1", optional = true }
uts2ts = "0.3.0"

[dependencies.config]
version = "0.13.3"
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ You can also set multiple auth tokens via the array field `[server].auth_tokens`

See [config.toml](./config.toml) for configuration options.

### List endpoint

Set `expose_list` to true in [config.toml](./config.toml) to be able to retrieve a JSON formatted list of files in your uploads directory. This will not include oneshot files, oneshot urls, or urls.

```sh
$ curl "http://<server_address>/list"

[{"expires_at":null,"file_name":"accepted-cicada.txt","file_size":241}]
```
This route will require an `AUTH_TOKEN` if one is set.
ajbdev marked this conversation as resolved.
Show resolved Hide resolved

ajbdev marked this conversation as resolved.
Show resolved Hide resolved
#### HTML Form

It is possible to use an HTML form for uploading files. To do so, you need to update two fields in your `config.toml`:
Expand Down
1 change: 1 addition & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ max_content_length = "10MB"
upload_path = "./upload"
timeout = "30s"
expose_version = false
expose_list = false
#auth_tokens = [
# "super_secret_token1",
# "super_secret_token2",
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ pub struct ServerConfig {
/// Landing page content-type.
#[deprecated(note = "use the [landing_page] table")]
pub landing_page_content_type: Option<String>,
/// Path of the JSON index.
pub expose_list: Option<bool>,
}

/// Landing page configuration.
Expand Down
111 changes: 110 additions & 1 deletion src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ use awc::Client;
use byte_unit::Byte;
use futures_util::stream::StreamExt;
use mime::TEXT_PLAIN_UTF_8;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::env;
use std::fs;
use std::path::Path;
use std::sync::RwLock;
use uts2ts;

/// Shows the landing page.
#[get("/")]
Expand Down Expand Up @@ -283,10 +285,78 @@ async fn upload(
Ok(HttpResponse::Ok().body(urls.join("")))
}

/// File entry item for list endpoint.
#[derive(Serialize, Deserialize)]
pub struct ListItem {
/// Uploaded file name.
pub filename: String,
/// Size of the file in bytes.
pub filesize: u64,
orhun marked this conversation as resolved.
Show resolved Hide resolved
/// ISO8601 formatted date-time string of the expiration timestamp if one exists for this file.
pub expires_at: Option<String>,
orhun marked this conversation as resolved.
Show resolved Hide resolved
}

/// Returns the list of files.
#[get("/list")]
async fn list(
request: HttpRequest,
config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> {
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.clone();
let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");

let tokens = config.get_tokens();
auth::check(host, request.headers(), tokens)?;
orhun marked this conversation as resolved.
Show resolved Hide resolved

if !config.server.expose_list.unwrap_or(false) {
log::warn!("server is not configured to expose list endpoint");
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
}
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
.filter_map(|entry| {
entry.ok().and_then(|e| {
let metadata = fs::metadata(e.path()).expect("Failed to retrieve metadata");

if metadata.is_dir() {
return None;
}

let filename = e.file_name().into_string().ok()?;
let extension: Option<i64> = Path::new(&filename)
.extension()
.and_then(|ext| ext.to_str())
.and_then(|v| v.parse().ok());

let mut expires_at = None;

if let Some(expiration) = extension {
let seconds = expiration / 1000;
let timestamp = uts2ts::uts2ts(seconds);

expires_at = Some(timestamp.as_string());
}

Some(ListItem {
filename,
filesize: metadata.len(),
expires_at,
})
})
})
.collect();

Ok(HttpResponse::Ok().json(entries))
}

/// Configures the server routes.
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(index)
.service(version)
.service(list)
.service(serve)
.service(upload)
.route("", web::head().to(HttpResponse::MethodNotAllowed));
Expand Down Expand Up @@ -513,6 +583,45 @@ mod tests {
Ok(())
}

#[actix_web::test]
async fn test_list() -> Result<(), Error> {
let mut config = Config::default();
config.server.expose_list = Some(true);

let test_upload_dir = "test_upload";
fs::create_dir(test_upload_dir)?;
config.server.upload_path = PathBuf::from(test_upload_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 filename = "test_file.txt";
let timestamp = util::get_system_time()?.as_secs().to_string();
test::call_service(
&app,
get_multipart_request(&timestamp, "file", filename).to_request(),
)
.await;

let request = TestRequest::default()
.insert_header(("content-type", "text/plain"))
.uri("/list")
.to_request();
let result: Vec<ListItem> = test::call_and_read_body_json(&app, request).await;

assert_eq!(result.len(), 1);
assert_eq!(result.first().expect("json object").filename, filename);

fs::remove_dir_all(test_upload_dir)?;

Ok(())
}

#[actix_web::test]
async fn test_auth() -> Result<(), Error> {
let mut config = Config::default();
Expand Down