add image upload feature in dev mode

This commit is contained in:
りき萌 2024-12-08 12:45:29 +01:00
parent 47c2b74ecb
commit 0ce7f50285
11 changed files with 369 additions and 15 deletions

View file

@ -8,7 +8,8 @@ edition = "2021"
treehouse-format = { workspace = true }
anyhow = "1.0.75"
axum = "0.7.4"
axum = { version = "0.7.9", features = ["macros"] }
axum-macros = "0.4.2"
base64 = "0.21.7"
blake3 = "1.5.3"
chrono = { version = "0.4.35", features = ["serde"] }

View file

@ -166,12 +166,12 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
Edit::Seq(vec![
Edit::Write(
backup_edit_path,
treehouse.source(file_id).input().to_owned(),
treehouse.source(file_id).input().to_owned().into(),
),
Edit::Write(edit_path, fixed),
Edit::Write(edit_path, fixed.into()),
])
} else {
Edit::Write(edit_path, fixed)
Edit::Write(edit_path, fixed.into())
}
} else {
println!("{fixed}");
@ -201,7 +201,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
if fixed != treehouse.source(file_id).input() {
return Ok(Edit::Write(edit_path, fixed));
return Ok(Edit::Write(edit_path, fixed.into()));
}
} else {
report_diagnostics(&treehouse, &diagnostics)?;

View file

@ -1,5 +1,6 @@
#[cfg(debug_assertions)]
mod live_reload;
mod picture_upload;
use std::fmt::Write;
use std::{net::Ipv4Addr, sync::Arc};
@ -18,6 +19,7 @@ use serde::Deserialize;
use tokio::net::TcpListener;
use tracing::{info, instrument};
use crate::dirs::Dirs;
use crate::sources::Sources;
use crate::vfs::asynch::AsyncDir;
use crate::vfs::VPath;
@ -36,17 +38,27 @@ struct Server {
target: AsyncDir,
}
#[instrument(skip(sources, target))]
pub async fn serve(sources: Arc<Sources>, target: AsyncDir, port: u16) -> anyhow::Result<()> {
#[instrument(skip(sources, dirs, target))]
pub async fn serve(
sources: Arc<Sources>,
dirs: Arc<Dirs>,
target: AsyncDir,
port: u16,
) -> anyhow::Result<()> {
let app = Router::new()
.route("/", get(index)) // needed explicitly because * does not match empty paths
.route("/*path", get(vfs_entry))
.route("/b", get(branch))
.fallback(get(four_oh_four))
.with_state(Arc::new(Server { sources, target }));
.with_state(Arc::new(Server {
sources: sources.clone(),
target,
}));
#[cfg(debug_assertions)]
let app = app.nest("/dev/live-reload", live_reload::router());
let app = app
.nest("/dev/live-reload", live_reload::router())
.nest("/dev/picture-upload", picture_upload::router(dirs));
info!("serving on port {port}");
let listener = TcpListener::bind((Ipv4Addr::from([0u8, 0, 0, 0]), port)).await?;

View file

@ -0,0 +1,84 @@
use std::sync::Arc;
use axum::{
body::Bytes,
debug_handler,
extract::{Query, State},
response::IntoResponse,
routing::post,
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::{
dirs::Dirs,
vfs::{self, Edit, EditPath, VPathBuf},
};
pub fn router<S>(dirs: Arc<Dirs>) -> Router<S> {
Router::new()
.route("/", post(picture_upload))
.with_state(dirs)
}
#[derive(Deserialize)]
struct PictureUpload {
label: String,
format: String,
compression: Compression,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
enum Compression {
Lossless,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
enum Response {
Ulid(String),
Error(String),
}
#[debug_handler]
async fn picture_upload(
State(dirs): State<Arc<Dirs>>,
Query(mut params): Query<PictureUpload>,
image: Bytes,
) -> impl IntoResponse {
let ulid = ulid::Generator::new()
.generate_with_source(&mut rand::thread_rng())
.expect("failed to generate ulid");
if params.label.is_empty() {
params.label = "untitled".into();
}
let file_name = VPathBuf::new(format!(
"{ulid}-{}.{}",
params.label,
get_extension(&params.format).unwrap_or("unknown")
));
let Some(edit_path) = vfs::query::<EditPath>(&dirs.pic, &file_name) else {
return Json(Response::Error(format!("{file_name} is not editable")));
};
let result = match params.compression {
Compression::Lossless => Edit::Write(edit_path, image.to_vec()).apply().await,
};
Json(match result {
Ok(()) => Response::Ulid(ulid.to_string()),
Err(error) => Response::Error(error.to_string()),
})
}
fn get_extension(content_type: &str) -> Option<&'static str> {
match content_type {
"image/png" => Some("png"),
"image/jpeg" => Some("jpg"),
"image/svg+xml" => Some("svg"),
"image/webp" => Some("webp"),
_ => None,
}
}

View file

@ -75,11 +75,11 @@ async fn fallible_main(
} => {
let _span = info_span!("load").entered();
let sources = Arc::new(Sources::load(&dirs).context("failed to load sources")?);
let target = generate::target(dirs, sources.clone());
let target = generate::target(dirs.clone(), sources.clone());
drop(_span);
drop(flush_guard);
serve(sources, AsyncDir::new(target), serve_args.port).await?;
serve(sources, dirs, AsyncDir::new(target), serve_args.port).await?;
}
Command::Fix(fix_args) => fix_file_cli(fix_args, &*dirs.content)?.apply().await?,

View file

@ -14,8 +14,8 @@ pub enum Edit {
/// An edit that doesn't do anything.
NoOp,
/// Write the given string to a file.
Write(EditPath, String),
/// Write the given content to a file.
Write(EditPath, Vec<u8>),
/// Execute a sequence of edits in order.
Seq(Vec<Edit>),