Nothing to see here

feat(knot): set cache-control headers for immutable responses

Signed-off-by: tjh <did:plc:65gha4t3avpfpzmvpbwovss7>

tjh.dev 4cd451f1 a42d4aa6

verified
Changed files
+105 -38
crates
knot
src
model
public
xrpc
sh
tangled
+103 -36
crates/knot/src/model/repository.rs
··· 7 7 public::xrpc::XrpcError, 8 8 types::sh::tangled::repo::{blob, branches, diff, log, tags}, 9 9 }; 10 + use axum::{ 11 + http::{HeaderMap, HeaderValue}, 12 + response::IntoResponse, 13 + }; 10 14 use gix::{ThreadSafeRepository, bstr::ByteSlice as _}; 11 15 use lexicon::sh::tangled::repo::{get_default_branch, refs, tree}; 16 + use serde::Serialize; 12 17 use std::cmp::Reverse; 13 18 use tokio_rayon::AsyncThreadPool as _; 14 19 ··· 40 35 fn blob( 41 36 &self, 42 37 params: blob::Input, 43 - ) -> impl Future<Output = Result<blob::Output, XrpcError>> + Send; 38 + ) -> impl Future<Output = Result<DirectResponse<blob::Output>, XrpcError>> + Send; 44 39 45 40 fn branches( 46 41 &self, ··· 55 50 fn diff( 56 51 &self, 57 52 params: diff::Input, 58 - ) -> impl Future<Output = Result<diff::Output, XrpcError>> + Send; 53 + ) -> impl Future<Output = Result<XrpcResponse<diff::Output>, XrpcError>> + Send; 59 54 60 55 fn log( 61 56 &self, ··· 70 65 fn tree( 71 66 &self, 72 67 params: tree::Input, 73 - ) -> impl Future<Output = Result<tree::Output, XrpcError>> + Send; 68 + ) -> impl Future<Output = Result<XrpcResponse<tree::Output>, XrpcError>> + Send; 69 + } 70 + 71 + pub struct XrpcResponse<T> { 72 + pub value: T, 73 + pub immutable: bool, 74 + } 75 + 76 + impl<T> IntoResponse for XrpcResponse<T> 77 + where 78 + T: Serialize, 79 + { 80 + fn into_response(self) -> axum::response::Response { 81 + use axum::{http::header::CACHE_CONTROL, response::Json}; 82 + 83 + let mut headers = HeaderMap::new(); 84 + if self.immutable { 85 + headers.insert( 86 + CACHE_CONTROL, 87 + HeaderValue::from_static("public, immutable, s-maxage=604800"), 88 + ); 89 + } 90 + 91 + (headers, Json(self.value)).into_response() 92 + } 93 + } 94 + 95 + pub struct DirectResponse<T> { 96 + pub value: T, 97 + pub immutable: bool, 98 + } 99 + 100 + impl<T> IntoResponse for DirectResponse<T> 101 + where 102 + T: IntoResponse, 103 + { 104 + fn into_response(self) -> axum::response::Response { 105 + use axum::http::header::CACHE_CONTROL; 106 + 107 + let mut headers = HeaderMap::new(); 108 + if self.immutable { 109 + headers.insert( 110 + CACHE_CONTROL, 111 + HeaderValue::from_static("public, immutable, s-maxage=604800"), 112 + ); 113 + } 114 + 115 + (headers, self.value).into_response() 116 + } 74 117 } 75 118 76 119 pub struct GitOxideRepository<'a> { ··· 133 80 } 134 81 135 82 impl TangledRepository for GitOxideRepository<'_> { 136 - async fn blob(&self, params: blob::Input) -> Result<blob::Output, XrpcError> { 83 + async fn blob(&self, params: blob::Input) -> Result<DirectResponse<blob::Output>, XrpcError> { 137 84 let repo = self.repo.to_thread_local(); 138 85 self.state 139 86 .pool() 140 87 .spawn_fifo_async(move || { 141 - let tip = resolve_rev(&repo, params.rev.as_deref())?; 88 + let (tip, immutable) = resolve_rev(&repo, params.rev.as_deref())?; 142 89 let entry = tip 143 90 .tree()? 144 91 .lookup_entry_by_path(&params.path)? ··· 151 98 let mut blob = entry.object()?.into_blob(); 152 99 let data = blob.take_data(); 153 100 if params.raw { 154 - return Ok(blob::Output::Raw(data)); 101 + return Ok(DirectResponse { 102 + value: blob::Output::Raw(data), 103 + immutable, 104 + }); 155 105 } 156 106 157 107 let size = data.len(); ··· 167 111 ), 168 112 }; 169 113 170 - Ok(blob::Output::Json(Box::new( 171 - lexicon::sh::tangled::repo::blob::Output { 114 + Ok(DirectResponse { 115 + value: blob::Output::Json(Box::new(lexicon::sh::tangled::repo::blob::Output { 172 116 rev: params.rev.as_deref().unwrap_or_default().to_owned(), 173 117 path: params.path.to_owned(), 174 118 content, ··· 177 121 is_binary, 178 122 mime_type: "".to_string(), 179 123 last_commit: None, 180 - }, 181 - ))) 124 + })), 125 + immutable, 126 + }) 182 127 }) 183 128 .await 184 129 } ··· 268 211 .await 269 212 } 270 213 271 - async fn diff(&self, params: diff::Input) -> Result<diff::Output, XrpcError> { 214 + async fn diff(&self, params: diff::Input) -> Result<XrpcResponse<diff::Output>, XrpcError> { 272 215 let repo = self.repo.to_thread_local(); 273 216 self.state 274 217 .pool() 275 218 .spawn_fifo_async(move || { 276 - let this_commit = resolve_rev(&repo, Some(&params.rev))?; 219 + let (this_commit, immutable) = resolve_rev(&repo, Some(&params.rev))?; 277 220 let diff = super::nicediff::unified_diff_from_parent(this_commit).unwrap(); 278 221 let response = diff::Output { 279 222 rev: params.rev, 280 223 diff, 281 224 }; 282 - Ok(response) 225 + Ok(XrpcResponse { 226 + value: response, 227 + immutable, 228 + }) 283 229 }) 284 230 .await 285 231 } ··· 307 247 } 308 248 }; 309 249 310 - let tip = resolve_rev(&repo, params.rev.as_deref())?; 250 + let (tip, _) = resolve_rev(&repo, params.rev.as_deref())?; 311 251 312 252 let mut commits = Vec::new(); 313 253 for commit in repo ··· 371 311 .await 372 312 } 373 313 374 - async fn tree(&self, params: tree::Input) -> Result<tree::Output, XrpcError> { 314 + async fn tree(&self, params: tree::Input) -> Result<XrpcResponse<tree::Output>, XrpcError> { 375 315 let repo = self.repo.to_thread_local(); 376 316 self.state 377 317 .pool() 378 318 .spawn_fifo_async(move || { 379 - let tip = resolve_rev(&repo, params.rev.as_deref())?; 319 + let (tip, immutable) = resolve_rev(&repo, params.rev.as_deref())?; 380 320 let dotdot = params.path.clone().and_then(|mut path| { 381 321 path.pop(); 382 322 match path.as_os_str().is_empty() { ··· 393 333 .ok_or(PathNotFound(subpath.to_string_lossy()))?; 394 334 395 335 if !entry.mode().is_tree() { 396 - return Ok(tree::Output { 397 - files: vec![], 398 - dotdot, 399 - parent: params.path.clone(), 400 - rev: params.rev.as_deref().unwrap_or_default().to_string(), 401 - readme: None, 336 + return Ok(XrpcResponse { 337 + value: tree::Output { 338 + files: vec![], 339 + dotdot, 340 + parent: params.path.clone(), 341 + rev: params.rev.as_deref().unwrap_or_default().to_string(), 342 + readme: None, 343 + }, 344 + immutable, 402 345 }); 403 346 } 404 347 ··· 442 379 }) 443 380 .collect(); 444 381 445 - Ok(tree::Output { 446 - files, 447 - dotdot, 448 - parent, 449 - rev: params.rev.as_deref().unwrap_or_default().to_string(), 450 - readme, 382 + Ok(XrpcResponse { 383 + value: tree::Output { 384 + files, 385 + dotdot, 386 + parent, 387 + rev: params.rev.as_deref().unwrap_or_default().to_string(), 388 + readme, 389 + }, 390 + immutable, 451 391 }) 452 392 }) 453 393 .await 454 394 } 455 395 } 456 396 397 + /// Resolve a revspec into a [`gix::Commit`] and an immutable flag. 398 + /// 399 + /// A revspec is considered immutable if, and only if, it is an object ID. 457 400 fn resolve_rev<'repo>( 458 401 repo: &'repo gix::Repository, 459 402 rev: Option<&str>, 460 - ) -> Result<gix::Commit<'repo>, XrpcError> { 403 + ) -> Result<(gix::Commit<'repo>, bool), XrpcError> { 461 404 use std::str::FromStr as _; 462 405 463 - let revision = if let Some(refspec) = rev { 406 + Ok(if let Some(refspec) = rev { 464 407 match gix::ObjectId::from_str(refspec) { 465 - Ok(id) => repo.find_commit(id).map_err(RefNotFound)?, 408 + Ok(id) => (repo.find_commit(id).map_err(RefNotFound)?, true), 466 409 Err(_) => { 467 410 // Assume the revspec is a branch or tag. 468 411 let mut reference = repo.find_reference(refspec).map_err(RefNotFound)?; 469 - reference.peel_to_commit().map_err(RefNotFound)? 412 + (reference.peel_to_commit().map_err(RefNotFound)?, false) 470 413 } 471 414 } 472 415 } else { 473 - repo.head_commit().map_err(RefNotFound)? 474 - }; 475 - 476 - Ok(revision) 416 + (repo.head_commit().map_err(RefNotFound)?, false) 417 + }) 477 418 }
+2 -2
crates/knot/src/public/xrpc/sh/tangled/repo.rs
··· 47 47 xrpc_query!("sh.tangled.repo.blob", direct blob); 48 48 xrpc_query!("sh.tangled.repo.branches", branches); 49 49 xrpc_query!("sh.tangled.repo.getDefaultBranch", get_default_branch); 50 - xrpc_query!("sh.tangled.repo.diff", diff); 50 + xrpc_query!("sh.tangled.repo.diff", direct diff); 51 51 xrpc_query!("sh.tangled.repo.log", log); 52 52 xrpc_query!("sh.tangled.repo.tags", tags); 53 - xrpc_query!("sh.tangled.repo.tree", tree); 53 + xrpc_query!("sh.tangled.repo.tree", direct tree); 54 54 55 55 #[xrpc::query("sh.tangled.repo.languages")] 56 56 #[tracing::instrument(skip(_knot))]