diff --git a/upub/core/src/config.rs b/upub/core/src/config.rs index cfd3522..3ead8a4 100644 --- a/upub/core/src/config.rs +++ b/upub/core/src/config.rs @@ -15,6 +15,9 @@ pub struct Config { #[serde(default)] pub compat: CompatibilityConfig, + #[serde(default)] + pub files: FileStorageConfig, + // TODO should i move app keys here? } @@ -115,6 +118,13 @@ pub struct CompatibilityConfig { pub skip_single_attachment_if_image_is_set: bool, } +#[serde_inline_default::serde_inline_default] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] +pub struct FileStorageConfig { + #[serde_inline_default("files/".to_string())] + pub path: String, +} + impl Config { pub fn load(path: Option<&std::path::PathBuf>) -> Self { let Some(cfg_path) = path else { return Config::default() }; diff --git a/upub/routes/Cargo.toml b/upub/routes/Cargo.toml index c958859..2d41d5c 100644 --- a/upub/routes/Cargo.toml +++ b/upub/routes/Cargo.toml @@ -22,7 +22,7 @@ jrd = "0.1" tracing = "0.1" tokio = { version = "1.40", features = ["full"] } # TODO slim this down reqwest = { version = "0.12", features = ["json"] } -axum = "0.7" +axum = { version = "0.7", features = ["multipart"] } tower-http = { version = "0.5", features = ["cors", "trace"] } httpsign = { path = "../../utils/httpsign/", features = ["axum"] } apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot", "jsonld"] } diff --git a/upub/routes/src/activitypub/file.rs b/upub/routes/src/activitypub/file.rs new file mode 100644 index 0000000..ecb4bda --- /dev/null +++ b/upub/routes/src/activitypub/file.rs @@ -0,0 +1,60 @@ +use axum::extract::{Multipart, Path, State}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use upub::Context; + +use crate::AuthIdentity; + +pub async fn upload( + State(ctx): State, + AuthIdentity(auth): AuthIdentity, + mut multipart: Multipart, +) -> crate::ApiResult<()> { + if !auth.is_local() { + return Err(crate::ApiError::forbidden()); + } + + let mut uploaded_something = false; + while let Some(field) = multipart.next_field().await.unwrap() { + let _ = if let Some(filename) = field.file_name() { + filename.to_string() + } else { + tracing::warn!("skipping multipart field {field:?}"); + continue; + }; + + let data = match field.bytes().await { + Ok(x) => x, + Err(e) => { + tracing::error!("error reading multipart part: {e:?}"); + continue; + }, + }; + + let name = sha256::digest(data.as_ref()); + let path = format!("{}{name}", ctx.cfg().files.path); + + tokio::fs::File::open(path).await?.write_all(&data).await?; + uploaded_something = true; + } + + if uploaded_something { + Ok(()) + } else { + Err(crate::ApiError::bad_request()) + } +} + +pub async fn download( + State(ctx): State, + AuthIdentity(_auth): AuthIdentity, + Path(id): Path, +) -> crate::ApiResult> { + let path = format!("{}{id}", ctx.cfg().files.path); + let mut buffer = Vec::new(); + tokio::fs::File::open(path) + .await? + .read_to_end(&mut buffer) + .await?; + + Ok(buffer) +} diff --git a/upub/routes/src/activitypub/mod.rs b/upub/routes/src/activitypub/mod.rs index 1aba26e..e649a1a 100644 --- a/upub/routes/src/activitypub/mod.rs +++ b/upub/routes/src/activitypub/mod.rs @@ -6,6 +6,7 @@ pub mod activity; pub mod application; pub mod auth; pub mod tags; +pub mod file; pub mod well_known; use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router}; @@ -69,13 +70,13 @@ impl ActivityPubRouter for Router { .route("/objects/:id/replies/page", get(ap::object::replies::page)) .route("/objects/:id/context", get(ap::object::context::get)) .route("/objects/:id/context/page", get(ap::object::context::page)) + // file routes + .route("/file", post(ap::file::upload)) + .route("/file/:id", get(ap::file::download)) //.route("/objects/:id/likes", get(ap::object::likes::get)) //.route("/objects/:id/likes/page", get(ap::object::likes::page)) //.route("/objects/:id/shares", get(ap::object::announces::get)) //.route("/objects/:id/shares/page", get(ap::object::announces::page)) - // hashtags routes - //.route("/hashtags/:name", get(ap::hashtags::get)) - //.route("/hashtags/:name/page", get(ap::hashtags::page)) } } diff --git a/upub/routes/src/error.rs b/upub/routes/src/error.rs index 9b1f222..885ff08 100644 --- a/upub/routes/src/error.rs +++ b/upub/routes/src/error.rs @@ -19,6 +19,9 @@ pub enum ApiError { #[error("fetch error: {0:?}")] FetchError(#[from] upub::traits::fetch::RequestError), + #[error("error interacting with file system: {0:?}")] + IO(#[from] std::io::Error), + // wrapper error to return arbitraty status codes #[error("{0}")] Status(StatusCode),