Compare commits

..

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 8c099d0586 Set up the delete route
Sets up the delete route, including post-delete redirect back to the root.
Also adds logging.

Delete does not actually delete things yet.
2023-09-22 00:38:26 -04:00
Savanni D'Gerinel f440e6f7ad Refactor file and thumbnail serving to common code 2023-09-21 22:15:58 -04:00
6 changed files with 226 additions and 76 deletions

60
Cargo.lock generated
View File

@ -622,6 +622,19 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "env_logger"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"humantime",
"is-terminal",
"log 0.4.20",
"regex",
"termcolor",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -706,12 +719,14 @@ dependencies = [
"http", "http",
"image 0.23.14", "image 0.23.14",
"iron", "iron",
"log 0.4.20",
"logger", "logger",
"mime 0.3.17", "mime 0.3.17",
"mime_guess 2.0.4", "mime_guess 2.0.4",
"mustache", "mustache",
"orizentic", "orizentic",
"params", "params",
"pretty_env_logger",
"router", "router",
"serde 1.0.188", "serde 1.0.188",
"serde_json", "serde_json",
@ -1532,6 +1547,12 @@ 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 = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.10.16" version = "0.10.16"
@ -1746,6 +1767,17 @@ dependencies = [
"url 1.7.2", "url 1.7.2",
] ]
[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi 0.3.2",
"rustix",
"windows-sys",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -2634,6 +2666,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "pretty_env_logger"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
dependencies = [
"env_logger",
"log 0.4.20",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.3.1" version = "1.3.1"
@ -3421,6 +3463,15 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "termcolor"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"
@ -4079,6 +4130,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

@ -27,3 +27,5 @@ thiserror = "1.0.20"
tokio = { version = "1", features = [ "full" ] } tokio = { version = "1", features = [ "full" ] }
uuid = { version = "0.4", features = [ "serde", "v4" ] } uuid = { version = "0.4", features = [ "serde", "v4" ] }
warp = { version = "0.3" } warp = { version = "0.3" }
pretty_env_logger = { version = "0.5" }
log = { version = "0.4" }

View File

@ -2,6 +2,7 @@ use build_html::{self, Html, HtmlContainer};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Form { pub struct Form {
path: String,
method: String, method: String,
encoding: Option<String>, encoding: Option<String>,
elements: String, elements: String,
@ -10,12 +11,18 @@ pub struct Form {
impl Form { impl Form {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
path: "/".to_owned(),
method: "get".to_owned(), method: "get".to_owned(),
encoding: None, encoding: None,
elements: "".to_owned(), elements: "".to_owned(),
} }
} }
pub fn with_path(mut self, path: &str) -> Self {
self.path = path.to_owned();
self
}
pub fn with_method(mut self, method: &str) -> Self { pub fn with_method(mut self, method: &str) -> Self {
self.method = method.to_owned(); self.method = method.to_owned();
self self
@ -30,11 +37,12 @@ impl Form {
impl Html for Form { impl Html for Form {
fn to_html_string(&self) -> String { fn to_html_string(&self) -> String {
let encoding = match self.encoding { let encoding = match self.encoding {
Some(ref encoding) => format!("encoding={encoding}", encoding = encoding), Some(ref encoding) => format!("encoding=\"{encoding}\"", encoding = encoding),
None => format!(""), None => format!(""),
}; };
format!( format!(
"<form method={method} {encoding}>\n{elements}\n</form>\n", "<form action=\"{path}\" method=\"{method}\" {encoding}>\n{elements}\n</form>\n",
path = self.path,
method = self.method, method = self.method,
encoding = encoding, encoding = encoding,
elements = self.elements.to_html_string() elements = self.elements.to_html_string()
@ -52,36 +60,58 @@ impl HtmlContainer for Form {
pub struct Input { pub struct Input {
ty: String, ty: String,
name: String, name: String,
id: String, id: Option<String>,
value: Option<String>, value: Option<String>,
content: Option<String>,
} }
impl Html for Input { impl Html for Input {
fn to_html_string(&self) -> String { fn to_html_string(&self) -> String {
let id = match self.id {
Some(ref id) => format!("id=\"{}\"", id),
None => "".to_owned(),
};
let value = match self.value {
Some(ref value) => format!("value=\"{}\"", value),
None => "".to_owned(),
};
format!( format!(
"<input type=\"{ty}\" name=\"{name}\" id=\"{id}\">{value}</input>\n", "<input type=\"{ty}\" name=\"{name}\" {id} {value}>{content}</input>\n",
ty = self.ty, ty = self.ty,
name = self.name, name = self.name,
id = self.id, id = id,
value = self.value.clone().unwrap_or("".to_owned()), value = value,
content = self.content.clone().unwrap_or("".to_owned()),
) )
} }
} }
impl Input { impl Input {
pub fn new(ty: &str, name: &str, id: &str) -> Self { pub fn new(ty: &str, name: &str) -> Self {
Self { Self {
ty: ty.to_owned(), ty: ty.to_owned(),
name: name.to_owned(), name: name.to_owned(),
id: id.to_owned(), id: None,
value: None, value: None,
content: None,
} }
} }
pub fn with_id(mut self, val: &str) -> Self {
self.id = Some(val.to_owned());
self
}
pub fn with_value(mut self, val: &str) -> Self { pub fn with_value(mut self, val: &str) -> Self {
self.value = Some(val.to_owned()); self.value = Some(val.to_owned());
self self
} }
pub fn with_content(mut self, val: &str) -> Self {
self.content = Some(val.to_owned());
self
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -111,25 +141,29 @@ impl Html for Label {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Button { pub struct Button {
name: String, name: Option<String>,
text: String, label: String,
} }
impl Button { impl Button {
pub fn new(name: &str, text: &str) -> Self { pub fn new(label: &str) -> Self {
Self { Self {
name: name.to_owned(), name: None,
text: text.to_owned(), label: label.to_owned(),
} }
} }
} }
impl Html for Button { impl Html for Button {
fn to_html_string(&self) -> String { fn to_html_string(&self) -> String {
let name = match self.name {
Some(ref name) => format!("name={}", name),
None => "".to_owned(),
};
format!( format!(
"<button name={name}>{text}</button>", "<button {name}>{label}</button>",
name = self.name, name = name,
text = self.text label = self.label
) )
} }
} }

View File

@ -43,7 +43,7 @@ impl App {
FileInfo::open(&id, &self.files_root) FileInfo::open(&id, &self.files_root)
} }
pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File), FileError> { pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> {
let f = File::open(&id, &self.files_root)?; let f = File::open(&id, &self.files_root)?;
let info = f.info(); let info = f.info();
let stream = f.stream()?; let stream = f.stream()?;

View File

@ -6,11 +6,15 @@ use iron::prelude::*;
use iron::response::BodyReader; use iron::response::BodyReader;
use iron::status; use iron::status;
*/ */
#[macro_use]
extern crate log;
use http::status::StatusCode; use http::status::StatusCode;
// use mustache::{compile_path, Template}; // use mustache::{compile_path, Template};
// use orizentic::{Permissions, ResourceName, Secret}; // use orizentic::{Permissions, ResourceName, Secret};
use build_html::Html; use build_html::Html;
use std::{ use std::{
collections::HashMap,
io::Read, io::Read,
net::{IpAddr, Ipv4Addr, SocketAddr}, net::{IpAddr, Ipv4Addr, SocketAddr},
path::Path, path::Path,
@ -221,6 +225,28 @@ fn script(_: &mut Request) -> IronResult<Response> {
} }
*/ */
fn serve_file(
info: FileInfo,
mut file: std::fs::File,
old_etags: Option<String>,
) -> http::Result<http::Response<Vec<u8>>> {
let mut content = Vec::new();
match old_etags {
Some(old_etags) if old_etags != info.hash => warp::http::Response::builder()
.header("content-type", info.file_type)
.status(StatusCode::NOT_MODIFIED)
.body(content),
_ => {
let _ = file.read_to_end(&mut content);
warp::http::Response::builder()
.header("content-type", info.file_type)
.header("etag", info.hash)
.status(StatusCode::OK)
.body(content)
}
}
}
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
/* /*
@ -232,21 +258,6 @@ pub async fn main() {
let mut router = Router::new(); let mut router = Router::new();
router.get(
"/:id",
files::GetFileHandler {
app: app.clone(),
template: compile_path("templates/file.html").expect("the template to compile"),
},
"get-file-page",
);
router.get(
"/:id/raw",
files::GetHandler { app: app.clone() },
"get-file",
);
router.post("/", files::PostHandler { app: app.clone() }, "upload-file"); router.post("/", files::PostHandler { app: app.clone() }, "upload-file");
router.delete( router.delete(
@ -279,13 +290,18 @@ pub async fn main() {
.await; .await;
*/ */
pretty_env_logger::init();
let app = Arc::new(RwLock::new(App::new(Path::new( let app = Arc::new(RwLock::new(App::new(Path::new(
&std::env::var("FILE_SHARE_DIR").unwrap(), &std::env::var("FILE_SHARE_DIR").unwrap(),
)))); ))));
let root = warp::path!().map({ let log = warp::log("file_service");
let root = warp::path!().and(warp::get()).map({
let app = app.clone(); let app = app.clone();
move || { move || {
info!("root handler");
warp::http::Response::builder() warp::http::Response::builder()
.header("content-type", "text/html") .header("content-type", "text/html")
.status(StatusCode::OK) .status(StatusCode::OK)
@ -293,37 +309,66 @@ pub async fn main() {
} }
}); });
let post_handler = warp::path!(String)
.and(warp::post())
.and(warp::filters::body::form())
.map(|id: String, form: HashMap<String, String>| {
info!("post_delete {}", id);
info!("form: {:?}", form);
warp::http::Response::builder()
.header("location", "/")
.status(StatusCode::SEE_OTHER)
.body(vec![])
});
let thumbnail = warp::path!(String / "tn") let thumbnail = warp::path!(String / "tn")
.and(warp::get())
.and(warp::header::optional::<String>("if-none-match")) .and(warp::header::optional::<String>("if-none-match"))
.map({ .map({
let app = app.clone(); let app = app.clone();
move |id: String, old_etags: Option<String>| { move |id: String, old_etags: Option<String>| match app
let mut content = Vec::new(); .read()
match app.read().unwrap().get_thumbnail(&id) { .unwrap()
Ok((info, mut stream)) => match old_etags { .get_thumbnail(&id)
Some(old_etags) if old_etags != info.hash => { {
warp::http::Response::builder() Ok((info, file)) => serve_file(info, file, old_etags),
.header("content-type", info.file_type)
.status(StatusCode::NOT_MODIFIED)
.body(content)
}
_ => {
let _ = stream.read_to_end(&mut content);
warp::http::Response::builder()
.header("content-type", info.file_type)
.header("etag", info.hash)
.status(StatusCode::OK)
.body(content)
}
},
Err(_err) => warp::http::Response::builder() Err(_err) => warp::http::Response::builder()
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
.body(content), .body(vec![]),
}
} }
}); });
let server = warp::serve(root.or(thumbnail)); let file = warp::path!(String)
.and(warp::get())
.and(warp::header::optional::<String>("if-none-match"))
.map({
let app = app.clone();
move |id: String, old_etags: Option<String>| match app.read().unwrap().get_file(&id) {
Ok((info, file)) => serve_file(info, file, old_etags),
Err(_err) => warp::http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(vec![]),
}
});
let upload = warp::path!().and(warp::post()).map(|| {
println!("upload");
warp::reply()
});
let delete = warp::path!(String).and(warp::delete()).map(|id: String| {
println!("delete {}", id);
warp::reply()
});
let server = warp::serve(
root.or(post_handler)
.or(file)
.or(thumbnail)
.or(upload)
.or(delete)
.with(log),
);
server server
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))
.await; .await;

View File

@ -11,17 +11,33 @@ pub fn index(files: Vec<Result<File, FileError>>) -> build_html::HtmlPage {
.with_encoding("multipart/form-data") .with_encoding("multipart/form-data")
.with_container( .with_container(
Container::new(ContainerType::Div) Container::new(ContainerType::Div)
.with_html(Input::new("file", "file", "file-selector-input")) .with_html(Input::new("file", "file"))
.with_html(Label::new("for-selector-input", "Select a file")), .with_html(Label::new("for-selector-input", "Select a file")),
) )
.with_html(Button::new("upload", "Upload file")), .with_html(Button::new("Upload file")),
); );
for file in files { for file in files {
let mut container = let container = match file {
Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); Ok(ref file) => thumbnail(file).with_html(
match file { Form::new()
Ok(file) => { .with_path(&format!("/{}", file.info().id))
.with_method("post")
.with_html(Input::new("hidden", "_method").with_value("delete"))
.with_html(Button::new("Delete")),
),
Err(err) => Container::new(ContainerType::Div)
.with_attributes(vec![("class", "file")])
.with_paragraph(format!("{:?}", err)),
};
page.add_container(container)
}
page
}
pub fn thumbnail(file: &File) -> Container {
let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]);
let tn = Container::new(ContainerType::Div) let tn = Container::new(ContainerType::Div)
.with_attributes(vec![("class", "thumbnail")]) .with_attributes(vec![("class", "thumbnail")])
.with_link( .with_link(
@ -29,12 +45,5 @@ pub fn index(files: Vec<Result<File, FileError>>) -> build_html::HtmlPage {
Image::new(&format!("{}/tn", file.info().id)).to_html_string(), Image::new(&format!("{}/tn", file.info().id)).to_html_string(),
); );
container.add_html(tn); container.add_html(tn);
} container
Err(err) => {
container.add_paragraph(format!("{:?}", err));
}
}
page.add_container(container);
}
page
} }