diff options
Diffstat (limited to '')
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 50 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | critch.toml | 2 | ||||
| -rw-r--r-- | migrations/20241113160730_critch.sql | 22 | ||||
| -rw-r--r-- | src/artist.rs | 18 | ||||
| -rw-r--r-- | src/artwork.rs | 26 | ||||
| -rw-r--r-- | src/comment.rs | 4 | ||||
| -rw-r--r-- | src/db/artists.rs | 4 | ||||
| -rw-r--r-- | src/db/artworks.rs | 59 | ||||
| -rw-r--r-- | src/db/comments.rs | 13 | ||||
| -rw-r--r-- | src/db/mod.rs | 6 | ||||
| -rw-r--r-- | src/error.rs | 30 | ||||
| -rw-r--r-- | src/file.rs | 72 | ||||
| -rw-r--r-- | src/lib.rs | 18 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/routes/admin.rs | 2 | ||||
| -rw-r--r-- | src/routes/artworks.rs | 45 | ||||
| -rw-r--r-- | templates/admin_dashboard.rs.html | 27 | 
19 files changed, 325 insertions, 79 deletions
@@ -1 +1,2 @@  /target +/files @@ -412,6 +412,15 @@ dependencies = [  ]  [[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]]  name = "equivalent"  version = "1.0.1"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1090,6 +1099,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"  [[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]]  name = "minimal-lexical"  version = "0.2.1"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1117,6 +1136,24 @@ dependencies = [  ]  [[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "tokio", + "version_check", +] + +[[package]]  name = "nix"  version = "0.29.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1336,9 +1373,12 @@ dependencies = [   "headers",   "http",   "http-body-util", + "httpdate",   "hyper",   "hyper-util",   "mime", + "mime_guess", + "multer",   "nix",   "parking_lot",   "percent-encoding", @@ -1353,6 +1393,7 @@ dependencies = [   "serde_urlencoded",   "smallvec",   "sync_wrapper", + "tempfile",   "thiserror",   "time",   "tokio", @@ -2241,6 +2282,15 @@ dependencies = [  ]  [[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]]  name = "unicode-bidi"  version = "0.3.17"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -11,7 +11,7 @@ ructe = { version = "0.17.2", features = ["sass", "mime03"] }  [dependencies]  mime = "0.3.17" -poem = { version = "3.1.3", features = ["session"] } +poem = { version = "3.1.3", features = ["session", "tempfile", "multipart", "static-files"] }  serde = "1.0.215"  sqlx = { version = "0.8.2", features = ["uuid", "postgres", "runtime-tokio", "time"] }  time = "0.3.36" diff --git a/critch.toml b/critch.toml index 6b578b9..b8789d6 100644 --- a/critch.toml +++ b/critch.toml @@ -1,4 +1,4 @@  admin_password = "clowning"  # site_password = "password" -files_dir = "./files" +files_dir = "/home/cel/src/critch/files"  database_connection = "postgres://critch:critch@localhost/critch" diff --git a/migrations/20241113160730_critch.sql b/migrations/20241113160730_critch.sql index 131daf3..013fd07 100644 --- a/migrations/20241113160730_critch.sql +++ b/migrations/20241113160730_critch.sql @@ -1,7 +1,7 @@  create extension if not exists "uuid-ossp";  create table artists ( -    id integer primary key generated always as identity, +    artist_id integer primary key generated always as identity,      handle varchar(128) not null unique,      name varchar(128),      bio text, @@ -9,39 +9,39 @@ create table artists (  );  create table artworks ( -    id integer primary key generated always as identity, +    artwork_id integer primary key generated always as identity,      title varchar(256),      description text,      url_source varchar(256),      created_at timestamp not null default current_timestamp,      artist_id integer not null,      comment_number integer not null default 0, -    foreign key (artist_id) references artists(id) +    foreign key (artist_id) references artists(artist_id)  );  create table comments ( -    id integer unique not null, +    comment_id integer unique not null,      text text not null,      artwork_id integer not null,      created_at timestamp not null default current_timestamp, -    primary key (id, artwork_id), -    foreign key (artwork_id) references artworks(id) +    primary key (comment_id, artwork_id), +    foreign key (artwork_id) references artworks(artwork_id)  );  create table comment_relations (      artwork_id integer, -    foreign key (artwork_id) references artworks(id), +    foreign key (artwork_id) references artworks(artwork_id),      in_reply_to_id integer, -    foreign key (in_reply_to_id) references comments(id), +    foreign key (in_reply_to_id) references comments(comment_id),      comment_id integer, -    foreign key (comment_id) references comments(id), +    foreign key (comment_id) references comments(comment_id),      primary key (artwork_id, in_reply_to_id, comment_id)  );  create table artwork_files ( -    id uuid primary key default gen_random_uuid(), +    file_id uuid primary key default gen_random_uuid(),      alt_text text,      extension varchar(16),      artwork_id integer, -    foreign key (artwork_id) references artworks(id) +    foreign key (artwork_id) references artworks(artwork_id)  ); diff --git a/src/artist.rs b/src/artist.rs index 476dad9..0ea9131 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -1,9 +1,9 @@  use crate::error::Error;  use crate::Result; -#[derive(sqlx::FromRow)] +#[derive(sqlx::FromRow, sqlx::Type)]  pub struct Artist { -    id: Option<i32>, +    artist_id: Option<i32>,      pub handle: String,      pub name: Option<String>,      pub bio: Option<String>, @@ -11,7 +11,17 @@ pub struct Artist {  }  impl Artist { -    pub fn id(&self) -> Option<i32> { -        self.id +    pub fn new(handle: String) -> Self { +        Self { +            artist_id: None, +            handle, +            name: None, +            bio: None, +            site: None, +        } +    } + +    pub fn artist_id(&self) -> Option<i32> { +        self.artist_id      }  } diff --git a/src/artwork.rs b/src/artwork.rs index 458fd38..8b13789 100644 --- a/src/artwork.rs +++ b/src/artwork.rs @@ -1,27 +1 @@ -use time::{OffsetDateTime, PrimitiveDateTime}; -use uuid::Uuid; -use crate::{artist::Artist, comment::Comment, file::File}; - -#[derive(sqlx::FromRow)] -pub struct Artwork { -    /// artwork id -    id: Option<i32>, -    /// name of the artwork -    pub title: Option<String>, -    /// description of the artwork -    pub description: Option<String>, -    /// source url of the artwork -    pub url_source: Option<String>, -    /// artwork creation time -    created_at: Option<PrimitiveDateTime>, -    /// id of the artist -    #[sqlx(Flatten)] -    pub artist: Artist, -    /// ids of files -    #[sqlx(Flatten)] -    pub files: Vec<File>, -    // /// TODO: comments in thread, -    // #[sqlx(Flatten)] -    // comments: Vec<Comment>, -} diff --git a/src/comment.rs b/src/comment.rs index 55c4607..6aa3ee8 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -6,7 +6,7 @@ use crate::Result;  #[derive(sqlx::FromRow)]  pub struct Comment {      /// id of the comment in the thread -    id: Option<i32>, +    comment_id: Option<i32>,      /// text of the comment      pub text: String,      /// id of artwork thread comment is in @@ -21,7 +21,7 @@ pub struct Comment {  impl Comment {      pub fn id(&self) -> Option<i32> { -        self.id +        self.comment_id      }      pub fn created_at(&self) -> Option<OffsetDateTime> { diff --git a/src/db/artists.rs b/src/db/artists.rs index 043f0bd..08a5968 100644 --- a/src/db/artists.rs +++ b/src/db/artists.rs @@ -13,7 +13,7 @@ impl Artists {      pub async fn create(&self, artist: Artist) -> Result<i32> {          let artist_id = sqlx::query!( -            "insert into artists (handle, name, bio, site) values ($1, $2, $3, $4) returning id", +            "insert into artists (handle, name, bio, site) values ($1, $2, $3, $4) returning artist_id",              artist.handle,              artist.name,              artist.bio, @@ -21,7 +21,7 @@ impl Artists {          )          .fetch_one(&self.0)          .await? -        .id; +        .artist_id;          Ok(artist_id)      } diff --git a/src/db/artworks.rs b/src/db/artworks.rs index 619f42d..0b62d1d 100644 --- a/src/db/artworks.rs +++ b/src/db/artworks.rs @@ -1,13 +1,49 @@  use sqlx::{Pool, Postgres};  use uuid::Uuid; -use crate::artist::Artist; -use crate::artwork::Artwork; -use crate::file::File;  use crate::Result;  use super::Database; +use time::{OffsetDateTime, PrimitiveDateTime}; + +use crate::{artist::Artist, comment::Comment, file::File}; + +#[derive(sqlx::FromRow)] +pub struct Artwork { +    /// artwork id +    artwork_id: Option<i32>, +    /// name of the artwork +    pub title: Option<String>, +    /// description of the artwork +    pub description: Option<String>, +    /// source url of the artwork +    pub url_source: Option<String>, +    /// artwork creation time +    created_at: Option<PrimitiveDateTime>, +    /// id of the artist +    pub artist: Artist, +    /// ids of files +    pub files: Vec<File>, +    // /// TODO: comments in thread, +    // #[sqlx(Flatten)] +    // comments: Vec<Comment>, +} + +impl Artwork { +    pub fn new(title: Option<String>, description: Option<String>, url_source: Option<String>, artist: Artist, files: Vec<File>) -> Self { +        Self { +            artwork_id: None, +            title, +            description, +            url_source, +            created_at: None, +            artist, +            files, +        } +    } +} +  #[derive(Clone)]  pub struct Artworks(Pool<Postgres>); @@ -21,16 +57,17 @@ impl Artworks {      }      pub async fn create(&self, artwork: Artwork) -> Result<i32> { -        let artist_id = if let Some(artist_id) = artwork.artist.id() { +        // TODO: efficiency? +        let artist_id = if let Some(artist_id) = self.downcast().artists().read_handle(&artwork.artist.handle).await.map(|artist| artist.artist_id()).unwrap_or(artwork.artist.artist_id()) {              artist_id          } else {              self.downcast().artists().create(artwork.artist).await?          }; -        let artwork_id = sqlx::query!("insert into artworks (title, description, url_source, artist_id) values ($1, $2, $3, $4) returning id", artwork.title, artwork.description, artwork.url_source, artist_id).fetch_one(&self.0).await?.id; +        let artwork_id = sqlx::query!("insert into artworks (title, description, url_source, artist_id) values ($1, $2, $3, $4) returning artwork_id", artwork.title, artwork.description, artwork.url_source, artist_id).fetch_one(&self.0).await?.artwork_id;          for file in artwork.files {              sqlx::query!( -                "insert into artwork_files (id, alt_text, extension, artwork_id) values ($1, $2, $3, $4)", -                file.id(), +                "insert into artwork_files (file_id, alt_text, extension, artwork_id) values ($1, $2, $3, $4)", +                file.file_id(),                  file.alt_text,                  file.extension(),                  artwork_id @@ -43,8 +80,12 @@ impl Artworks {      pub async fn read_all(&self) -> Result<Vec<Artwork>> {          // TODO: join comments and files -        Ok(sqlx::query_as( -            "select * from artworks left join artists on artworks.artist_id = artists.id left join artwork_files on artworks.id = artwork_files.artwork_id group by artworks.id, artists.id, artwork_files.id", +        Ok(sqlx::query_as!(Artwork,  +            r#"select artworks.artwork_id, artworks.title, artworks.description, artworks.url_source, artworks.created_at, coalesce(artists.*) as "artist!: Artist", coalesce(array_agg((artwork_files.file_id, artwork_files.alt_text, artwork_files.extension, artwork_files.artwork_id)) filter (where artwork_files.file_id is not null), '{}') as "files!: Vec<File>"  +            from artworks +            left join artists on artworks.artist_id = artists.artist_id  +            left join artwork_files on artworks.artwork_id = artwork_files.artwork_id  +            group by artworks.artwork_id, artists.artist_id"#,          )          .fetch_all(&self.0)          .await?) diff --git a/src/db/comments.rs b/src/db/comments.rs index ec07aa0..3c14852 100644 --- a/src/db/comments.rs +++ b/src/db/comments.rs @@ -13,13 +13,13 @@ impl Comments {      pub async fn create(&self, comment: Comment) -> Result<i32> {          let comment_id = sqlx::query!( -            r#"insert into comments (text, artwork_id) values ($1, $2) returning id"#, +            r#"insert into comments (text, artwork_id) values ($1, $2) returning comment_id"#,              comment.text,              comment.artwork_id          )          .fetch_one(&self.0)          .await? -        .id; +        .comment_id;          for in_reply_to_id in comment.in_reply_to_ids {              sqlx::query!("insert into comment_relations (artwork_id, in_reply_to_id, comment_id) values ($1, $2, $3)", comment.artwork_id, in_reply_to_id, comment_id).execute(&self.0).await?;          } @@ -35,8 +35,11 @@ impl Comments {      }      pub async fn read_thread(&self, artwork_id: i32) -> Result<Vec<Comment>> { -        Ok(sqlx::query_as("select * from comments") -            .fetch_all(&self.0) -            .await?) +        Ok( +            sqlx::query_as("select * from comments where artwork_id = $1") +                .bind(artwork_id) +                .fetch_all(&self.0) +                .await?, +        )      }  } diff --git a/src/db/mod.rs b/src/db/mod.rs index 79e8717..6f794a7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,9 +3,9 @@ use artworks::Artworks;  use comments::Comments;  use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; -mod artists; -mod artworks; -mod comments; +pub mod artists; +pub mod artworks; +pub mod comments;  #[derive(Clone)]  pub struct Database(Pool<Postgres>); diff --git a/src/error.rs b/src/error.rs index ef8ddd3..2704519 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@  use std::fmt::Display; -use poem::{error::ResponseError, http::StatusCode}; +use poem::{ +    error::{ParseMultipartError, ResponseError}, +    http::StatusCode, +};  use sqlx::postgres::PgDatabaseError;  #[derive(Debug)] @@ -11,6 +14,11 @@ pub enum Error {      DatabaseError(sqlx::Error),      NotFound,      MissingField, +    Unauthorized, +    MultipartError, +    BadRequest, +    UnsupportedFileType(String), +    UploadDirectory(String),  }  impl Display for Error { @@ -22,6 +30,15 @@ impl Display for Error {              Error::DatabaseError(error) => write!(f, "database error: {}", error),              Error::NotFound => write!(f, "not found"),              Error::MissingField => write!(f, "missing field in row"), +            Error::Unauthorized => write!(f, "user unauthorized"), +            Error::MultipartError => write!(f, "error parsing multipart form data"), +            Error::BadRequest => write!(f, "bad request"), +            Error::UnsupportedFileType(filetype) => { +                write!(f, "unsupported file upload type: {}", filetype) +            } +            Error::UploadDirectory(directory) => { +                write!(f, "invalid uploads directory: {}", directory) +            }          }      }  } @@ -37,6 +54,11 @@ impl ResponseError for Error {              Error::NotFound => StatusCode::NOT_FOUND,              Error::SQLError(_) => StatusCode::INTERNAL_SERVER_ERROR,              Error::MissingField => StatusCode::INTERNAL_SERVER_ERROR, +            Error::Unauthorized => StatusCode::UNAUTHORIZED, +            Error::MultipartError => StatusCode::BAD_REQUEST, +            Error::BadRequest => StatusCode::BAD_REQUEST, +            Error::UnsupportedFileType(_) => StatusCode::BAD_REQUEST, +            Error::UploadDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,          }      }  } @@ -67,3 +89,9 @@ impl From<sqlx::Error> for Error {          }      }  } + +impl From<ParseMultipartError> for Error { +    fn from(e: ParseMultipartError) -> Self { +        Error::MultipartError +    } +} diff --git a/src/file.rs b/src/file.rs index 15d457a..2e0dd1e 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,26 +1,34 @@ +use std::str::FromStr; +  use uuid::Uuid; -#[derive(sqlx::FromRow)] +use crate::error::Error; +use crate::Result; + +#[derive(sqlx::FromRow, sqlx::Type)]  pub struct File { -    id: Uuid, +    file_id: Uuid,      pub alt_text: String,      extension: String,      artwork_id: Option<i32>,  }  impl File { -    pub fn new(file: std::fs::File, extension: String) -> Self { -        let id = Uuid::new_v4(); -        Self { -            id, +    pub fn new(content_type: &str) -> Result<Self> { +        let file_id = Uuid::new_v4(); +        let content_type: FileType = FromStr::from_str(content_type)?; +        let extension = content_type.extension().to_owned(); +        // TODO: check file type really is content-type reported by form. +        Ok(Self { +            file_id,              alt_text: String::new(),              extension,              artwork_id: None, -        } +        })      } -    pub fn id(&self) -> Uuid { -        self.id +    pub fn file_id(&self) -> Uuid { +        self.file_id      }      pub fn extension(&self) -> &str { @@ -31,3 +39,49 @@ impl File {          self.artwork_id      }  } + +pub enum FileType { +    Audio(String), +    Video(String), +    Image(String), +    // TODO: text types +    // Text(String), +} + +impl FileType { +    pub fn extension(&self) -> &str { +        match self { +            FileType::Audio(e) => e, +            FileType::Video(e) => e, +            FileType::Image(e) => e, +        } +    } +} + +impl FromStr for FileType { +    type Err = Error; + +    fn from_str(s: &str) -> Result<Self> { +        match s { +            "mp4" | "video/mp4" => Ok(FileType::Video("mp4".to_owned())), +            "mpeg" | "video/mpeg" => Ok(FileType::Video("mpeg".to_owned())), +            "webm" | "video/webm" => Ok(FileType::Video("webm".to_owned())), +            "ogv" | "video/ogg" => Ok(FileType::Video("ogv".to_owned())), +            "mp3" | "audio/mpeg" => Ok(FileType::Audio("mp3".to_owned())), +            "weba" | "audio/webm" => Ok(FileType::Audio("weba".to_owned())), +            "aac" | "audio/aac" => Ok(FileType::Audio("aac".to_owned())), +            "oga" | "audio/ogg" => Ok(FileType::Audio("oga".to_owned())), +            "wav" | "audio/wav" => Ok(FileType::Audio("wav".to_owned())), +            "webp" | "image/webp" => Ok(FileType::Image("webp".to_owned())), +            "apng" | "image/apng" => Ok(FileType::Image("apng".to_owned())), +            "avif" | "image/avif" => Ok(FileType::Image("avif".to_owned())), +            "bmp" | "image/bmp" => Ok(FileType::Image("bmp".to_owned())), +            "gif" | "image/gif" => Ok(FileType::Image("gif".to_owned())), +            "jpeg" | "image/jpeg" => Ok(FileType::Image("jpeg".to_owned())), +            "png" | "image/png" => Ok(FileType::Image("png".to_owned())), +            "svg" | "image/svg+xml" => Ok(FileType::Image("svg".to_owned())), +            "tiff" | "image/tiff" => Ok(FileType::Image("tiff".to_owned())), +            s => Err(Error::UnsupportedFileType(s.to_owned())), +        } +    } +} @@ -1,5 +1,9 @@ +#![feature(async_closure)] +  use config::Config;  use db::Database; +use error::Error; +use tokio::{fs::File, io::AsyncWriteExt};  mod artist;  mod artwork; @@ -25,6 +29,20 @@ impl Critch {          Self { db, config }      } + +    pub async fn save_file(&self, file_name: &str, file_data: Vec<u8>) -> Result<()> { +        let upload_dir = self.config.files_dir(); +        if upload_dir.is_dir() { +            let file_handle = upload_dir.join(file_name); +            let mut file = File::create(file_handle).await?; +            file.write_all(&file_data).await?; +            return Ok(()); +        } else { +            return Err(Error::UploadDirectory( +                self.config.files_dir().to_string_lossy().to_string(), +            )); +        } +    }  }  include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/src/main.rs b/src/main.rs index 93ab9d6..e0bf429 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use poem::endpoint::StaticFilesEndpoint;  use poem::session::{CookieConfig, CookieSession};  use poem::web::cookie::CookieKey;  use poem::{delete, post, put, EndpointExt}; @@ -10,6 +11,7 @@ use critch::Critch;  #[tokio::main]  async fn main() -> Result<(), std::io::Error> {      let config = Config::from_file("./critch.toml").unwrap(); +    let uploads_dir = config.files_dir().to_owned();      let state = Critch::new(config).await;      let cookie_config = CookieConfig::private(CookieKey::generate()); @@ -47,6 +49,7 @@ async fn main() -> Result<(), std::io::Error> {              "/artworks/:artwork/comments",              post(routes::artworks::comments::post).delete(routes::artworks::comments::delete),          ) +        .nest("/uploads", StaticFilesEndpoint::new(uploads_dir))          .catch_all_error(routes::error::error)          .data(state)          .with(cookie_session); diff --git a/src/routes/admin.rs b/src/routes/admin.rs index ccca2de..7ba63ac 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -19,7 +19,7 @@ pub async fn get_dashboard(session: &Session, critch: Data<&Critch>) -> Result<R      if let Some(true) = session.get("is_admin") {          let comments = critch.db.comments().read_all().await?;          let artworks = critch.db.artworks().read_all().await?; -        return Ok(render!(templates::admin_dashboard_html).into_response()); +        return Ok(render!(templates::admin_dashboard_html, artworks).into_response());      } else {          return Ok(Redirect::see_other("/admin/login").into_response());      } diff --git a/src/routes/artworks.rs b/src/routes/artworks.rs index 556265f..fae7a10 100644 --- a/src/routes/artworks.rs +++ b/src/routes/artworks.rs @@ -1,10 +1,49 @@ -use poem::{handler, web::Path}; +use poem::web::{Data, Multipart, Redirect}; +use poem::IntoResponse; +use poem::{handler, session::Session, web::Path, Response}; + +use crate::artist::Artist; +use crate::db::artworks::Artwork; +use crate::error::Error; +use crate::file::File; +use crate::{Critch, Result};  pub mod comments;  #[handler] -pub async fn post() { -    todo!() +pub async fn post( +    session: &Session, +    mut multipart: Multipart, +    critch: Data<&Critch>, +) -> Result<Response> { +    if let Some(true) = session.get("is_admin") { +        let (mut title, mut handle, mut url_source, mut description, mut files) = +            (None, None, None, None, Vec::new()); +        while let Some(field) = multipart.next_field().await? { +            match field.name() { +                Some("title") => title = Some(field.text().await?), +                Some("artist") => handle = Some(field.text().await?), +                Some("url") => url_source = Some(field.text().await?), +                Some("description") => description = Some(field.text().await?), +                Some("file") => { +                    let content_type = field.content_type().ok_or(Error::BadRequest)?.to_owned(); +                    let file_data = field.bytes().await?; +                    let file = File::new(&content_type)?; +                    let file_name = format!("{}.{}", file.file_id(), file.extension()); +                    critch.save_file(&file_name, file_data).await?; +                    files.push(file); +                } +                _ => return Err(Error::BadRequest), +            } +        } +        let artist = Artist::new(handle.ok_or(Error::BadRequest)?); +        let artwork = Artwork::new(title, description, url_source, artist, files); +        critch.db.artworks().create(artwork).await?; +        println!("saved file"); +        return Ok(Redirect::see_other("/admin").into_response()); +    } else { +        return Err(Error::Unauthorized); +    }  }  #[handler] diff --git a/templates/admin_dashboard.rs.html b/templates/admin_dashboard.rs.html index cf7d27c..e8b94a2 100644 --- a/templates/admin_dashboard.rs.html +++ b/templates/admin_dashboard.rs.html @@ -1,8 +1,33 @@  @use super::base_html; +@use crate::db::artworks::Artwork; -@() +@(artworks: Vec<Artwork>)  @:base_html({ +<form action="/artworks" method="post" enctype="multipart/form-data"> +  <label for="title">title:</label><br> +  <input type="text" id="title" name="title"><br> +  <label for="artist">artist handle:</label><br> +  <input type="text" id="artist" name="artist" required><br> +  <label for="url">url source:</label><br> +  <input type="text" id="url" name="url"><br> +  <label for="description">description:</label><br> +  <textarea id="description" name="description"></textarea><br> +  <input type="file" name="file" multiple> +  <button type="post">post artwork</button> +</form> +<ul> +@for artwork in artworks { +  <li> +    @if let Some(title) = artwork.title { +      <h2>@title</h2> +    } +  @for file in artwork.files { +    <img src="/uploads/@file.file_id().@file.extension()"> +  } +  </li> +} +</ul>  <form action="/admin/logout" method="post">    <button type="logout">log out</button>  </form>  | 
