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