feat: super basic file uploads for logged in users
this is super crude and unauthorized!!! also vulnerable to AP injections so much more work has to be done
This commit is contained in:
parent
005524201d
commit
6793f0fdc9
5 changed files with 78 additions and 4 deletions
|
@ -15,6 +15,9 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compat: CompatibilityConfig,
|
pub compat: CompatibilityConfig,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub files: FileStorageConfig,
|
||||||
|
|
||||||
// TODO should i move app keys here?
|
// TODO should i move app keys here?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +118,13 @@ pub struct CompatibilityConfig {
|
||||||
pub skip_single_attachment_if_image_is_set: bool,
|
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 {
|
impl Config {
|
||||||
pub fn load(path: Option<&std::path::PathBuf>) -> Self {
|
pub fn load(path: Option<&std::path::PathBuf>) -> Self {
|
||||||
let Some(cfg_path) = path else { return Config::default() };
|
let Some(cfg_path) = path else { return Config::default() };
|
||||||
|
|
|
@ -22,7 +22,7 @@ jrd = "0.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tokio = { version = "1.40", features = ["full"] } # TODO slim this down
|
tokio = { version = "1.40", features = ["full"] } # TODO slim this down
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
axum = "0.7"
|
axum = { version = "0.7", features = ["multipart"] }
|
||||||
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||||
httpsign = { path = "../../utils/httpsign/", features = ["axum"] }
|
httpsign = { path = "../../utils/httpsign/", features = ["axum"] }
|
||||||
apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot", "jsonld"] }
|
apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot", "jsonld"] }
|
||||||
|
|
60
upub/routes/src/activitypub/file.rs
Normal file
60
upub/routes/src/activitypub/file.rs
Normal file
|
@ -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<Context>,
|
||||||
|
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<Context>,
|
||||||
|
AuthIdentity(_auth): AuthIdentity,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> crate::ApiResult<Vec<u8>> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ pub mod activity;
|
||||||
pub mod application;
|
pub mod application;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
|
pub mod file;
|
||||||
pub mod well_known;
|
pub mod well_known;
|
||||||
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router};
|
use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router};
|
||||||
|
@ -69,13 +70,13 @@ impl ActivityPubRouter for Router<upub::Context> {
|
||||||
.route("/objects/:id/replies/page", get(ap::object::replies::page))
|
.route("/objects/:id/replies/page", get(ap::object::replies::page))
|
||||||
.route("/objects/:id/context", get(ap::object::context::get))
|
.route("/objects/:id/context", get(ap::object::context::get))
|
||||||
.route("/objects/:id/context/page", get(ap::object::context::page))
|
.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", get(ap::object::likes::get))
|
||||||
//.route("/objects/:id/likes/page", get(ap::object::likes::page))
|
//.route("/objects/:id/likes/page", get(ap::object::likes::page))
|
||||||
//.route("/objects/:id/shares", get(ap::object::announces::get))
|
//.route("/objects/:id/shares", get(ap::object::announces::get))
|
||||||
//.route("/objects/:id/shares/page", get(ap::object::announces::page))
|
//.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ pub enum ApiError {
|
||||||
#[error("fetch error: {0:?}")]
|
#[error("fetch error: {0:?}")]
|
||||||
FetchError(#[from] upub::traits::fetch::RequestError),
|
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
|
// wrapper error to return arbitraty status codes
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Status(StatusCode),
|
Status(StatusCode),
|
||||||
|
|
Loading…
Reference in a new issue