diff --git a/radicle-httpd/src/raw.rs b/radicle-httpd/src/raw.rs index 3aa2dad6..cce1f337 100644 --- a/radicle-httpd/src/raw.rs +++ b/radicle-httpd/src/raw.rs @@ -7,6 +7,7 @@ use axum::response::IntoResponse; use axum::routing::get; use axum::Router; use hyper::HeaderMap; +use radicle_surf::blob::{Blob, BlobRef}; use tower_http::cors; use radicle::prelude::RepoId; @@ -94,8 +95,9 @@ static MIMES: &[(&str, &str)] = &[ pub fn router(profile: Arc) -> Router { Router::new() - .route("/:project/:sha/*path", get(file_by_path_handler)) - .route("/:project/blobs/:oid", get(file_by_oid_handler)) + .route("/:rid/:sha/*path", get(file_by_commit_handler)) + .route("/:rid/head/*path", get(file_by_canonical_head_handler)) + .route("/:rid/blobs/:oid", get(file_by_oid_handler)) .with_state(profile) .layer( cors::CorsLayer::new() @@ -106,7 +108,7 @@ pub fn router(profile: Arc) -> Router { ) } -async fn file_by_path_handler( +async fn file_by_commit_handler( Path((rid, sha, path)): Path<(RepoId, Oid, String)>, State(profile): State>, ) -> impl IntoResponse { @@ -118,10 +120,36 @@ async fn file_by_path_handler( return Err(Error::NotFound); } - let mut response_headers = HeaderMap::new(); let repo: Repository = repo.backend.into(); let blob = repo.blob(sha, &path)?; + blob_response(blob, path) +} + +async fn file_by_canonical_head_handler( + Path((rid, path)): Path<(RepoId, String)>, + State(profile): State>, +) -> impl IntoResponse { + let storage = &profile.storage; + let repo = storage.repository(rid)?; + + // Don't allow downloading raw files for private repos. + if repo.identity_doc()?.visibility.is_private() { + return Err(Error::NotFound); + } + + let (_, sha) = repo.head()?; + let repo: Repository = repo.backend.into(); + let blob = repo.blob(sha, &path)?; + + blob_response(blob, path) +} + +fn blob_response( + blob: Blob, + path: String, +) -> Result<(StatusCode, HeaderMap, Vec), Error> { + let mut response_headers = HeaderMap::new(); if blob.size() > MAX_BLOB_SIZE { return Ok::<_, Error>((StatusCode::PAYLOAD_TOO_LARGE, response_headers, vec![])); } @@ -171,8 +199,8 @@ async fn file_by_oid_handler( mod routes { use axum::http::StatusCode; - use crate::test::{self, get, HEAD, RID, RID_PRIVATE}; - use radicle::storage::{ReadRepository, ReadStorage}; + use crate::test::{self, get, RID, RID_PRIVATE}; + use radicle::storage::ReadStorage; #[tokio::test] async fn test_file_handler() { @@ -180,20 +208,18 @@ mod routes { let ctx = test::seed(tmp.path()); let app = super::router(ctx.profile().to_owned()); - let response = get(&app, format!("/{RID}/{HEAD}/dir1/README")).await; + let response = get(&app, format!("/{RID}/head/dir1/README")).await; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().await, "Hello World from dir1!\n"); // Make sure the repo exists in storage. - let repo = ctx - .profile() + ctx.profile() .storage .repository(RID_PRIVATE.parse().unwrap()) .unwrap(); - let (_, head) = repo.head().unwrap(); - let response = get(&app, format!("/{RID_PRIVATE}/{head}/README")).await; + let response = get(&app, format!("/{RID_PRIVATE}/head/README")).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } }