Serve both files and directories

This commit is contained in:
Matteo Settenvini 2023-07-02 02:06:27 +02:00
parent 053854ce72
commit 9fa09de8df
3 changed files with 61 additions and 36 deletions

View File

@ -29,9 +29,31 @@ Then just configure Apache or NGINX to proxy to the given port. For example:
# ... other options ... # ... other options ...
</VirtualHost> </VirtualHost>
``` ```
You probably also want a systemd unit file, for instance `/etc/systemd/system/serves3@.service`:
```ini
[Unit]
Description=ServeS3, a S3 proxy
StartLimitInterval=100
StartLimitBurst=10
[Service]
Type=simple
ExecStart=/usr/local/bin/serves3
WorkingDirectory=/etc/serves3/%i/
Environment=ROCKET_PORT=%i
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
Then, e.g. for running on port 8000, you would put the corresponding configuration file in `/etc/serves3/8000/` and install the unit with `systemctl enable --now servers3@8000.service`.
## Build and install ## Build and install
```bash ```bash

View File

@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: © Matteo Settenvini <matteo.settenvini@montecristosoftware.eu> // SPDX-FileCopyrightText: © Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
use s3::serde_types::ListBucketResult;
use { use {
lazy_static::lazy_static, lazy_static::lazy_static,
rocket::response::Responder, rocket::response::Responder,
@ -80,61 +78,61 @@ enum Error {
#[rocket::get("/<path..>")] #[rocket::get("/<path..>")]
async fn index(path: PathBuf) -> Result<FileView, Error> { async fn index(path: PathBuf) -> Result<FileView, Error> {
let parent = path.parent();
let s3_path = format!(
"{}{}",
path.display(),
match parent {
Some(_) => "/",
None => "",
}
);
/* /*
The way things work in S3, the following holds for us: The way things work in S3, the following holds for us:
- we need to use a slash as separator - we need to use a slash as separator
- folders need to be queried ending with a slash - folders need to be queried ending with a slash
- getting the bucket address will return an XML file
with all properties; we don't want that.
We try first to retrieve list an object as a folder. If we fail, We try first to retrieve list an object as a file. If we fail,
we fallback to retrieving the object itself. we fallback to retrieving the equivalent folder.
*/ */
let s3_objects = BUCKET.list(s3_path, Some("/".into())).await;
let s3_objects = match s3_objects { // FIXME: this can be big, we should use streaming,
Ok(s3_objects) => s3_objects, // not loading in memory!
Err(_) => { if !path.as_os_str().is_empty() {
// TODO: this can be big, we should use streaming, let data = BUCKET
// not loading in memory. .get_object(format!("{}", path.display()))
let data = BUCKET .await
.get_object(format!("{}", path.display())) .map_err(|_| Error::NotFound("Object not found".into()));
.await
.map_err(|_| Error::NotFound("Object not found".into()))? if let Ok(contents) = data {
.bytes() let bytes = contents.bytes().to_vec();
.to_vec(); return Ok(FileView::File(bytes));
return Ok(FileView::File(data));
} }
}; }
let objects = s3_fileview(&s3_objects); let objects = s3_fileview(&path).await?;
let rendered = Template::render( let rendered = Template::render(
"index", "index",
context! { context! {
path: format!("{}/", path.display()), path: format!("{}/", path.display()),
has_parent: !path.as_os_str().is_empty(),
objects objects
}, },
); );
Ok(FileView::Folder(rendered)) Ok(FileView::Folder(rendered))
} }
fn s3_fileview(s3_objects: &Vec<ListBucketResult>) -> Vec<&str> { async fn s3_fileview(path: &PathBuf) -> Result<Vec<String>, Error> {
/* /*
if listing a folder: if listing a folder:
- folders will be under 'common_prefixes' - folders will be under 'common_prefixes'
- files will be under the 'contents' property - files will be under the 'contents' property
*/ */
s3_objects
let parent = path.parent();
let s3_folder_path = match parent {
Some(_) => format!("{}/", path.display()),
None => "".into(),
};
let s3_objects = BUCKET
.list(s3_folder_path, Some("/".into()))
.await
.map_err(|_| Error::NotFound("Object not found".into()))?;
let objects = s3_objects
.iter() .iter()
.flat_map(|list| -> Vec<Option<&str>> { .flat_map(|list| -> Vec<Option<&str>> {
let prefix = if let Some(p) = &list.prefix { let prefix = if let Some(p) = &list.prefix {
@ -148,14 +146,19 @@ fn s3_fileview(s3_objects: &Vec<ListBucketResult>) -> Vec<&str> {
.iter() .iter()
.flatten() .flatten()
.map(|dir| dir.prefix.strip_prefix(&prefix)); .map(|dir| dir.prefix.strip_prefix(&prefix));
let files = list let files = list
.contents .contents
.iter() .iter()
.map(|obj| obj.key.strip_prefix(&prefix)); .map(|obj| obj.key.strip_prefix(&prefix));
folders.chain(files).collect() folders.chain(files).collect()
}) })
.flatten() .flatten()
.collect() .map(str::to_owned)
.collect();
Ok(objects)
} }
#[rocket::launch] #[rocket::launch]

View File

@ -8,7 +8,7 @@
<body> <body>
<h1>{{ path }}</h1> <h1>{{ path }}</h1>
<ul> <ul>
{% if has_parent %} {% if path != "/" %}
<li><a href="../">..</a></li> <li><a href="../">..</a></li>
{% endif %} {% endif %}