implement artwork upload
This commit is contained in:
parent
469a3ad339
commit
67b54449a1
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/files
|
||||
|
|
|
@ -411,6 +411,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
|
@ -1089,6 +1098,16 @@ version = "0.3.17"
|
|||
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"
|
||||
|
@ -1116,6 +1135,24 @@ dependencies = [
|
|||
"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]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
|
@ -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",
|
||||
|
@ -2240,6 +2281,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.17"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>);
|
||||
|
|
30
src/error.rs
30
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
|
||||
}
|
||||
}
|
||||
|
|
72
src/file.rs
72
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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
18
src/lib.rs
18
src/lib.rs
|
@ -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"));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue