mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-10 11:03:21 +00:00
add serving files
This commit is contained in:
parent
de9a0225a7
commit
b3f42a6336
12 changed files with 153 additions and 61 deletions
27
server/Cargo.lock
generated
27
server/Cargo.lock
generated
|
@ -822,6 +822,15 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humanize-bytes"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a92093c50b761e6ba595c797365301d3aaae67db1382ddf9d5d0092d98df799"
|
||||||
|
dependencies = [
|
||||||
|
"smartstring",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.32"
|
version = "0.14.32"
|
||||||
|
@ -1996,6 +2005,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smartstring"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"static_assertions",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
|
@ -2244,6 +2264,12 @@ dependencies = [
|
||||||
"loom",
|
"loom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "storage-server"
|
name = "storage-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -2251,6 +2277,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"humanize-bytes",
|
||||||
"int-enum",
|
"int-enum",
|
||||||
"log",
|
"log",
|
||||||
"rocket",
|
"rocket",
|
||||||
|
|
|
@ -16,3 +16,4 @@ serde_json = "1.0.140"
|
||||||
int-enum = "1.2.0"
|
int-enum = "1.2.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
rocket_dyn_templates = { version = "0.2.0", features = ["handlebars"] }
|
rocket_dyn_templates = { version = "0.2.0", features = ["handlebars"] }
|
||||||
|
humanize-bytes = "1.0.6"
|
21
server/src/helpers.rs
Normal file
21
server/src/helpers.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use rocket_dyn_templates::handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, RenderErrorReason};
|
||||||
|
pub(crate) fn bytes(h: &Helper< '_>, _: &Handlebars<'_>, _: &Context, rc:
|
||||||
|
&mut RenderContext<'_, '_>, out: &mut dyn Output) -> HelperResult {
|
||||||
|
// get parameter from helper or throw an error
|
||||||
|
let param = h.param(0).and_then(|v| v.value().as_i64()).unwrap_or(0);
|
||||||
|
let output = humanize_bytes::humanize_bytes_decimal!(param);
|
||||||
|
out.write(&*output)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn debug(h: &Helper< '_>, _: &Handlebars<'_>, _: &Context, rc:
|
||||||
|
&mut RenderContext<'_, '_>, out: &mut dyn Output) -> HelperResult {
|
||||||
|
let param = h.param(0)
|
||||||
|
.and_then(|v| v.value().as_object())
|
||||||
|
.ok_or::<RenderError>(RenderErrorReason::ParamNotFoundForIndex("", 0).into())?;
|
||||||
|
let output = serde_json::to_string(param).unwrap();
|
||||||
|
out.write(&output)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -3,7 +3,8 @@ use log::{debug, error, info, trace, warn};
|
||||||
use rocket::{catch, launch, routes, Request, State};
|
use rocket::{catch, launch, routes, Request, State};
|
||||||
use rocket::data::ByteUnit;
|
use rocket::data::ByteUnit;
|
||||||
use rocket::fs::{relative, FileServer};
|
use rocket::fs::{relative, FileServer};
|
||||||
use rocket_dyn_templates::handlebars::Handlebars;
|
use rocket::futures::AsyncWriteExt;
|
||||||
|
use rocket_dyn_templates::handlebars::{handlebars_helper, Context, Handlebars, Helper, HelperResult, Output, RenderContext};
|
||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use sqlx::{migrate, Pool, Postgres};
|
use sqlx::{migrate, Pool, Postgres};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
@ -24,6 +25,7 @@ mod user;
|
||||||
mod models;
|
mod models;
|
||||||
mod managers;
|
mod managers;
|
||||||
mod objs;
|
mod objs;
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
pub type DB = Pool<Postgres>;
|
pub type DB = Pool<Postgres>;
|
||||||
|
|
||||||
|
@ -69,11 +71,13 @@ async fn rocket() -> _ {
|
||||||
api::library::move_file, api::library::upload_file, api::library::download_file, api::library::list_files, api::library::get_file, api::library::delete_file,
|
api::library::move_file, api::library::upload_file, api::library::download_file, api::library::list_files, api::library::get_file, api::library::delete_file,
|
||||||
])
|
])
|
||||||
.mount("/", routes![
|
.mount("/", routes![
|
||||||
ui::user::index, ui::user::list_library_files
|
ui::user::index, ui::user::list_library_files, ui::user::get_library_file
|
||||||
])
|
])
|
||||||
.attach(Template::custom(|engines| {
|
.attach(Template::custom(|engines| {
|
||||||
let _ = engines
|
let hb = &mut engines.handlebars;
|
||||||
.handlebars;
|
|
||||||
|
hb.register_helper("bytes", Box::new(helpers::bytes));
|
||||||
|
hb.register_helper("debug", Box::new(helpers::debug));
|
||||||
}))
|
}))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use rocket::response::stream::ReaderStream;
|
||||||
|
use tokio::io::BufStream;
|
||||||
use crate::managers::repos::RepoContainer;
|
use crate::managers::repos::RepoContainer;
|
||||||
use crate::{models, DB};
|
use crate::{models, DB};
|
||||||
use crate::models::library::LibraryModel;
|
use crate::models::library::LibraryModel;
|
||||||
|
@ -24,6 +28,11 @@ impl Library {
|
||||||
&self.model
|
&self.model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_read_stream(&self, rel_path: &PathBuf) -> Result<BufReader<File>, anyhow::Error> {
|
||||||
|
let mut repo = self.repo.read().await;
|
||||||
|
repo.backend.get_read_stream(&self.model.id.to_string(), rel_path)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn write_file(&self, rel_path: &PathBuf, contents: &[u8]) -> Result<(), anyhow::Error> {
|
pub async fn write_file(&self, rel_path: &PathBuf, contents: &[u8]) -> Result<(), anyhow::Error> {
|
||||||
let mut repo = self.repo.read().await;
|
let mut repo = self.repo.read().await;
|
||||||
repo.backend.write_file(&self.model.id.to_string(), rel_path, contents)
|
repo.backend.write_file(&self.model.id.to_string(), rel_path, contents)
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
use std::io::Cursor;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use rocket::{get, State};
|
use rocket::{get, Response, State};
|
||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use rocket::http::{ContentType, Header};
|
||||||
|
use rocket::http::hyper::body::Buf;
|
||||||
|
use rocket::response::{status, Responder};
|
||||||
|
use rocket::response::stream::ByteStream;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{context, Template};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
@ -13,7 +19,9 @@ pub async fn index() -> Template {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/libraries/<library_id>/<_>/<path..>")]
|
#[get("/libraries/<library_id>/<_>/<path..>")]
|
||||||
pub async fn list_library_files(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: PathBuf) -> Result<Template, ResponseError> {
|
pub async fn list_library_files(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: PathBuf)
|
||||||
|
-> Result<Template, ResponseError>
|
||||||
|
{
|
||||||
let libs = libraries.lock().await;
|
let libs = libraries.lock().await;
|
||||||
let library = libs.get(library_id).await?;
|
let library = libs.get(library_id).await?;
|
||||||
let files = library.list_files(&PathBuf::from(path)).await
|
let files = library.list_files(&PathBuf::from(path)).await
|
||||||
|
@ -27,3 +35,48 @@ pub async fn list_library_files(libraries: &State<Arc<Mutex<LibraryManager>>>, l
|
||||||
files: files
|
files: files
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Responder)]
|
||||||
|
#[response(status = 200)]
|
||||||
|
struct FileAttachment {
|
||||||
|
content: Vec<u8>,
|
||||||
|
// Override the Content-Type declared above.
|
||||||
|
content_type: ContentType,
|
||||||
|
disposition: Header<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/file/<library_id>/<path..>")]
|
||||||
|
pub async fn get_library_file<'a>(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: PathBuf)
|
||||||
|
-> Result<FileAttachment, ResponseError>
|
||||||
|
{
|
||||||
|
let libs = libraries.lock().await;
|
||||||
|
let library = libs.get(library_id).await?;
|
||||||
|
match library.read_file(&PathBuf::from(&path)).await
|
||||||
|
.map_err(|e| ResponseError::GenericError)?
|
||||||
|
{
|
||||||
|
None => {
|
||||||
|
Err(ResponseError::NotFound(JsonErrorResponse {
|
||||||
|
code: "FILE_NOT_FOUND".to_string(),
|
||||||
|
message: "Requested file does not exist".to_string()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Some(contents) => {
|
||||||
|
// TODO: headers?
|
||||||
|
let file_name = path.file_name().unwrap().to_string_lossy();
|
||||||
|
let ext = path.extension().unwrap().to_string_lossy();
|
||||||
|
let file_type = ContentType::from_extension(&ext);
|
||||||
|
// let res = Response::build()
|
||||||
|
// .header(file_type.unwrap_or(ContentType::Binary))
|
||||||
|
// .header(Header::new("Content-Disposition", format!("attachment; filename=\"{}\"", file_name)))
|
||||||
|
// .sized_body(contents.len(), Cursor::new(contents))
|
||||||
|
// .finalize();
|
||||||
|
Ok(FileAttachment {
|
||||||
|
content: contents,
|
||||||
|
content_type: file_type.unwrap_or(ContentType::Binary),
|
||||||
|
disposition: Header::new("Content-Disposition", format!("filename=\"{}\"", file_name))
|
||||||
|
})
|
||||||
|
// Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
mod local;
|
mod local;
|
||||||
mod s3;
|
mod s3;
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use int_enum::IntEnum;
|
use int_enum::IntEnum;
|
||||||
|
@ -68,4 +70,5 @@ pub trait StorageBackend {
|
||||||
|
|
||||||
fn delete_file(&self, library_id: &str, rel_path: &PathBuf) -> Result<(), anyhow::Error>;
|
fn delete_file(&self, library_id: &str, rel_path: &PathBuf) -> Result<(), anyhow::Error>;
|
||||||
fn move_file(&self, library_id: &str, rel_path: &PathBuf, new_rel_path: &PathBuf) -> Result<(), Error>;
|
fn move_file(&self, library_id: &str, rel_path: &PathBuf, new_rel_path: &PathBuf) -> Result<(), Error>;
|
||||||
|
fn get_read_stream(&self, library_id: &str, rel_path: &PathBuf,) -> Result<BufReader<File>, Error>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::env::join_paths;
|
use std::env::join_paths;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
|
@ -35,6 +37,12 @@ fn get_path(folder_root: &PathBuf, library_id: &str, mut path: &Path) -> Result<
|
||||||
}
|
}
|
||||||
impl StorageBackend for LocalStorage {
|
impl StorageBackend for LocalStorage {
|
||||||
|
|
||||||
|
fn get_read_stream(&self, library_id: &str, rel_path: &PathBuf,) -> Result<BufReader<File>, Error> {
|
||||||
|
let path = get_path(&self.folder_root, library_id, rel_path)?;
|
||||||
|
let file = File::open(path)?;
|
||||||
|
Ok(BufReader::new(file))
|
||||||
|
}
|
||||||
|
|
||||||
fn write_file(&self, library_id: &str, rel_path: &PathBuf, contents: &[u8]) -> Result<(), Error> {
|
fn write_file(&self, library_id: &str, rel_path: &PathBuf, contents: &[u8]) -> Result<(), Error> {
|
||||||
let path = get_path(&self.folder_root, library_id, rel_path)?;
|
let path = get_path(&self.folder_root, library_id, rel_path)?;
|
||||||
std::fs::write(path, contents).map_err(|e| anyhow!(e))
|
std::fs::write(path, contents).map_err(|e| anyhow!(e))
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
:root {
|
||||||
|
--accent-color: rgb(231, 148, 162);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -61,3 +65,9 @@ tr.file-list td input[type="checkbox"] {
|
||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
tr.file-list td.filecell-icon .fa-folder {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
tr.file-list td.filecell-label a {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{> partials/nav }}
|
{{> partials/nav }}
|
||||||
<div class="mx-5 columns" style="height: 100%">
|
<div class="mx-3 columns" style="height: 100%">
|
||||||
<div class="column pl-0 sidebar-column">
|
<div class="column pl-0 sidebar-column">
|
||||||
{{> partials/sidebar }}
|
{{> partials/sidebar }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="">
|
<td class="filecell-icon">
|
||||||
<span class="icon is-large">
|
<span class="icon is-large">
|
||||||
{{#if (eq type "folder") }}
|
{{#if (eq type "folder") }}
|
||||||
<i class="fas fa-folder fa-xl"></i>
|
<i class="fas fa-folder fa-xl"></i>
|
||||||
|
@ -59,14 +59,16 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="pl-4">
|
<td class="filecell-label pl-4">
|
||||||
<a href="{{ path }}">{{ path }}</a>
|
{{#if (eq type "folder")}}
|
||||||
{{#if (eq type "folder") }}
|
<a href="{{ path }}">{{ path }}/</a>
|
||||||
/
|
{{/if}}
|
||||||
|
{{#if (eq type "file") }}
|
||||||
|
<a target="_blank" href="/file/{{../library.id}}/{{ path }}">{{ path }}</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td>{{ bytes size }}</td>
|
||||||
<td></td>
|
<td>{{ updated }}</td>
|
||||||
<td>Me</td>
|
<td>Me</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -11,49 +11,3 @@
|
||||||
<li><a href="#">About</a></li>
|
<li><a href="#">About</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{{!-- <style>
|
|
||||||
.sidebar {
|
|
||||||
|
|
||||||
}
|
|
||||||
.sidebar-header {
|
|
||||||
color: hsl(0, 0%, 48%);
|
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
ul.sidebar-list {
|
|
||||||
/* margin-top: 2px; */
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-left: 5px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
li a {
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding-right: 5px;
|
|
||||||
border-radius: 2.5px;
|
|
||||||
/* margin-top: 4px; */
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
li a:hover, li a.is-active {
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ul.sidebar-list li a {
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding-right: 5px;
|
|
||||||
border-radius: 2.5px;
|
|
||||||
/* margin-top: 4px; */
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
ul.sidebar-list li a:hover, ul.sidebar-list li a.is-active {
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
.sidebar i {
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style> --}}
|
|
Loading…
Add table
Add a link
Reference in a new issue