refactor!: setup file proxy for projects

This commit is contained in:
2026-01-20 19:10:01 -08:00
parent 5eccfe32da
commit a2afc3fa05
17 changed files with 279 additions and 103 deletions

17
api/Cargo.lock generated
View File

@@ -248,6 +248,7 @@ dependencies = [
"aws-sdk-dynamodb",
"chrono",
"jsonwebtoken",
"mime_guess",
"reqwest 0.13.1",
"sentry",
"serde",
@@ -2114,6 +2115,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -4104,6 +4115,12 @@ dependencies = [
"libc",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-bidi"
version = "0.3.18"

View File

@@ -9,6 +9,7 @@ aws-config = "1.8.12"
aws-sdk-dynamodb = "1.102.0"
chrono = { version = "0.4.43", features = ["serde"] }
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
mime_guess = "2.0.5"
reqwest = { version = "0.13.1", features = ["json", "query"] }
sentry = { version = "0.46.1", features = ["actix", "tracing"] }
serde = "1.0.228"

View File

@@ -1,4 +1,4 @@
use std::{env, sync::Arc};
use std::env;
use crate::error::Result;
use jsonwebtoken::{

View File

@@ -6,9 +6,11 @@ use crate::AppState;
pub async fn add_repo(
app_state: web::Data<AppState>,
user: web::ReqData<User>,
payload: web::Json<RepositorySchema>,
repo: web::Json<RepositorySchema>,
) -> Result<HttpResponse> {
let repo = payload.into_inner();
validate_repo(app_state.clone(), &repo).await?;
app_state.user.add_repository(&user.id, repo).await
validate_repo(app_state.clone(), &repo, &user.id).await?;
app_state
.user
.add_repository(&user.id, repo.into_inner())
.await
}

View File

@@ -24,9 +24,8 @@ pub struct Repository {
pub async fn get_repos(
app_state: web::Data<AppState>,
req: web::ReqData<User>,
user: web::ReqData<User>,
) -> Result<HttpResponse> {
let user = req.into_inner();
let token = app_state.user.get_access_token(&user.id).await?;
let response = app_state
@@ -38,7 +37,7 @@ pub async fn get_repos(
response.error_for_status_ref()?;
let added_ids = app_state
.user
.get_repositories(&user.id)
.get_repositories_user(&user.id)
.await?
.into_iter()
.map(|r| r.id)
@@ -47,11 +46,9 @@ pub async fn get_repos(
.json::<Vec<Repository>>()
.await?
.into_iter()
.filter_map(|mut r| {
(!r.private).then(|| {
r.added = added_ids.contains(&r.id.to_string());
r
})
.map(|mut r| {
r.added = added_ids.contains(&r.id.to_string());
r
})
.collect::<Vec<Repository>>();

View File

@@ -1,4 +1,5 @@
pub mod add_repo;
pub mod get_repos;
pub mod global_repos;
pub mod proxy_file;
pub mod search_repos;

View File

@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use crate::{
AppState,
error::{Error, Result},
};
#[derive(serde::Deserialize)]
pub struct Params {
repo_id: String,
file: String,
}
pub async fn proxy_file(
app_state: web::Data<AppState>,
path: web::Path<Params>,
) -> Result<HttpResponse> {
let repo = app_state
.user
.get_approved_repository(&path.repo_id)
.await?
.ok_or(Error::NotFound)?;
let token = app_state.user.get_access_token(&repo.owner_id).await?;
let url = format!(
"https://raw.githubusercontent.com/{}/HEAD/dist/{}",
repo.full_name, path.file
);
let response = app_state
.reqwest_client
.get(&url)
.bearer_auth(token)
.send()
.await?;
response.error_for_status_ref()?;
let bytes = response.bytes().await?;
let mime = mime_guess::from_path(&path.file)
.first_or_octet_stream()
.to_string();
Ok(HttpResponse::Ok()
.content_type(mime)
.insert_header(("Cache-Control", "public, max-age=3600"))
.body(bytes))
}

View File

@@ -1,6 +1,6 @@
use crate::{auth::User, endpoints::get_repos::Repository, error::Result};
use actix_web::{
HttpRequest, HttpResponse,
HttpResponse,
web::{self, ReqData},
};
use serde::{Deserialize, Serialize};
@@ -20,9 +20,8 @@ struct SearchResponse {
pub async fn search_repos(
app_state: web::Data<AppState>,
query: web::Query<SearchQuery>,
req: ReqData<User>,
user: ReqData<User>,
) -> Result<HttpResponse> {
let user = req.into_inner();
let token = app_state.user.get_access_token(&user.id).await?;
let search_query = format!("user:{} {} fork:true", user.name, query.q);

View File

@@ -1,4 +1,4 @@
use actix_web::{HttpResponse, ResponseError, http::StatusCode};
use actix_web::{HttpResponse, ResponseError};
use aws_sdk_dynamodb::error::SdkError;
use serde::Serialize;
use thiserror::Error;
@@ -25,6 +25,8 @@ pub enum Error {
AlreadyExists,
#[error("validation failed: {0}")]
ValidationFailed(String),
#[error("not found")]
NotFound,
}
impl<E: std::fmt::Debug> From<SdkError<E>> for Error {
@@ -49,9 +51,10 @@ impl ResponseError for Error {
Error::AlreadyExists => HttpResponse::BadRequest().json(ErrorResponse {
error: "item already exists".to_string(),
}),
Error::ValidationFailed(msg) => HttpResponse::BadRequest().json(ErrorResponse {
error: msg.clone(),
}),
Error::ValidationFailed(msg) => {
HttpResponse::BadRequest().json(ErrorResponse { error: msg.clone() })
}
Error::NotFound => HttpResponse::NotFound().finish(),
_ => HttpResponse::InternalServerError().finish(),
}
}

View File

@@ -89,6 +89,10 @@ async fn run() -> std::io::Result<()> {
.route(
"/repos",
web::get().to(endpoints::global_repos::global_repos),
)
.route(
"/repos/{repo_id}/files/{file:.*}",
web::get().to(endpoints::proxy_file::proxy_file),
),
),
)

View File

@@ -18,6 +18,16 @@ pub struct UserRepository {
pub struct RepositorySchema {
pub id: String,
pub full_name: String,
#[serde(skip)]
pub owner_id: String,
pub description: String,
}
#[derive(Serialize)]
pub struct GlobalRepositoriesResponse {
pub id: String,
pub full_name: String,
pub description: String,
}
impl UserRepository {
@@ -38,7 +48,7 @@ impl UserRepository {
.map_err(|_| crate::error::Error::AccessToken)
}
pub async fn get_repositories(&self, user_id: &str) -> Result<Vec<RepositorySchema>> {
pub async fn get_repositories_user(&self, user_id: &str) -> Result<Vec<RepositorySchema>> {
let response = self
.dynamodb_client
.query()
@@ -59,6 +69,8 @@ impl UserRepository {
.strip_prefix("REPO#")?
.to_string(),
full_name: item.get("full_name")?.as_s().ok()?.to_string(),
owner_id: item.get("owner_id")?.as_s().ok()?.to_string(),
description: item.get("description")?.as_s().ok()?.to_string(),
})
})
.collect::<Vec<RepositorySchema>>();
@@ -70,7 +82,9 @@ impl UserRepository {
let response = self
.dynamodb_client
.query()
.key_condition_expression("pk = :pk")
.table_name(&self.table_name)
.index_name("gsi1")
.key_condition_expression("gsi1pk = :pk")
.expression_attribute_values(":pk", AttributeValue::S("REPOS".into()))
.send()
.await?
@@ -80,13 +94,53 @@ impl UserRepository {
if (*item.get("approved")?.as_bool().ok()?) == false {
return None;
};
Some(item.get("full_name")?.as_s().ok()?.to_string())
Some(GlobalRepositoriesResponse {
id: item
.get("sk")?
.as_s()
.ok()?
.strip_prefix("REPO#")?
.to_string(),
full_name: item.get("full_name")?.as_s().ok()?.to_string(),
description: item.get("description")?.as_s().ok()?.to_string(),
})
})
.collect::<Vec<String>>();
.collect::<Vec<GlobalRepositoriesResponse>>();
Ok(HttpResponse::Ok().json(response))
}
pub async fn get_approved_repository(&self, repo_id: &str) -> Result<Option<RepositorySchema>> {
let response = self
.dynamodb_client
.query()
.table_name(&self.table_name)
.index_name("gsi1")
.key_condition_expression("gsi1pk = :pk")
.filter_expression("sk = :sk AND approved = :approved")
.expression_attribute_values(":pk", AttributeValue::S("REPOS".into()))
.expression_attribute_values(":sk", AttributeValue::S(format!("REPO#{repo_id}")))
.expression_attribute_values(":approved", AttributeValue::Bool(true))
.send()
.await?;
let repo = response.items().first().and_then(|item| {
Some(RepositorySchema {
id: item
.get("sk")?
.as_s()
.ok()?
.strip_prefix("REPO#")?
.to_string(),
full_name: item.get("full_name")?.as_s().ok()?.to_string(),
owner_id: item.get("owner_id")?.as_s().ok()?.to_string(),
description: item.get("description")?.as_s().ok()?.to_string(),
})
});
Ok(repo)
}
pub async fn add_repository(
&self,
user_id: &str,
@@ -105,6 +159,8 @@ impl UserRepository {
.item("gsi1sk", AttributeValue::S(now.clone()))
.item("imported_at", AttributeValue::S(now))
.item("approved", AttributeValue::Bool(false))
.item("owner_id", AttributeValue::S(user_id.into()))
.item("description", AttributeValue::S(repo.description.clone()))
.send()
.await;

View File

@@ -4,13 +4,19 @@ use crate::AppState;
use crate::error::{Error, Result};
use crate::user::RepositorySchema;
pub async fn validate_repo(app_state: web::Data<AppState>, repo: &RepositorySchema) -> Result<()> {
pub async fn validate_repo(
app_state: web::Data<AppState>,
repo: &RepositorySchema,
user_id: &str,
) -> Result<()> {
let token = app_state.user.get_access_token(&user_id).await?;
let response = app_state
.reqwest_client
.get(format!(
"https://raw.githubusercontent.com/{}/HEAD/dist/index.html",
repo.full_name
))
.bearer_auth(token)
.send()
.await?;