implement artwork upload

This commit is contained in:
cel 🌸 2024-11-14 21:43:54 +00:00
parent 469a3ad339
commit 67b54449a1
19 changed files with 325 additions and 79 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
/files

50
Cargo.lock generated
View File

@ -411,6 +411,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -1089,6 +1098,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 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]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -1116,6 +1135,24 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[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]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.29.0"
@ -1336,9 +1373,12 @@ dependencies = [
"headers", "headers",
"http", "http",
"http-body-util", "http-body-util",
"httpdate",
"hyper", "hyper",
"hyper-util", "hyper-util",
"mime", "mime",
"mime_guess",
"multer",
"nix", "nix",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
@ -1353,6 +1393,7 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"smallvec", "smallvec",
"sync_wrapper", "sync_wrapper",
"tempfile",
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
@ -2240,6 +2281,15 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.17" version = "0.3.17"

View File

@ -11,7 +11,7 @@ ructe = { version = "0.17.2", features = ["sass", "mime03"] }
[dependencies] [dependencies]
mime = "0.3.17" 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" serde = "1.0.215"
sqlx = { version = "0.8.2", features = ["uuid", "postgres", "runtime-tokio", "time"] } sqlx = { version = "0.8.2", features = ["uuid", "postgres", "runtime-tokio", "time"] }
time = "0.3.36" time = "0.3.36"

View File

@ -1,4 +1,4 @@
admin_password = "clowning" admin_password = "clowning"
# site_password = "password" # site_password = "password"
files_dir = "./files" files_dir = "/home/cel/src/critch/files"
database_connection = "postgres://critch:critch@localhost/critch" database_connection = "postgres://critch:critch@localhost/critch"

View File

@ -1,7 +1,7 @@
create extension if not exists "uuid-ossp"; create extension if not exists "uuid-ossp";
create table artists ( 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, handle varchar(128) not null unique,
name varchar(128), name varchar(128),
bio text, bio text,
@ -9,39 +9,39 @@ create table artists (
); );
create table artworks ( create table artworks (
id integer primary key generated always as identity, artwork_id integer primary key generated always as identity,
title varchar(256), title varchar(256),
description text, description text,
url_source varchar(256), url_source varchar(256),
created_at timestamp not null default current_timestamp, created_at timestamp not null default current_timestamp,
artist_id integer not null, artist_id integer not null,
comment_number integer not null default 0, 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 ( create table comments (
id integer unique not null, comment_id integer unique not null,
text text not null, text text not null,
artwork_id integer not null, artwork_id integer not null,
created_at timestamp not null default current_timestamp, created_at timestamp not null default current_timestamp,
primary key (id, artwork_id), primary key (comment_id, artwork_id),
foreign key (artwork_id) references artworks(id) foreign key (artwork_id) references artworks(artwork_id)
); );
create table comment_relations ( create table comment_relations (
artwork_id integer, artwork_id integer,
foreign key (artwork_id) references artworks(id), foreign key (artwork_id) references artworks(artwork_id),
in_reply_to_id integer, 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, 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) primary key (artwork_id, in_reply_to_id, comment_id)
); );
create table artwork_files ( create table artwork_files (
id uuid primary key default gen_random_uuid(), file_id uuid primary key default gen_random_uuid(),
alt_text text, alt_text text,
extension varchar(16), extension varchar(16),
artwork_id integer, artwork_id integer,
foreign key (artwork_id) references artworks(id) foreign key (artwork_id) references artworks(artwork_id)
); );

View File

@ -1,9 +1,9 @@
use crate::error::Error; use crate::error::Error;
use crate::Result; use crate::Result;
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow, sqlx::Type)]
pub struct Artist { pub struct Artist {
id: Option<i32>, artist_id: Option<i32>,
pub handle: String, pub handle: String,
pub name: Option<String>, pub name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -11,7 +11,17 @@ pub struct Artist {
} }
impl Artist { impl Artist {
pub fn id(&self) -> Option<i32> { pub fn new(handle: String) -> Self {
self.id Self {
artist_id: None,
handle,
name: None,
bio: None,
site: None,
}
}
pub fn artist_id(&self) -> Option<i32> {
self.artist_id
} }
} }

View File

@ -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>,
}

View File

@ -6,7 +6,7 @@ use crate::Result;
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
pub struct Comment { pub struct Comment {
/// id of the comment in the thread /// id of the comment in the thread
id: Option<i32>, comment_id: Option<i32>,
/// text of the comment /// text of the comment
pub text: String, pub text: String,
/// id of artwork thread comment is in /// id of artwork thread comment is in
@ -21,7 +21,7 @@ pub struct Comment {
impl Comment { impl Comment {
pub fn id(&self) -> Option<i32> { pub fn id(&self) -> Option<i32> {
self.id self.comment_id
} }
pub fn created_at(&self) -> Option<OffsetDateTime> { pub fn created_at(&self) -> Option<OffsetDateTime> {

View File

@ -13,7 +13,7 @@ impl Artists {
pub async fn create(&self, artist: Artist) -> Result<i32> { pub async fn create(&self, artist: Artist) -> Result<i32> {
let artist_id = sqlx::query!( 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.handle,
artist.name, artist.name,
artist.bio, artist.bio,
@ -21,7 +21,7 @@ impl Artists {
) )
.fetch_one(&self.0) .fetch_one(&self.0)
.await? .await?
.id; .artist_id;
Ok(artist_id) Ok(artist_id)
} }

View File

@ -1,13 +1,49 @@
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use uuid::Uuid; use uuid::Uuid;
use crate::artist::Artist;
use crate::artwork::Artwork;
use crate::file::File;
use crate::Result; use crate::Result;
use super::Database; 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)] #[derive(Clone)]
pub struct Artworks(Pool<Postgres>); pub struct Artworks(Pool<Postgres>);
@ -21,16 +57,17 @@ impl Artworks {
} }
pub async fn create(&self, artwork: Artwork) -> Result<i32> { 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 artist_id
} else { } else {
self.downcast().artists().create(artwork.artist).await? 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 { for file in artwork.files {
sqlx::query!( sqlx::query!(
"insert into artwork_files (id, alt_text, extension, artwork_id) values ($1, $2, $3, $4)", "insert into artwork_files (file_id, alt_text, extension, artwork_id) values ($1, $2, $3, $4)",
file.id(), file.file_id(),
file.alt_text, file.alt_text,
file.extension(), file.extension(),
artwork_id artwork_id
@ -43,8 +80,12 @@ impl Artworks {
pub async fn read_all(&self) -> Result<Vec<Artwork>> { pub async fn read_all(&self) -> Result<Vec<Artwork>> {
// TODO: join comments and files // TODO: join comments and files
Ok(sqlx::query_as( Ok(sqlx::query_as!(Artwork,
"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", 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) .fetch_all(&self.0)
.await?) .await?)

View File

@ -13,13 +13,13 @@ impl Comments {
pub async fn create(&self, comment: Comment) -> Result<i32> { pub async fn create(&self, comment: Comment) -> Result<i32> {
let comment_id = sqlx::query!( 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.text,
comment.artwork_id comment.artwork_id
) )
.fetch_one(&self.0) .fetch_one(&self.0)
.await? .await?
.id; .comment_id;
for in_reply_to_id in comment.in_reply_to_ids { 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?; 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>> { pub async fn read_thread(&self, artwork_id: i32) -> Result<Vec<Comment>> {
Ok(sqlx::query_as("select * from comments") Ok(
sqlx::query_as("select * from comments where artwork_id = $1")
.bind(artwork_id)
.fetch_all(&self.0) .fetch_all(&self.0)
.await?) .await?,
)
} }
} }

View File

@ -3,9 +3,9 @@ use artworks::Artworks;
use comments::Comments; use comments::Comments;
use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
mod artists; pub mod artists;
mod artworks; pub mod artworks;
mod comments; pub mod comments;
#[derive(Clone)] #[derive(Clone)]
pub struct Database(Pool<Postgres>); pub struct Database(Pool<Postgres>);

View File

@ -1,6 +1,9 @@
use std::fmt::Display; use std::fmt::Display;
use poem::{error::ResponseError, http::StatusCode}; use poem::{
error::{ParseMultipartError, ResponseError},
http::StatusCode,
};
use sqlx::postgres::PgDatabaseError; use sqlx::postgres::PgDatabaseError;
#[derive(Debug)] #[derive(Debug)]
@ -11,6 +14,11 @@ pub enum Error {
DatabaseError(sqlx::Error), DatabaseError(sqlx::Error),
NotFound, NotFound,
MissingField, MissingField,
Unauthorized,
MultipartError,
BadRequest,
UnsupportedFileType(String),
UploadDirectory(String),
} }
impl Display for Error { impl Display for Error {
@ -22,6 +30,15 @@ impl Display for Error {
Error::DatabaseError(error) => write!(f, "database error: {}", error), Error::DatabaseError(error) => write!(f, "database error: {}", error),
Error::NotFound => write!(f, "not found"), Error::NotFound => write!(f, "not found"),
Error::MissingField => write!(f, "missing field in row"), 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::NotFound => StatusCode::NOT_FOUND,
Error::SQLError(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::SQLError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::MissingField => 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
}
}

View File

@ -1,26 +1,34 @@
use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
#[derive(sqlx::FromRow)] use crate::error::Error;
use crate::Result;
#[derive(sqlx::FromRow, sqlx::Type)]
pub struct File { pub struct File {
id: Uuid, file_id: Uuid,
pub alt_text: String, pub alt_text: String,
extension: String, extension: String,
artwork_id: Option<i32>, artwork_id: Option<i32>,
} }
impl File { impl File {
pub fn new(file: std::fs::File, extension: String) -> Self { pub fn new(content_type: &str) -> Result<Self> {
let id = Uuid::new_v4(); let file_id = Uuid::new_v4();
Self { let content_type: FileType = FromStr::from_str(content_type)?;
id, 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(), alt_text: String::new(),
extension, extension,
artwork_id: None, artwork_id: None,
} })
} }
pub fn id(&self) -> Uuid { pub fn file_id(&self) -> Uuid {
self.id self.file_id
} }
pub fn extension(&self) -> &str { pub fn extension(&self) -> &str {
@ -31,3 +39,49 @@ impl File {
self.artwork_id 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())),
}
}
}

View File

@ -1,5 +1,9 @@
#![feature(async_closure)]
use config::Config; use config::Config;
use db::Database; use db::Database;
use error::Error;
use tokio::{fs::File, io::AsyncWriteExt};
mod artist; mod artist;
mod artwork; mod artwork;
@ -25,6 +29,20 @@ impl Critch {
Self { db, config } 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")); include!(concat!(env!("OUT_DIR"), "/templates.rs"));

View File

@ -1,3 +1,4 @@
use poem::endpoint::StaticFilesEndpoint;
use poem::session::{CookieConfig, CookieSession}; use poem::session::{CookieConfig, CookieSession};
use poem::web::cookie::CookieKey; use poem::web::cookie::CookieKey;
use poem::{delete, post, put, EndpointExt}; use poem::{delete, post, put, EndpointExt};
@ -10,6 +11,7 @@ use critch::Critch;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), std::io::Error> { async fn main() -> Result<(), std::io::Error> {
let config = Config::from_file("./critch.toml").unwrap(); let config = Config::from_file("./critch.toml").unwrap();
let uploads_dir = config.files_dir().to_owned();
let state = Critch::new(config).await; let state = Critch::new(config).await;
let cookie_config = CookieConfig::private(CookieKey::generate()); let cookie_config = CookieConfig::private(CookieKey::generate());
@ -47,6 +49,7 @@ async fn main() -> Result<(), std::io::Error> {
"/artworks/:artwork/comments", "/artworks/:artwork/comments",
post(routes::artworks::comments::post).delete(routes::artworks::comments::delete), post(routes::artworks::comments::post).delete(routes::artworks::comments::delete),
) )
.nest("/uploads", StaticFilesEndpoint::new(uploads_dir))
.catch_all_error(routes::error::error) .catch_all_error(routes::error::error)
.data(state) .data(state)
.with(cookie_session); .with(cookie_session);

View File

@ -19,7 +19,7 @@ pub async fn get_dashboard(session: &Session, critch: Data<&Critch>) -> Result<R
if let Some(true) = session.get("is_admin") { if let Some(true) = session.get("is_admin") {
let comments = critch.db.comments().read_all().await?; let comments = critch.db.comments().read_all().await?;
let artworks = critch.db.artworks().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 { } else {
return Ok(Redirect::see_other("/admin/login").into_response()); return Ok(Redirect::see_other("/admin/login").into_response());
} }

View File

@ -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; pub mod comments;
#[handler] #[handler]
pub async fn post() { pub async fn post(
todo!() 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] #[handler]

View File

@ -1,8 +1,33 @@
@use super::base_html; @use super::base_html;
@use crate::db::artworks::Artwork;
@() @(artworks: Vec<Artwork>)
@:base_html({ @: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"> <form action="/admin/logout" method="post">
<button type="logout">log out</button> <button type="logout">log out</button>
</form> </form>