From 12df1f4b9b1dea8671cedb388d74d52e3a55fab7 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 25 Oct 2023 09:47:27 -0400 Subject: [PATCH 1/4] Create an UnorderedList HTML container --- file-service/src/html.rs | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/file-service/src/html.rs b/file-service/src/html.rs index d38fb21..1ad0760 100644 --- a/file-service/src/html.rs +++ b/file-service/src/html.rs @@ -274,3 +274,54 @@ impl Html for Image { ) } } + +#[derive(Debug)] +pub struct UnorderedList { + children: Vec, + attributes: Attributes, +} + +impl UnorderedList { + pub fn new() -> Self { + Self { + children: vec![], + attributes: Attributes::default(), + } + } + + pub fn with_attributes<'a>( + mut self, + values: impl IntoIterator, + ) -> Self { + self.attributes = Attributes( + values + .into_iter() + .map(|(a, b)| (a.to_owned(), b.to_owned())) + .collect::>(), + ); + self + } +} + +impl Html for UnorderedList { + fn to_html_string(&self) -> String { + let children = self + .children + .iter() + .map(|item| format!("
  • {}
  • ", item.to_html_string())) + .collect::>(); + format!( + "
      + {children} +
    ", + attrs = self.attributes.to_string(), + children = children.join("\n") + ) + } +} + +impl HtmlContainer for UnorderedList { + fn add_html(&mut self, html: H) { + self.children.push(html.to_html_string()) + } +} -- 2.44.1 From e96b8087e21b66c347ac1a44d1a1faed398f3996 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 25 Oct 2023 10:11:24 -0400 Subject: [PATCH 2/4] Add filenames to FileInfo and then set those filenames when creating the file --- file-service/src/store/filehandle.rs | 19 ++++++++++++++++++- file-service/src/store/fileinfo.rs | 7 +++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs index e03974f..c516411 100644 --- a/file-service/src/store/filehandle.rs +++ b/file-service/src/store/filehandle.rs @@ -120,8 +120,13 @@ impl FileHandle { /// Create a new entry in the database pub fn new(filename: String, root: PathBuf) -> Result { let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); + let path = PathBuf::from(filename); - let extension = PathBuf::from(filename) + let name = path + .file_stem() + .and_then(|s| s.to_str().map(|s| s.to_owned())) + .ok_or(WriteFileError::InvalidPath)?; + let extension = path .extension() .and_then(|s| s.to_str().map(|s| s.to_owned())) .ok_or(WriteFileError::InvalidPath)?; @@ -138,6 +143,7 @@ impl FileHandle { let info = FileInfo { id: id.clone(), + name, size: 0, created: Utc::now(), file_type, @@ -233,6 +239,17 @@ mod test { ); } + #[test] + fn it_creates_file_info() { + let tmp = TempDir::new("var").unwrap(); + let handle = + FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); + assert_eq!(handle.info.name, Some("rawr".to_owned())); + assert_eq!(handle.info.size, 0); + assert_eq!(handle.info.file_type, "image/png"); + assert_eq!(handle.info.extension, "png"); + } + #[test] fn it_opens_a_file() { let tmp = TempDir::new("var").unwrap(); diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index 0574550..bcfbc4e 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -11,6 +11,12 @@ use std::{ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInfo { pub id: FileId, + + // Early versions of the application didn't support a name field, so it is possible that + // metadata won't contain the name. We can just default to an empty string when loading the + // metadata, as all future versions will require a filename when the file gets uploaded. + #[serde(default)] + pub name: String, pub size: usize, pub created: DateTime, pub file_type: String, @@ -50,6 +56,7 @@ mod test { let info = FileInfo { id: FileId("temp-id".to_owned()), + name: "test-image".to_owned(), size: 23777, created, file_type: "image/png".to_owned(), -- 2.44.1 From ee348c29cbd87f6480bddb0139316af25216596e Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 25 Oct 2023 10:20:14 -0400 Subject: [PATCH 3/4] Render the name and the uploaded date for each file in the gallery --- file-service/src/pages.rs | 33 ++++++++++++++++++++------------ file-service/templates/style.css | 4 ++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 18dd832..65997e3 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,6 +1,6 @@ use crate::html::*; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; -use file_service::{FileHandle, FileId, ReadFileError}; +use file_service::{FileHandle, FileInfo, ReadFileError}; pub fn auth(_message: Option) -> build_html::HtmlPage { build_html::HtmlPage::new() @@ -49,14 +49,7 @@ pub fn gallery(handles: Vec>) -> build_html::H let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]); for handle in handles { let container = match handle { - Ok(ref handle) => thumbnail(&handle.id).with_html( - Form::new() - .with_path(&format!("/{}", *handle.id)) - .with_method("post") - .with_html(Input::new("hidden", "_method").with_value("delete")) - .with_html(Button::new("Delete")), - ), - + Ok(ref handle) => thumbnail(&handle.info), Err(err) => Container::new(ContainerType::Div) .with_attributes(vec![("class", "file")]) .with_paragraph(format!("{:?}", err)), @@ -88,15 +81,31 @@ pub fn upload_form() -> Form { ) } -pub fn thumbnail(id: &FileId) -> Container { +pub fn thumbnail(info: &FileInfo) -> Container { Container::new(ContainerType::Div) .with_attributes(vec![("class", "card thumbnail")]) .with_html( Container::new(ContainerType::Div).with_link( - format!("/{}", **id), - Image::new(&format!("{}/tn", **id)) + format!("/{}", *info.id), + Image::new(&format!("{}/tn", *info.id)) .with_attributes([("class", "thumbnail__image")]) .to_html_string(), ), ) + .with_html( + Container::new(ContainerType::Div) + .with_html( + UnorderedList::new() + .with_attributes(vec![("class", "thumbnail__metadata")]) + .with_html(info.name.clone()) + .with_html(format!("{}", info.created.format("%Y-%m-%d"))), + ) + .with_html( + Form::new() + .with_path(&format!("/{}", *info.id)) + .with_method("post") + .with_html(Input::new("hidden", "_method").with_value("delete")) + .with_html(Button::new("Delete")), + ), + ) } diff --git a/file-service/templates/style.css b/file-service/templates/style.css index ae81cdf..a423bbf 100644 --- a/file-service/templates/style.css +++ b/file-service/templates/style.css @@ -77,6 +77,10 @@ body { border: none; } +.thumbnail__metadata { + list-style: none; +} + /* [type="submit"] { border-radius: 1em; -- 2.44.1 From 66876e41c075ecb2dab762af127f5cd47e69ffe5 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 25 Oct 2023 10:35:24 -0400 Subject: [PATCH 4/4] Clean up broken tests and clippy warnings --- file-service/src/store/filehandle.rs | 2 +- tree/src/lib.rs | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs index c516411..cd57693 100644 --- a/file-service/src/store/filehandle.rs +++ b/file-service/src/store/filehandle.rs @@ -244,7 +244,7 @@ mod test { let tmp = TempDir::new("var").unwrap(); let handle = FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); - assert_eq!(handle.info.name, Some("rawr".to_owned())); + assert_eq!(handle.info.name, "rawr"); assert_eq!(handle.info.size, 0); assert_eq!(handle.info.file_type, "image/png"); assert_eq!(handle.info.extension, "png"); diff --git a/tree/src/lib.rs b/tree/src/lib.rs index 2b9cdc8..4644876 100644 --- a/tree/src/lib.rs +++ b/tree/src/lib.rs @@ -9,18 +9,13 @@ use std::{ rc::Rc, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub enum Tree { + #[default] Empty, Root(Node), } -impl Default for Tree { - fn default() -> Self { - Tree::Empty - } -} - impl Tree { pub fn new(value: T) -> (Tree, Node) { let node = Node::new(value); @@ -129,14 +124,14 @@ impl Node { } /// Immutably retrieve the data in this node. - pub fn value<'a>(&self) -> Ref { + pub fn value(&self) -> Ref { // Ref::map is not actually a member function. I don't know why this was done, other than // maybe to avoid conflicting with other `map` declarations. Why that is necessary when // Option::map exists as a member, I don't know. Ref::map(self.0.borrow(), |v| &v.value) } - pub fn children<'a>(&self) -> Ref>> { + pub fn children(&self) -> Ref>> { Ref::map(self.0.borrow(), |v| &v.children) } -- 2.44.1