···66use std::path::PathBuf;
77use std::time::Instant;
8899+/// Print failed bundles with their error messages
1010+///
1111+/// # Arguments
1212+/// * `failed_bundles` - Vector of (bundle_num, errors) tuples
1313+/// * `threshold` - Maximum number of bundles to list in full before truncating (e.g., 10 or 20)
1414+fn print_failed_bundles(failed_bundles: &[(u32, Vec<String>)], threshold: usize) {
1515+ if failed_bundles.is_empty() {
1616+ return;
1717+ }
1818+1919+ if failed_bundles.len() <= threshold {
2020+ eprintln!("\n ⚠️ Failed bundles:");
2121+ for (bundle_num, errors) in failed_bundles {
2222+ eprintln!(" Bundle {}:", bundle_num);
2323+ if errors.is_empty() {
2424+ eprintln!(" • Verification failed (no error details)");
2525+ } else {
2626+ for err in errors {
2727+ eprintln!(" • {}", err);
2828+ }
2929+ }
3030+ }
3131+ } else {
3232+ eprintln!(
3333+ " ⚠️ Failed bundles: {} (too many to list)",
3434+ failed_bundles.len()
3535+ );
3636+ // Show first few with details
3737+ eprintln!("\n First few failures:");
3838+ for (bundle_num, errors) in failed_bundles.iter().take(5) {
3939+ eprintln!(" Bundle {}:", bundle_num);
4040+ if errors.is_empty() {
4141+ eprintln!(" • Verification failed (no error details)");
4242+ } else {
4343+ for err in errors.iter().take(3) {
4444+ eprintln!(" • {}", err);
4545+ }
4646+ if errors.len() > 3 {
4747+ eprintln!(" • ... and {} more error(s)", errors.len() - 3);
4848+ }
4949+ }
5050+ }
5151+ eprintln!(" ... and {} more failed bundles", failed_bundles.len() - 5);
5252+ }
5353+}
5454+955#[derive(Args)]
1056#[command(
1157 about = "Verify bundle integrity and chain",
···547593 eprintln!(" Errors: {}", error_count);
548594549595 // Show failed bundles with their error messages
550550- if !failed_bundles.is_empty() {
551551- if failed_bundles.len() <= 10 {
552552- eprintln!("\n ⚠️ Failed bundles:");
553553- for (bundle_num, errors) in &failed_bundles {
554554- eprintln!(" Bundle {}:", bundle_num);
555555- if errors.is_empty() {
556556- eprintln!(" • Verification failed (no error details)");
557557- } else {
558558- for err in errors {
559559- eprintln!(" • {}", err);
560560- }
561561- }
562562- }
563563- } else {
564564- eprintln!(
565565- " ⚠️ Failed bundles: {} (too many to list)",
566566- failed_bundles.len()
567567- );
568568- // Show first few with details
569569- eprintln!("\n First few failures:");
570570- for (bundle_num, errors) in failed_bundles.iter().take(5) {
571571- eprintln!(" Bundle {}:", bundle_num);
572572- if errors.is_empty() {
573573- eprintln!(" • Verification failed (no error details)");
574574- } else {
575575- for err in errors.iter().take(3) {
576576- eprintln!(" • {}", err);
577577- }
578578- if errors.len() > 3 {
579579- eprintln!(" • ... and {} more error(s)", errors.len() - 3);
580580- }
581581- }
582582- }
583583- eprintln!(" ... and {} more failed bundles", failed_bundles.len() - 5);
584584- }
585585- }
596596+ print_failed_bundles(&failed_bundles, 10);
586597587598 eprintln!(" Time: {:?}", elapsed);
588599···771782 eprintln!(" Verified: {}/{}", verified, total);
772783 eprintln!(" Failed: {}", failed);
773784774774- if !failed_bundles.is_empty() {
775775- if failed_bundles.len() <= 20 {
776776- eprintln!("\n ⚠️ Failed bundles:");
777777- for (bundle_num, errors) in &failed_bundles {
778778- eprintln!(" Bundle {}:", bundle_num);
779779- if errors.is_empty() {
780780- eprintln!(" • Verification failed (no error details)");
781781- } else {
782782- for err in errors {
783783- eprintln!(" • {}", err);
784784- }
785785- }
786786- }
787787- } else {
788788- eprintln!(
789789- "\n ⚠️ Failed bundles: {} (too many to list)",
790790- failed_bundles.len()
791791- );
792792- // Show first few with details
793793- eprintln!("\n First few failures:");
794794- for (bundle_num, errors) in failed_bundles.iter().take(5) {
795795- eprintln!(" Bundle {}:", bundle_num);
796796- if errors.is_empty() {
797797- eprintln!(" • Verification failed (no error details)");
798798- } else {
799799- for err in errors.iter().take(3) {
800800- eprintln!(" • {}", err);
801801- }
802802- if errors.len() > 3 {
803803- eprintln!(" • ... and {} more error(s)", errors.len() - 3);
804804- }
805805- }
806806- }
807807- eprintln!(" ... and {} more failed bundles", failed_bundles.len() - 5);
808808- }
809809- }
785785+ print_failed_bundles(&failed_bundles, 20);
810786811787 bail!("verification failed for {} bundles", failed)
812788 }
-4
src/cli/utils.rs
···18181919 /// Dim/bright black color (used for context, unchanged lines, etc.)
2020 pub const DIM: &str = "\x1b[2m";
2121-2222- /// Bold text
2323- #[allow(dead_code)]
2424- pub const BOLD: &str = "\x1b[1m";
2521}
26222723#[cfg(feature = "cli")]
+1-1
src/index.rs
···298298299299 // Extract embedded metadata from bundle file
300300 let embedded = crate::bundle_format::extract_metadata_from_file(bundle_path)
301301- .with_context(|| format!("Failed to extract metadata from bundle {:06}", bundle_num))?;
301301+ .with_context(|| format!("Failed to extract metadata from bundle {}", bundle_num))?;
302302303303 // Auto-detect origin from first bundle if not provided
304304 {
-170
src/server/handlers/bundle.rs
···11-// Bundle-related handlers
22-33-use crate::constants;
44-use crate::server::error::{bad_request, internal_error, is_not_found_error, not_found, task_join_error};
55-use crate::server::handlers::ServerState;
66-use crate::server::utils::{bundle_download_headers, parse_operation_pointer};
77-use axum::{
88- body::Body,
99- extract::{Path, State},
1010- http::{HeaderMap, HeaderValue, StatusCode},
1111- response::IntoResponse,
1212-};
1313-use std::sync::Arc;
1414-use std::time::Instant;
1515-use tokio_util::io::ReaderStream;
1616-1717-pub async fn handle_bundle(
1818- State(state): State<ServerState>,
1919- Path(number): Path<u32>,
2020-) -> impl IntoResponse {
2121- match state.manager.get_bundle_metadata(number) {
2222- Ok(Some(meta)) => (StatusCode::OK, axum::Json(meta)).into_response(),
2323- Ok(None) => not_found("Bundle not found").into_response(),
2424- Err(e) => internal_error(&e.to_string()).into_response(),
2525- }
2626-}
2727-2828-pub async fn handle_bundle_data(
2929- State(state): State<ServerState>,
3030- Path(number): Path<u32>,
3131-) -> impl IntoResponse {
3232- // Use BundleManager API to get bundle file stream
3333- let file_result = tokio::task::spawn_blocking({
3434- let manager = Arc::clone(&state.manager);
3535- move || manager.stream_bundle_raw(number)
3636- })
3737- .await;
3838-3939- match file_result {
4040- Ok(Ok(std_file)) => {
4141- // Convert std::fs::File to tokio::fs::File
4242- let file = tokio::fs::File::from_std(std_file);
4343- let stream = ReaderStream::new(file);
4444- let body = Body::from_stream(stream);
4545-4646- let headers = bundle_download_headers(
4747- "application/zstd",
4848- &constants::bundle_filename(number),
4949- );
5050-5151- (StatusCode::OK, headers, body).into_response()
5252- }
5353- Ok(Err(e)) => {
5454- // Handle errors from BundleManager
5555- if is_not_found_error(&e) {
5656- not_found("Bundle not found").into_response()
5757- } else {
5858- internal_error(&e.to_string()).into_response()
5959- }
6060- }
6161- Err(e) => task_join_error(e).into_response(),
6262- }
6363-}
6464-6565-pub async fn handle_bundle_jsonl(
6666- State(state): State<ServerState>,
6767- Path(number): Path<u32>,
6868-) -> impl IntoResponse {
6969- // For streaming decompressed data, read in spawn_blocking and stream chunks
7070- // TODO: Implement true async streaming when tokio-util supports it better
7171- match tokio::task::spawn_blocking({
7272- let manager = Arc::clone(&state.manager);
7373- move || {
7474- let mut reader = manager.stream_bundle_decompressed(number)?;
7575- use std::io::Read;
7676- let mut buf = Vec::new();
7777- reader.read_to_end(&mut buf)?;
7878- Ok::<Vec<u8>, anyhow::Error>(buf)
7979- }
8080- })
8181- .await
8282- {
8383- Ok(Ok(data)) => {
8484- let filename = constants::bundle_filename(number).replace(".zst", "");
8585- let headers = bundle_download_headers("application/x-ndjson", &filename);
8686-8787- (StatusCode::OK, headers, data).into_response()
8888- }
8989- Ok(Err(e)) => {
9090- if is_not_found_error(&e) {
9191- not_found("Bundle not found").into_response()
9292- } else {
9393- internal_error(&e.to_string()).into_response()
9494- }
9595- }
9696- Err(_) => internal_error("Task join error").into_response(),
9797- }
9898-}
9999-100100-pub async fn handle_operation(
101101- State(state): State<ServerState>,
102102- Path(pointer): Path<String>,
103103-) -> impl IntoResponse {
104104- // Parse pointer: "bundle:position" or global position
105105- let (bundle_num, position) = match parse_operation_pointer(&pointer) {
106106- Ok((b, p)) => (b, p),
107107- Err(e) => return bad_request(&e.to_string()).into_response(),
108108- };
109109-110110- if position >= constants::BUNDLE_SIZE {
111111- return bad_request("Position must be 0-9999").into_response();
112112- }
113113-114114- let total_start = Instant::now();
115115- let load_start = Instant::now();
116116-117117- // get_operation_raw performs blocking file I/O, so we need to use spawn_blocking
118118- let json_result = tokio::task::spawn_blocking({
119119- let manager = Arc::clone(&state.manager);
120120- move || manager.get_operation_raw(bundle_num, position)
121121- })
122122- .await;
123123-124124- match json_result {
125125- Ok(Ok(json)) => {
126126- let load_duration = load_start.elapsed();
127127- let total_duration = total_start.elapsed();
128128-129129- let global_pos = (bundle_num as u64 * constants::BUNDLE_SIZE as u64) + position as u64;
130130-131131- let mut headers = HeaderMap::new();
132132- headers.insert("X-Bundle-Number", HeaderValue::from(bundle_num));
133133- headers.insert("X-Position", HeaderValue::from(position));
134134- headers.insert(
135135- "X-Global-Position",
136136- HeaderValue::from_str(&global_pos.to_string()).unwrap(),
137137- );
138138- headers.insert(
139139- "X-Pointer",
140140- HeaderValue::from_str(&format!("{}:{}", bundle_num, position)).unwrap(),
141141- );
142142- headers.insert(
143143- "X-Load-Time-Ms",
144144- HeaderValue::from_str(&format!("{:.3}", load_duration.as_secs_f64() * 1000.0))
145145- .unwrap(),
146146- );
147147- headers.insert(
148148- "X-Total-Time-Ms",
149149- HeaderValue::from_str(&format!("{:.3}", total_duration.as_secs_f64() * 1000.0))
150150- .unwrap(),
151151- );
152152- headers.insert(
153153- "Cache-Control",
154154- HeaderValue::from_static("public, max-age=31536000, immutable"),
155155- );
156156- headers.insert("Content-Type", HeaderValue::from_static("application/json"));
157157-158158- (StatusCode::OK, headers, json).into_response()
159159- }
160160- Ok(Err(e)) => {
161161- if is_not_found_error(&e) {
162162- not_found("Operation not found").into_response()
163163- } else {
164164- internal_error(&e.to_string()).into_response()
165165- }
166166- }
167167- Err(e) => task_join_error(e).into_response(),
168168- }
169169-}
170170-