httpd: Add routes for handling embeds in issues
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
parent
a3f460e67d
commit
9df1922f15
|
|
@ -316,9 +316,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
|||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.2"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
|
|
@ -1928,6 +1928,7 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-auth",
|
||||
"axum-server",
|
||||
"base64 0.21.3",
|
||||
"chrono",
|
||||
"fastrand",
|
||||
"flate2",
|
||||
|
|
@ -2981,7 +2982,7 @@ version = "2.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"base64 0.21.3",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ impl Embed<Vec<u8>> {
|
|||
.into()
|
||||
}
|
||||
|
||||
/// Return am embed where the content is replaced by a content hash.
|
||||
/// Return an embed where the content is replaced by a content hash.
|
||||
pub fn hashed<T: From<Oid>>(&self) -> Embed<T> {
|
||||
Embed {
|
||||
name: self.name.clone(),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ anyhow = { version = "1" }
|
|||
axum = { version = "0.6.7", default-features = false, features = ["headers", "json", "query", "tokio"] }
|
||||
axum-auth = { version= "0.4.0", default-features = false, features = ["auth-bearer"] }
|
||||
axum-server = { version = "0.5.1", default-features = false }
|
||||
base64 = "0.21.3"
|
||||
chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
|
||||
fastrand = { version = "2.0.0" }
|
||||
flate2 = { version = "1" }
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ pub struct PaginationQuery {
|
|||
pub per_page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawQuery {
|
||||
pub mime: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CobsQuery<T> {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use radicle::cob::patch::Review;
|
|||
use radicle::cob::patch::{Patch, PatchId};
|
||||
use radicle::cob::thread;
|
||||
use radicle::cob::thread::CommentId;
|
||||
use radicle::cob::{ActorId, Author, Reaction, Timestamp};
|
||||
use radicle::cob::{ActorId, Author, Embed, Reaction, Timestamp, Uri};
|
||||
use radicle::git::RefString;
|
||||
use radicle::node::{Alias, AliasStore};
|
||||
use radicle::prelude::NodeId;
|
||||
|
|
@ -241,6 +241,7 @@ struct Comment<'a> {
|
|||
id: CommentId,
|
||||
author: Value,
|
||||
body: &'a str,
|
||||
embeds: Vec<Embed<Uri>>,
|
||||
reactions: Vec<(&'a ActorId, &'a Reaction)>,
|
||||
#[serde(with = "radicle::serde_ext::localtime::time")]
|
||||
timestamp: Timestamp,
|
||||
|
|
@ -254,6 +255,7 @@ impl<'a> Comment<'a> {
|
|||
id: *id,
|
||||
author: author(&comment_author, aliases.alias(comment_author.id())),
|
||||
body: comment.body(),
|
||||
embeds: comment.embeds().to_vec(),
|
||||
reactions: comment.reactions().collect::<Vec<_>>(),
|
||||
timestamp: comment.timestamp(),
|
||||
reply_to: comment.reply_to(),
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ use axum::response::IntoResponse;
|
|||
use axum::routing::{get, patch, post};
|
||||
use axum::{Json, Router};
|
||||
use axum_auth::AuthBearer;
|
||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||
use hyper::StatusCode;
|
||||
use radicle_surf::blob::{Blob, BlobRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
|
||||
use radicle::cob::{issue, patch, Label};
|
||||
use radicle::cob::{issue, patch, Embed, Label, Uri};
|
||||
use radicle::identity::{Did, Id};
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::node::AliasStore;
|
||||
|
|
@ -511,6 +512,7 @@ pub struct IssueCreate {
|
|||
pub description: String,
|
||||
pub labels: Vec<Label>,
|
||||
pub assignees: Vec<Did>,
|
||||
pub embeds: Vec<Embed<Uri>>,
|
||||
}
|
||||
|
||||
/// Create a new issue.
|
||||
|
|
@ -528,6 +530,25 @@ async fn issue_create_handler(
|
|||
.signer()
|
||||
.map_err(|_| Error::Auth("Unauthorized"))?;
|
||||
let repo = storage.repository(project)?;
|
||||
let embeds: Vec<Embed> = issue
|
||||
.embeds
|
||||
.into_iter()
|
||||
.filter_map(|embed| {
|
||||
if let Some(content) = embed
|
||||
.content
|
||||
.as_str()
|
||||
.strip_prefix("data:content/type;base64,")
|
||||
{
|
||||
return BASE64_STANDARD.decode(content).ok().map(|content| Embed {
|
||||
name: embed.name,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut issues = issue::Issues::open(&repo)?;
|
||||
let issue = issues
|
||||
.create(
|
||||
|
|
@ -535,7 +556,7 @@ async fn issue_create_handler(
|
|||
issue.description,
|
||||
&issue.labels,
|
||||
&issue.assignees,
|
||||
[],
|
||||
embeds,
|
||||
&signer,
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
|
|
@ -575,9 +596,30 @@ async fn issue_update_handler(
|
|||
issue::Action::Edit { title } => {
|
||||
issue.edit(title, &signer)?;
|
||||
}
|
||||
issue::Action::Comment { body, reply_to, .. } => {
|
||||
issue::Action::Comment {
|
||||
body,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
let embeds: Vec<Embed> = embeds
|
||||
.into_iter()
|
||||
.filter_map(|embed| {
|
||||
if let Some(content) = embed
|
||||
.content
|
||||
.as_str()
|
||||
.strip_prefix("data:content/type;base64,")
|
||||
{
|
||||
return BASE64_STANDARD.decode(content).ok().map(|content| Embed {
|
||||
name: embed.name,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
if let Some(to) = reply_to {
|
||||
issue.comment(body, to, [], &signer)?;
|
||||
issue.comment(body, to, embeds, &signer)?;
|
||||
} else {
|
||||
return Err(Error::BadRequest("`replyTo` missing".to_owned()));
|
||||
}
|
||||
|
|
@ -1777,6 +1819,7 @@ mod routes {
|
|||
"id": DID
|
||||
},
|
||||
"body": "Change 'hello world' to 'hello everyone'",
|
||||
"embeds": [],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null
|
||||
|
|
@ -1790,7 +1833,7 @@ mod routes {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_projects_issues_create() {
|
||||
const CREATED_ISSUE_ID: &str = "c7cff5ab610408470406e023baa1ab087ce78adc";
|
||||
const CREATED_ISSUE_ID: &str = "e712eb0b5874d5256022fb620f26caf847d96723";
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = contributor(tmp.path());
|
||||
|
|
@ -1802,6 +1845,12 @@ mod routes {
|
|||
"title": "Issue #2",
|
||||
"description": "Change 'hello world' to 'hello everyone'",
|
||||
"labels": ["bug"],
|
||||
"embeds": [
|
||||
{
|
||||
"name": "example.html",
|
||||
"content": "data:content/type;base64,PGh0bWw+SGVsbG8gV29ybGQhPC9odG1sPg=="
|
||||
}
|
||||
],
|
||||
"assignees": [],
|
||||
}))
|
||||
.unwrap();
|
||||
|
|
@ -1833,17 +1882,23 @@ mod routes {
|
|||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"assignees": [],
|
||||
"title": "Issue #2",
|
||||
"state": {
|
||||
"status": "open",
|
||||
},
|
||||
"assignees": [],
|
||||
"discussion": [{
|
||||
"id": CREATED_ISSUE_ID,
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "Change 'hello world' to 'hello everyone'",
|
||||
"embeds": [
|
||||
{
|
||||
"name": "example.html",
|
||||
"content": "git:b62df2ec90365e3749cd4fa431cb844492908b84"
|
||||
}
|
||||
],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null,
|
||||
|
|
@ -1866,6 +1921,12 @@ mod routes {
|
|||
let body = serde_json::to_vec(&json!({
|
||||
"type": "comment",
|
||||
"body": "This is first-level comment",
|
||||
"embeds": [
|
||||
{
|
||||
"name": "image.jpg",
|
||||
"content": "data:content/type;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC"
|
||||
}
|
||||
],
|
||||
"replyTo": CONTRIBUTOR_ISSUE_ID,
|
||||
}))
|
||||
.unwrap();
|
||||
|
|
@ -1883,7 +1944,7 @@ mod routes {
|
|||
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"type": "comment.react",
|
||||
"id": "26cadcc7cb51ee9c56b6232023e9bf63b7b0df60",
|
||||
"id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863",
|
||||
"reaction": "🚀",
|
||||
"active": true,
|
||||
}))
|
||||
|
|
@ -1921,16 +1982,23 @@ mod routes {
|
|||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "Change 'hello world' to 'hello everyone'",
|
||||
"embeds": [],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null,
|
||||
},
|
||||
{
|
||||
"id": "26cadcc7cb51ee9c56b6232023e9bf63b7b0df60",
|
||||
"id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863",
|
||||
"author": {
|
||||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "This is first-level comment",
|
||||
"embeds": [
|
||||
{
|
||||
"name": "image.jpg",
|
||||
"content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
|
||||
},
|
||||
],
|
||||
"reactions": [
|
||||
[
|
||||
"z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8",
|
||||
|
|
@ -1998,6 +2066,7 @@ mod routes {
|
|||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "Change 'hello world' to 'hello everyone'",
|
||||
"embeds": [],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null,
|
||||
|
|
@ -2008,6 +2077,7 @@ mod routes {
|
|||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "This is a reply to the first comment",
|
||||
"embeds": [],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": ISSUE_DISCUSSION_ID,
|
||||
|
|
@ -2498,6 +2568,7 @@ mod routes {
|
|||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "EDIT: This is a root level comment",
|
||||
"embeds": [],
|
||||
"reactions": [["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8","🚀"]],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": null,
|
||||
|
|
@ -2508,6 +2579,7 @@ mod routes {
|
|||
"id": CONTRIBUTOR_DID,
|
||||
},
|
||||
"body": "This is a root level comment",
|
||||
"embeds": [],
|
||||
"reactions": [],
|
||||
"timestamp": TIMESTAMP,
|
||||
"replyTo": CONTRIBUTOR_COMMENT_1,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,18 @@ pub enum RawError {
|
|||
#[error(transparent)]
|
||||
Surf(#[from] radicle_surf::Error),
|
||||
|
||||
/// Git error.
|
||||
#[error(transparent)]
|
||||
Git(#[from] radicle::git::ext::Error),
|
||||
|
||||
/// Radicle Storage error.
|
||||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
|
||||
/// Http Headers error.
|
||||
#[error(transparent)]
|
||||
Headers(#[from] http::header::InvalidHeaderValue),
|
||||
|
||||
/// Surf file error.
|
||||
#[error(transparent)]
|
||||
SurfFile(#[from] radicle_surf::fs::error::File),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::http::{header, Method, StatusCode};
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::{header, HeaderValue, Method, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
|
|
@ -11,9 +11,10 @@ use tower_http::cors;
|
|||
|
||||
use radicle::prelude::Id;
|
||||
use radicle::profile::Profile;
|
||||
use radicle::storage::git::paths;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
use radicle_surf::{Oid, Repository};
|
||||
|
||||
use crate::api::RawQuery;
|
||||
use crate::axum_extra::Path;
|
||||
use crate::error::RawError as Error;
|
||||
|
||||
|
|
@ -93,7 +94,8 @@ static MIMES: &[(&str, &str)] = &[
|
|||
|
||||
pub fn router(profile: Arc<Profile>) -> Router {
|
||||
Router::new()
|
||||
.route("/:project/:sha/*path", get(file_handler))
|
||||
.route("/:project/:sha/*path", get(file_by_path_handler))
|
||||
.route("/:project/blobs/:oid", get(file_by_oid_handler))
|
||||
.with_state(profile)
|
||||
.layer(
|
||||
cors::CorsLayer::new()
|
||||
|
|
@ -104,34 +106,55 @@ pub fn router(profile: Arc<Profile>) -> Router {
|
|||
)
|
||||
}
|
||||
|
||||
async fn file_handler(
|
||||
async fn file_by_path_handler(
|
||||
Path((project, sha, path)): Path<(Id, Oid, String)>,
|
||||
State(profile): State<Arc<Profile>>,
|
||||
) -> impl IntoResponse {
|
||||
let storage = &profile.storage;
|
||||
let repo = Repository::open(paths::repository(storage, &project))?;
|
||||
let repo = storage.repository(project)?;
|
||||
let mut response_headers = HeaderMap::new();
|
||||
let repo: Repository = repo.backend.into();
|
||||
let blob = repo.blob(sha, &path)?;
|
||||
|
||||
if repo.file(sha, &path)?.content(&repo)?.size() > MAX_BLOB_SIZE {
|
||||
if blob.size() > MAX_BLOB_SIZE {
|
||||
return Ok::<_, Error>((StatusCode::PAYLOAD_TOO_LARGE, response_headers, vec![]));
|
||||
}
|
||||
|
||||
let blob = repo.blob(sha, &path)?;
|
||||
let mime = {
|
||||
if let Some(ext) = path.split('.').last() {
|
||||
let mime = if let Some(ext) = path.split('.').last() {
|
||||
MIMES
|
||||
.binary_search_by(|(k, _)| k.cmp(&ext))
|
||||
.map(|k| MIMES[k].1)
|
||||
.unwrap_or("text; charset=utf-8")
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
}
|
||||
};
|
||||
response_headers.insert(header::CONTENT_TYPE, mime.parse().unwrap());
|
||||
response_headers.insert(header::CONTENT_TYPE, HeaderValue::from_str(mime)?);
|
||||
|
||||
Ok::<_, Error>((StatusCode::OK, response_headers, blob.content().to_owned()))
|
||||
}
|
||||
|
||||
async fn file_by_oid_handler(
|
||||
Path((project, oid)): Path<(Id, Oid)>,
|
||||
State(profile): State<Arc<Profile>>,
|
||||
Query(qs): Query<RawQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let storage = &profile.storage;
|
||||
let repo = storage.repository(project)?;
|
||||
let blob = repo.blob(oid)?;
|
||||
let mut response_headers = HeaderMap::new();
|
||||
|
||||
if blob.size() > MAX_BLOB_SIZE {
|
||||
return Ok::<_, Error>((StatusCode::PAYLOAD_TOO_LARGE, response_headers, vec![]));
|
||||
}
|
||||
|
||||
response_headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(&qs.mime.unwrap_or("application/octet-stream".to_string()))?,
|
||||
);
|
||||
|
||||
Ok::<_, Error>((StatusCode::OK, response_headers, blob.content().to_vec()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod routes {
|
||||
use axum::http::StatusCode;
|
||||
|
|
|
|||
Loading…
Reference in New Issue