diff --git a/common/Cargo.toml b/common/Cargo.toml index b72dcae6f..a35933105 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -10,6 +10,7 @@ name = "common" path = "src/lib.rs" [dependencies] +axum = { workspace = true } anyhow = { workspace = true } sea-orm = { workspace = true } thiserror = { workspace = true } diff --git a/common/src/errors.rs b/common/src/errors.rs index 82ab537ec..be3366af8 100644 --- a/common/src/errors.rs +++ b/common/src/errors.rs @@ -1,6 +1,12 @@ +use anyhow::Result; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; use thiserror::Error; -use anyhow::Result; +use crate::model::CommonResult; pub type MegaResult = Result<(), MegaError>; @@ -75,7 +81,6 @@ pub enum GitLFSError { GeneralError(String), } - #[derive(Debug, Error)] pub enum ProtocolError { #[error("{0}")] @@ -84,6 +89,36 @@ pub enum ProtocolError { Deny(String), #[error("Repository not found: {0}")] NotFound(String), + #[error("Invalid Input: {0}")] + InvalidInput(String), + #[error("HTTP Push Has Been Disabled")] + Disabled, +} + +impl IntoResponse for ProtocolError { + fn into_response(self) -> Response { + let (status, message) = match self { + ProtocolError::Deny(err) => { + // This error is caused by bad user input so don't log it + (StatusCode::UNAUTHORIZED, err) + } + ProtocolError::NotFound(err) => { + // Because `TraceLayer` wraps each request in a span that contains the request + // method, uri, etc we don't need to include those details here + // tracing::error!(%err, "error"); + + // Don't expose any details about the error to the client + (StatusCode::NOT_FOUND, err) + } + ProtocolError::InvalidInput(err) => (StatusCode::BAD_REQUEST, err), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong".to_owned(), + ), + }; + + (status, Json(CommonResult::::failed(&message))).into_response() + } } #[cfg(test)] diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs index 8fde2a8f4..8f8fbc549 100644 --- a/mono/src/api/api_router.rs +++ b/mono/src/api/api_router.rs @@ -14,7 +14,7 @@ use ceres::{ tree::{LatestCommitInfo, TreeBriefItem, TreeCommitItem}, }, }; -use common::model::CommonResult; +use common::{errors::ProtocolError, model::CommonResult}; use http::StatusCode; use taurus::event::api_request::{ApiRequestEvent, ApiType}; @@ -32,8 +32,8 @@ pub fn routers() -> Router { .route("/tree", get(get_tree_info)) .route("/blob", get(get_blob_string)) .route("/file/blob/:object_id", get(get_blob_file)) - .route("/file/tree", get(get_tree_file)); - + .route("/file/tree", get(get_tree_file)) + .route("/path-can-clone", get(path_can_be_cloned)); Router::new() .merge(router) .merge(mr_router::routers()) @@ -47,7 +47,7 @@ async fn get_blob_string( ApiRequestEvent::notify(ApiType::Blob, &state.0.context.config); let res = state .api_handler(query.path.clone().into()) - .await + .await? .get_blob_as_string(query.path.into()) .await; @@ -69,7 +69,7 @@ async fn create_file( ApiRequestEvent::notify(ApiType::CreateFile, &state.0.context.config); let res = state .api_handler(json.path.clone().into()) - .await + .await? .create_monorepo_file(json.clone()) .await; let res = match res { @@ -86,7 +86,7 @@ async fn get_latest_commit( ApiRequestEvent::notify(ApiType::LastestCommit, &state.0.context.config); let res = state .api_handler(query.path.clone().into()) - .await + .await? .get_latest_commit(query.path.into()) .await?; Ok(Json(res)) @@ -99,7 +99,7 @@ async fn get_tree_info( ApiRequestEvent::notify(ApiType::TreeInfo, &state.0.context.config); let res = state .api_handler(query.path.clone().into()) - .await + .await? .get_tree_info(query.path.into()) .await; let res = match res { @@ -112,11 +112,11 @@ async fn get_tree_info( async fn get_tree_commit_info( Query(query): Query, state: State, -) -> Result>>, ApiError> { +) -> Result>>, ProtocolError> { ApiRequestEvent::notify(ApiType::CommitInfo, &state.0.context.config); let res = state .api_handler(query.path.clone().into()) - .await + .await? .get_tree_commit_info(query.path.into()) .await; let res = match res { @@ -155,7 +155,7 @@ pub async fn get_tree_file( ) -> Result { let res = state .api_handler(query.path.clone().into()) - .await + .await? .get_tree_as_data(std::path::Path::new(&query.path)) .await; @@ -174,3 +174,11 @@ pub async fn get_tree_file( }), } } + +async fn path_can_be_cloned( + Query(query): Query, + state: State, +) -> Result>, ApiError> { + let res = state.api_handler(query.path.clone().into()).await.is_ok(); + Ok(Json(CommonResult::success(Some(res)))) +} diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index a80fb6293..cf9d5097e 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -10,7 +10,7 @@ use ceres::{ }, protocol::repo::Repo, }; -use common::model::CommonOptions; +use common::{errors::ProtocolError, model::CommonOptions}; use jupiter::{context::Context, storage::user_storage::UserStorage}; pub mod api_router; @@ -54,7 +54,7 @@ impl MonoApiServiceState { } } - async fn api_handler(&self, path: PathBuf) -> Box { + async fn api_handler(&self, path: PathBuf) -> Result, ProtocolError> { let import_dir = self.context.config.monorepo.import_dir.clone(); if path.starts_with(&import_dir) && path != import_dir { if let Some(model) = self @@ -66,14 +66,18 @@ impl MonoApiServiceState { .unwrap() { let repo: Repo = model.into(); - return Box::new(ImportApiService { + return Ok(Box::new(ImportApiService { context: self.context.clone(), repo, - }); + })); } + return Err(ProtocolError::InvalidInput(format!( + "Invalid Path{}", + path.to_str().unwrap() + ))); } - Box::new(MonoApiService { + Ok(Box::new(MonoApiService { context: self.context.clone(), - }) + })) } } diff --git a/mono/src/git_protocol/http.rs b/mono/src/git_protocol/http.rs index b8cec5029..967c2fd95 100644 --- a/mono/src/git_protocol/http.rs +++ b/mono/src/git_protocol/http.rs @@ -2,7 +2,7 @@ use std::convert::Infallible; use anyhow::Result; use axum::body::Body; -use axum::http::{HeaderValue, Request, Response, StatusCode}; +use axum::http::{HeaderValue, Request, Response}; use bytes::{Bytes, BytesMut}; use futures::{stream, TryStreamExt}; use tokio::io::AsyncReadExt; @@ -21,19 +21,19 @@ use common::model::InfoRefsParams; pub async fn git_info_refs( params: InfoRefsParams, mut pack_protocol: SmartProtocol, -) -> Result, (StatusCode, String)> { +) -> Result, ProtocolError> { let service_name = params.service.unwrap(); pack_protocol.service_type = Some(service_name.parse::().unwrap()); - let response = match pack_protocol.git_info_refs().await { - Ok(pkt_line_stream) => Response::builder() - .body(Body::from(pkt_line_stream.freeze())) - .unwrap(), - Err(err) => return Ok(handler_err(err)), - }; + let pkt_line_stream = pack_protocol.git_info_refs().await?; let content_type = format!("application/x-{}-advertisement", service_name); - let response = add_default_header(content_type, response); + let response = add_default_header( + content_type, + Response::builder() + .body(Body::from(pkt_line_stream.freeze())) + .unwrap(), + ); Ok(response) } @@ -60,7 +60,7 @@ pub async fn git_info_refs( pub async fn git_upload_pack( req: Request, mut pack_protocol: SmartProtocol, -) -> Result, (StatusCode, String)> { +) -> Result, ProtocolError> { let upload_request: BytesMut = req .into_body() .into_data_stream() @@ -71,42 +71,37 @@ pub async fn git_upload_pack( .await .unwrap(); tracing::debug!("bytes from client: {:?}", upload_request); - let response = match pack_protocol + let (mut send_pack_data, protocol_buf) = pack_protocol .git_upload_pack(&mut upload_request.freeze()) - .await - { - Ok((mut send_pack_data, protocol_buf)) => { - let body_stream = async_stream::stream! { - tracing::info!("send ack/nak message buf: {:?}", &protocol_buf); - yield Ok::<_, Infallible>(Bytes::copy_from_slice(&protocol_buf)); - // send packdata with sideband64k - while let Some(chunk) = send_pack_data.next().await { - let mut reader = chunk.as_slice(); - loop { - let mut temp = BytesMut::new(); - temp.reserve(65500); - let length = reader.read_buf(&mut temp).await.unwrap(); - if length == 0 { - break; - } - let bytes_out = pack_protocol.build_side_band_format(temp, length); - // tracing::info!("send pack file: length: {:?}", bytes_out.len()); - yield Ok::<_, Infallible>(bytes_out.freeze()); - } + .await?; + + let body_stream = async_stream::stream! { + tracing::info!("send ack/nak message buf: {:?}", &protocol_buf); + yield Ok::<_, Infallible>(Bytes::copy_from_slice(&protocol_buf)); + // send packdata with sideband64k + while let Some(chunk) = send_pack_data.next().await { + let mut reader = chunk.as_slice(); + loop { + let mut temp = BytesMut::new(); + temp.reserve(65500); + let length = reader.read_buf(&mut temp).await.unwrap(); + if length == 0 { + break; } - let bytes_out = Bytes::from_static(smart::PKT_LINE_END_MARKER); - tracing::info!("send back pkt-flush line '0000', actually: {:?}", bytes_out); - yield Ok::<_, Infallible>(bytes_out); - }; - Response::builder() - .body(Body::from_stream(body_stream)) - .unwrap() + let bytes_out = pack_protocol.build_side_band_format(temp, length); + // tracing::info!("send pack file: length: {:?}", bytes_out.len()); + yield Ok::<_, Infallible>(bytes_out.freeze()); + } } - Err(err) => return Ok(handler_err(err)), + let bytes_out = Bytes::from_static(smart::PKT_LINE_END_MARKER); + tracing::info!("send back pkt-flush line '0000', actually: {:?}", bytes_out); + yield Ok::<_, Infallible>(bytes_out); }; let response = add_default_header( String::from("application/x-git-upload-pack-result"), - response, + Response::builder() + .body(Body::from_stream(body_stream)) + .unwrap(), ); Ok(response) } @@ -129,7 +124,7 @@ pub async fn git_upload_pack( pub async fn git_receive_pack( req: Request, mut pack_protocol: SmartProtocol, -) -> Result, (StatusCode, String)> { +) -> Result, ProtocolError> { // Convert the request body into a data stream. let mut data_stream = req.into_body().into_data_stream(); let mut report_status = Bytes::new(); @@ -179,22 +174,5 @@ fn add_default_header(content_type: String, mut response: Response) -> Res response } -fn handler_err(err: ProtocolError) -> Response { - match err { - ProtocolError::NotFound(err) => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from(err.to_string())) - .unwrap(), - ProtocolError::Deny(err) => Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(Body::from(err.to_string())) - .unwrap(), - _ => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap(), - } -} - #[cfg(test)] mod tests {} diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs index 164ed0a27..cd480bd94 100644 --- a/mono/src/server/https_server.rs +++ b/mono/src/server/https_server.rs @@ -6,7 +6,7 @@ use anyhow::Result; use async_session::MemoryStore; use axum::body::Body; use axum::extract::{Query, State}; -use axum::http::{self, Request, StatusCode, Uri}; +use axum::http::{self, Request, Uri}; use axum::response::Response; use axum::routing::get; use axum::Router; @@ -21,6 +21,7 @@ use tower_http::trace::TraceLayer; use ceres::protocol::{ServiceType, SmartProtocol, TransportProtocol}; use common::config::Config; +use common::errors::ProtocolError; use common::model::{CommonOptions, InfoRefsParams}; use jupiter::context::Context; @@ -155,7 +156,7 @@ pub async fn get_method_router( state: State, Query(params): Query, uri: Uri, -) -> Result, (StatusCode, String)> { +) -> Result, ProtocolError> { if INFO_REFS_REGEX.is_match(uri.path()) { let pack_protocol = SmartProtocol::new( remove_git_suffix(uri, "/info/refs"), @@ -164,9 +165,8 @@ pub async fn get_method_router( ); crate::git_protocol::http::git_info_refs(params, pack_protocol).await } else { - Err(( - StatusCode::NOT_FOUND, - String::from("Operation not supported\n"), + Err(ProtocolError::NotFound( + "Operation not supported".to_owned(), )) } } @@ -175,7 +175,7 @@ pub async fn post_method_router( state: State, uri: Uri, req: Request, -) -> Result { +) -> Result { if REGEX_GIT_UPLOAD_PACK.is_match(uri.path()) { let mut pack_protocol = SmartProtocol::new( remove_git_suffix(uri.clone(), "/git-upload-pack"), @@ -186,10 +186,7 @@ pub async fn post_method_router( crate::git_protocol::http::git_upload_pack(req, pack_protocol).await } else if REGEX_GIT_RECEIVE_PACK.is_match(uri.path()) { if state.context.config.monorepo.disable_http_push { - return Err(( - StatusCode::FORBIDDEN, - String::from("HTTP Push Has Been Disabled"), - )) + return Err(ProtocolError::Disabled); } let mut pack_protocol = SmartProtocol::new( remove_git_suffix(uri.clone(), "/git-receive-pack"), @@ -199,10 +196,9 @@ pub async fn post_method_router( pack_protocol.service_type = Some(ServiceType::ReceivePack); crate::git_protocol::http::git_receive_pack(req, pack_protocol).await } else { - Err(( - StatusCode::NOT_FOUND, - String::from("Operation not supported"), - )) + return Err(ProtocolError::NotFound( + "Operation not supported".to_owned(), + )); } } diff --git a/moon/src/app/(dashboard)/blob/[...path]/page.tsx b/moon/src/app/(dashboard)/blob/[...path]/page.tsx index 840ffe814..814943291 100644 --- a/moon/src/app/(dashboard)/blob/[...path]/page.tsx +++ b/moon/src/app/(dashboard)/blob/[...path]/page.tsx @@ -2,15 +2,16 @@ import CodeContent from '@/components/CodeContent'; import Bread from '@/components/BreadCrumb'; import { useEffect, useState } from 'react'; +import { Flex, Layout } from "antd/lib"; export default function BlobPage({ params }: { params: { path: string[] } }) { let path = '/' + params.path.join('/'); - const [readmeContent, setReadmeContent] = useState(""); + const [fileContent, setFileContent] = useState(""); useEffect(() => { const fetchData = async () => { try { - let readmeContent = await getReadmeContent(path); - setReadmeContent(readmeContent); + let fileContent = await getFileContent(path); + setFileContent(fileContent); } catch (error) { console.error('Error fetching data:', error); } @@ -18,15 +19,47 @@ export default function BlobPage({ params }: { params: { path: string[] } }) { fetchData(); }, [path]); + const treeStyle = { + borderRadius: 8, + overflow: 'hidden', + width: 'calc(15% - 8px)', + maxWidth: 'calc(15% - 8px)', + background: '#fff', + }; + + const codeStyle = { + borderRadius: 8, + overflow: 'hidden', + width: 'calc(85% - 8px)', + background: '#fff', + }; + + const breadStyle = { + minHeight: 30, + borderRadius: 8, + overflow: 'hidden', + width: 'calc(100% - 8px)', + background: '#fff', + }; + return (
- - + + + + + + {/* */} + + + + +
) } -async function getReadmeContent(pathname: string) { +async function getFileContent(pathname: string) { const res = await fetch(`/api/blob?path=${pathname}`); const response = await res.json(); const directory = response.data.data; diff --git a/moon/src/components/CodeContent.module.css b/moon/src/components/CodeContent.module.css index 7762baab6..9469cdf76 100644 --- a/moon/src/components/CodeContent.module.css +++ b/moon/src/components/CodeContent.module.css @@ -1,7 +1,3 @@ -.codeShowContainer { - border-radius: 1rem; - padding-top: 70px; -} .codeLineNumber { margin-left: 15px; @@ -12,7 +8,6 @@ background-color: rgba(53, 53, 53, 0.103); display: flex; position: absolute; - width: 80%; height: 50px; border-top-left-radius: 1rem; border-top-right-radius: 1rem; @@ -33,8 +28,6 @@ } .fileCodeContainer { - width: 80%; - margin-left: 18%; border-radius: 0.5rem; margin-top: 10px; diff --git a/moon/src/components/CodeContent.tsx b/moon/src/components/CodeContent.tsx index d3cc85a07..81b7fe98e 100644 --- a/moon/src/components/CodeContent.tsx +++ b/moon/src/components/CodeContent.tsx @@ -1,13 +1,20 @@ import Editor from './editor/Editor' import 'github-markdown-css/github-markdown-light.css' import { Highlight, themes } from "prism-react-renderer" -import { useState } from 'react' +import { useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' import styles from './CodeContent.module.css' const CodeContent = ({ fileContent }) => { const [showEditor, setShowEditor] = useState(false); + const [lfs, setLfs] = useState(false); + + useEffect(() => { + if (isLfsContent(fileContent)) { + setLfs(true); + } + }, [fileContent]) const handleLineNumberClick = (lineIndex) => { setShowEditor(!showEditor); @@ -33,9 +40,28 @@ const CodeContent = ({ fileContent }) => { }; + function isLfsContent(content: string): boolean { + const lines = content.split('\n'); + let foundVersion = false; + let foundOid = false; + let foundSize = false; + for (const line of lines) { + if (line.startsWith('version ')) { + foundVersion = true; + } else if (line.startsWith('oid sha256:')) { + foundOid = true; + } else if (line.startsWith('size ')) { + foundSize = true; + } + if (foundVersion && foundOid && foundSize) { + return true; + } + } + return false; + } return ( -
+
- {i + 1} - {line.map((token, key) => ( - - ))} -
- ))} +
+                        {
+                            !lfs &&
+                            tokens.map((line, i) => (
+                                
+ + {i + 1} + {line.map((token, key) => ( + + ))} +
+ )) + } + { + lfs && (Sorry about that, but we can’t show files that are this big right now.) + }
)}