feat: implement simple file upload
This commit is contained in:
parent
c10b9baa95
commit
52478379a0
11 changed files with 232 additions and 9 deletions
|
|
@ -1,4 +1,5 @@
|
|||
mod models;
|
||||
#[rustfmt::skip]
|
||||
mod schema;
|
||||
|
||||
use diesel::{
|
||||
|
|
@ -10,6 +11,7 @@ use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
|||
use std::{error::Error, fmt, path::Path};
|
||||
|
||||
pub use models::event::{Event, NewEvent, EVENT_TYPES};
|
||||
pub use models::image::{Image, NewImage};
|
||||
pub use models::plant::{NewPlant, Plant};
|
||||
pub use models::session::Session;
|
||||
pub use models::user::{NewUser, User};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use chrono::NaiveDate;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::db::{schema::*, DbPool, DbResult};
|
||||
|
||||
|
|
@ -8,10 +8,10 @@ use crate::db::{schema::*, DbPool, DbResult};
|
|||
#[diesel(table_name = images)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Image {
|
||||
id: i32,
|
||||
plant_id: i32,
|
||||
date_taken: NaiveDate,
|
||||
note: Option<String>,
|
||||
pub id: i32,
|
||||
pub plant_id: i32,
|
||||
pub date_taken: NaiveDate,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Insertable)]
|
||||
|
|
@ -24,6 +24,14 @@ pub struct NewImage {
|
|||
}
|
||||
|
||||
impl NewImage {
|
||||
pub fn new(plant_id: i32, date_taken: NaiveDate, note: Option<String>) -> Self {
|
||||
Self {
|
||||
plant_id,
|
||||
date_taken,
|
||||
note,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(self, pool: &DbPool) -> DbResult<Image> {
|
||||
Ok(self
|
||||
.insert_into(images::table)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub mod event;
|
||||
pub mod image;
|
||||
pub mod plant;
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
pub mod image;
|
||||
|
|
|
|||
13
src/main.rs
13
src/main.rs
|
|
@ -2,7 +2,11 @@ mod cli;
|
|||
mod db;
|
||||
mod server;
|
||||
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use tera::Tera;
|
||||
|
|
@ -12,11 +16,13 @@ use cli::UserCmd;
|
|||
use db::DbError;
|
||||
|
||||
const DB_FILENAME: &str = "db.sqlite3";
|
||||
const IMG_DIR: &str = "imgs";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pool: db::DbPool,
|
||||
tera: Arc<Tera>,
|
||||
data_dir: PathBuf,
|
||||
}
|
||||
|
||||
fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError> {
|
||||
|
|
@ -59,6 +65,10 @@ async fn main() {
|
|||
fs::create_dir_all(&args.data_dir).unwrap();
|
||||
}
|
||||
|
||||
if !fs::exists(args.data_dir.join(IMG_DIR)).unwrap() {
|
||||
fs::create_dir(args.data_dir.join(IMG_DIR)).unwrap();
|
||||
}
|
||||
|
||||
let pool = db::initialize_db(args.data_dir.join(DB_FILENAME), true).unwrap();
|
||||
|
||||
let tera =
|
||||
|
|
@ -67,6 +77,7 @@ async fn main() {
|
|||
let ctx = Context {
|
||||
pool,
|
||||
tera: Arc::new(tera),
|
||||
data_dir: args.data_dir.clone(),
|
||||
};
|
||||
let app = server::app(ctx, &args.static_dir)
|
||||
.layer(CompressionLayer::new().br(true).gzip(true));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt::{self, Write};
|
||||
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use axum::{extract::multipart::MultipartError, http::StatusCode, response::IntoResponse};
|
||||
|
||||
use crate::db;
|
||||
|
||||
|
|
@ -8,6 +8,9 @@ use crate::db;
|
|||
pub enum AppError {
|
||||
Db(db::DbError),
|
||||
Tera(tera::Error),
|
||||
Multipart(MultipartError),
|
||||
IO(std::io::Error),
|
||||
BadRequest,
|
||||
Unauthorized,
|
||||
NotFound,
|
||||
}
|
||||
|
|
@ -17,6 +20,9 @@ impl fmt::Display for AppError {
|
|||
match self {
|
||||
Self::Db(_) => write!(f, "database error"),
|
||||
Self::Tera(_) => write!(f, "error rendering template"),
|
||||
Self::Multipart(_) => write!(f, "error processing multipart request"),
|
||||
Self::IO(_) => write!(f, "io error"),
|
||||
Self::BadRequest => write!(f, "bad request"),
|
||||
Self::Unauthorized => write!(f, "unauthorized"),
|
||||
Self::NotFound => write!(f, "not found"),
|
||||
}
|
||||
|
|
@ -28,7 +34,9 @@ impl std::error::Error for AppError {
|
|||
match self {
|
||||
Self::Db(err) => Some(err),
|
||||
Self::Tera(err) => Some(err),
|
||||
Self::NotFound | Self::Unauthorized => None,
|
||||
Self::IO(err) => Some(err),
|
||||
Self::Multipart(err) => Some(err),
|
||||
Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,11 +71,25 @@ impl From<tera::Error> for AppError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<MultipartError> for AppError {
|
||||
fn from(value: MultipartError) -> Self {
|
||||
Self::Multipart(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AppError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
Self::NotFound => StatusCode::NOT_FOUND.into_response(),
|
||||
Self::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
||||
Self::BadRequest => StatusCode::BAD_REQUEST.into_response(),
|
||||
Self::Multipart(err) => err.into_response(),
|
||||
_ => {
|
||||
tracing::error!("{}", self.stack());
|
||||
|
||||
|
|
|
|||
93
src/server/images.rs
Normal file
93
src/server/images.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use axum::{
|
||||
extract::{multipart::Field, Multipart, State},
|
||||
response::Html,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use futures::TryStreamExt;
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
use super::error::AppError;
|
||||
use crate::db::NewImage;
|
||||
|
||||
pub fn app() -> axum::Router<crate::Context> {
|
||||
Router::new().route("/", post(post_image))
|
||||
}
|
||||
|
||||
async fn post_image(
|
||||
State(ctx): State<crate::Context>,
|
||||
mut mt: Multipart,
|
||||
) -> super::Result<Html<String>> {
|
||||
let mut plant_id: Option<i32> = None;
|
||||
let mut date_taken: Option<NaiveDate> = None;
|
||||
let mut note: Option<String> = None;
|
||||
|
||||
while let Some(field) = mt.next_field().await? {
|
||||
match field.name() {
|
||||
Some("plant_id") => {
|
||||
plant_id = Some(
|
||||
field
|
||||
.text()
|
||||
.await?
|
||||
.parse()
|
||||
.map_err(|_| AppError::BadRequest)?,
|
||||
);
|
||||
}
|
||||
Some("date_taken") => {
|
||||
date_taken = Some(
|
||||
field
|
||||
.text()
|
||||
.await?
|
||||
.parse()
|
||||
.map_err(|_| AppError::BadRequest)?,
|
||||
);
|
||||
}
|
||||
Some("note") => {
|
||||
note = Some(field.text().await?);
|
||||
}
|
||||
Some("image") => {
|
||||
// These fields are required to be provided before the image field (note is
|
||||
// optional)
|
||||
if plant_id.is_none() || date_taken.is_none() {
|
||||
return Err(AppError::BadRequest);
|
||||
}
|
||||
|
||||
receive_image(ctx, plant_id.unwrap(), date_taken.unwrap(), note, field).await?;
|
||||
|
||||
return Ok(Html(String::new()));
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::BadRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::BadRequest)
|
||||
}
|
||||
|
||||
async fn receive_image<'a>(
|
||||
ctx: crate::Context,
|
||||
plant_id: i32,
|
||||
date_taken: NaiveDate,
|
||||
note: Option<String>,
|
||||
field: Field<'a>,
|
||||
) -> super::Result<()> {
|
||||
let image = tokio::task::spawn_blocking(move || {
|
||||
NewImage::new(plant_id, date_taken, note).insert(&ctx.pool)
|
||||
})
|
||||
.await
|
||||
.unwrap()?;
|
||||
|
||||
let mut r = StreamReader::new(field.map_err(std::io::Error::other));
|
||||
let mut f = tokio::fs::File::create(
|
||||
ctx.data_dir
|
||||
.join(crate::IMG_DIR)
|
||||
.join(image.id.to_string())
|
||||
.with_extension("png"),
|
||||
)
|
||||
.await?;
|
||||
tokio::io::copy(&mut r, &mut f).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
mod auth;
|
||||
mod error;
|
||||
mod events;
|
||||
mod images;
|
||||
mod plants;
|
||||
|
||||
use std::path::Path;
|
||||
|
|
@ -55,6 +56,7 @@ pub fn app(ctx: crate::Context, static_dir: impl AsRef<Path>) -> axum::Router {
|
|||
let router = Router::new()
|
||||
.nest("/plants", plants::app())
|
||||
.nest("/events", events::app())
|
||||
.nest("/images", images::app())
|
||||
.layer(middleware::from_fn_with_state(
|
||||
ctx.clone(),
|
||||
auth::auth_middleware,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue