From 403cf264261e029c729593bd84b7322e566695da Mon Sep 17 00:00:00 2001 From: Jackz Date: Tue, 15 Apr 2025 13:05:03 -0500 Subject: [PATCH] add serving files --- server/Cargo.lock | 27 ++++++++++ server/Cargo.toml | 3 +- server/src/helpers.rs | 21 ++++++++ server/src/main.rs | 12 +++-- server/src/objs/library.rs | 9 ++++ server/src/routes/ui/user.rs | 57 +++++++++++++++++++++- server/src/storage.rs | 3 ++ server/src/storage/local.rs | 8 +++ server/static/css/main.css | 10 ++++ server/templates/layouts/main.html.hbs | 2 +- server/templates/libraries.html.hbs | 16 +++--- server/templates/partials/sidebar.html.hbs | 46 ----------------- 12 files changed, 153 insertions(+), 61 deletions(-) create mode 100644 server/src/helpers.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index b9b7e0a..ca4ab9c 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -822,6 +822,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "hyper" version = "0.14.32" @@ -1996,6 +2005,17 @@ dependencies = [ "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]] name = "socket2" version = "0.5.9" @@ -2244,6 +2264,12 @@ dependencies = [ "loom", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "storage-server" version = "0.1.0" @@ -2251,6 +2277,7 @@ dependencies = [ "anyhow", "chrono", "dotenvy", + "humanize-bytes", "int-enum", "log", "rocket", diff --git a/server/Cargo.toml b/server/Cargo.toml index 419c828..683c782 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,4 +15,5 @@ serde = "1.0.219" serde_json = "1.0.140" int-enum = "1.2.0" dotenvy = "0.15.7" -rocket_dyn_templates = { version = "0.2.0", features = ["handlebars"] } \ No newline at end of file +rocket_dyn_templates = { version = "0.2.0", features = ["handlebars"] } +humanize-bytes = "1.0.6" \ No newline at end of file diff --git a/server/src/helpers.rs b/server/src/helpers.rs new file mode 100644 index 0000000..8eaf926 --- /dev/null +++ b/server/src/helpers.rs @@ -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::(RenderErrorReason::ParamNotFoundForIndex("", 0).into())?; + let output = serde_json::to_string(param).unwrap(); + out.write(&output)?; + Ok(()) +} \ No newline at end of file diff --git a/server/src/main.rs b/server/src/main.rs index 3c7e84f..362a94d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,7 +3,8 @@ use log::{debug, error, info, trace, warn}; use rocket::{catch, launch, routes, Request, State}; use rocket::data::ByteUnit; 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 sqlx::{migrate, Pool, Postgres}; use sqlx::postgres::PgPoolOptions; @@ -24,6 +25,7 @@ mod user; mod models; mod managers; mod objs; +mod helpers; pub type DB = Pool; @@ -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, ]) .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| { - let _ = engines - .handlebars; + let hb = &mut engines.handlebars; + + hb.register_helper("bytes", Box::new(helpers::bytes)); + hb.register_helper("debug", Box::new(helpers::debug)); })) } diff --git a/server/src/objs/library.rs b/server/src/objs/library.rs index ad12666..69ca2a7 100644 --- a/server/src/objs/library.rs +++ b/server/src/objs/library.rs @@ -1,5 +1,9 @@ +use std::fs::File; +use std::io::BufReader; use std::path::PathBuf; use anyhow::Error; +use rocket::response::stream::ReaderStream; +use tokio::io::BufStream; use crate::managers::repos::RepoContainer; use crate::{models, DB}; use crate::models::library::LibraryModel; @@ -24,6 +28,11 @@ impl Library { &self.model } + pub async fn get_read_stream(&self, rel_path: &PathBuf) -> Result, 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> { let mut repo = self.repo.read().await; repo.backend.write_file(&self.model.id.to_string(), rel_path, contents) diff --git a/server/src/routes/ui/user.rs b/server/src/routes/ui/user.rs index 3791a31..3a64d3e 100644 --- a/server/src/routes/ui/user.rs +++ b/server/src/routes/ui/user.rs @@ -1,6 +1,12 @@ +use std::io::Cursor; use std::path::PathBuf; 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_dyn_templates::{context, Template}; use tokio::sync::Mutex; @@ -13,7 +19,9 @@ pub async fn index() -> Template { } #[get("/libraries//<_>/")] -pub async fn list_library_files(libraries: &State>>, library_id: &str, path: PathBuf) -> Result { +pub async fn list_library_files(libraries: &State>>, library_id: &str, path: PathBuf) + -> Result +{ let libs = libraries.lock().await; let library = libs.get(library_id).await?; let files = library.list_files(&PathBuf::from(path)).await @@ -26,4 +34,49 @@ pub async fn list_library_files(libraries: &State>>, l library: library.model(), files: files })) +} + +#[derive(Responder)] +#[response(status = 200)] +struct FileAttachment { + content: Vec, + // Override the Content-Type declared above. + content_type: ContentType, + disposition: Header<'static>, +} + +#[get("/file//")] +pub async fn get_library_file<'a>(libraries: &State>>, library_id: &str, path: PathBuf) + -> Result +{ + 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) + } + } + } \ No newline at end of file diff --git a/server/src/storage.rs b/server/src/storage.rs index 75d963e..a86212a 100644 --- a/server/src/storage.rs +++ b/server/src/storage.rs @@ -1,6 +1,8 @@ mod local; mod s3; +use std::fs::File; +use std::io::BufReader; use std::path::PathBuf; use anyhow::{anyhow, Error}; 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 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, Error>; } diff --git a/server/src/storage/local.rs b/server/src/storage/local.rs index 769b96d..78cf3ba 100644 --- a/server/src/storage/local.rs +++ b/server/src/storage/local.rs @@ -1,4 +1,6 @@ use std::env::join_paths; +use std::fs::File; +use std::io::BufReader; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; 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 { + fn get_read_stream(&self, library_id: &str, rel_path: &PathBuf,) -> Result, 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> { let path = get_path(&self.folder_root, library_id, rel_path)?; std::fs::write(path, contents).map_err(|e| anyhow!(e)) diff --git a/server/static/css/main.css b/server/static/css/main.css index 013226a..d71b484 100644 --- a/server/static/css/main.css +++ b/server/static/css/main.css @@ -1,3 +1,7 @@ +:root { + --accent-color: rgb(231, 148, 162); +} + .sidebar { } @@ -60,4 +64,10 @@ tr.file-list td input[type="checkbox"] { /* Opera */ transform: scale(1.5); 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); } \ No newline at end of file diff --git a/server/templates/layouts/main.html.hbs b/server/templates/layouts/main.html.hbs index 806c1d1..768b44b 100644 --- a/server/templates/layouts/main.html.hbs +++ b/server/templates/layouts/main.html.hbs @@ -15,7 +15,7 @@ {{> partials/nav }} -
+
diff --git a/server/templates/libraries.html.hbs b/server/templates/libraries.html.hbs index 6d4729f..d98b3ab 100644 --- a/server/templates/libraries.html.hbs +++ b/server/templates/libraries.html.hbs @@ -49,7 +49,7 @@ - + {{#if (eq type "folder") }} @@ -59,14 +59,16 @@ {{/if}} - - {{ path }} - {{#if (eq type "folder") }} - / + + {{#if (eq type "folder")}} + {{ path }}/ + {{/if}} + {{#if (eq type "file") }} + {{ path }} {{/if}} - - + {{ bytes size }} + {{ updated }} Me {{/each}} diff --git a/server/templates/partials/sidebar.html.hbs b/server/templates/partials/sidebar.html.hbs index c17018c..193d770 100644 --- a/server/templates/partials/sidebar.html.hbs +++ b/server/templates/partials/sidebar.html.hbs @@ -11,49 +11,3 @@
  • About
  • - -{{!-- --}} \ No newline at end of file