@@ -5,6 +5,7 @@ use std::sync::Arc;
55
66use bytes:: Bytes ;
77use camino:: Utf8Path ;
8+ use serde:: Deserialize ;
89use sha2:: Digest ;
910use thiserror:: Error ;
1011use url:: Url ;
@@ -16,8 +17,9 @@ use crate::{
1617
1718/// Defensive upper bound on kpar file size (100 MiB) to catch unexpected uploads by mistake.
1819const MAX_KPAR_PUBLISH_SIZE : u64 = 100 * 1024 * 1024 ;
19- /// Path segments appended to the index URL to form the upload endpoint.
20- const UPLOAD_ENDPOINT_SEGMENTS : [ & str ; 3 ] = [ "api" , "v1" , "upload" ] ;
20+ /// Path appended to the index URL to form the upload endpoint.
21+ const UPLOAD_ENDPOINT_PATH : & str = "/api/v1/upload" ;
22+ const NO_ERROR_DETAILS : & str = "no error details provided" ;
2123
2224pub fn do_publish < P : AsRef < Utf8Path > > (
2325 path : P ,
@@ -30,14 +32,12 @@ pub fn do_publish<P: AsRef<Utf8Path>>(
3032 let header = crate :: style:: get_style_config ( ) . header ;
3133 let upload_url = build_upload_url ( & index) ?;
3234 let PublishPreparation {
33- name,
34- version,
35- file_name,
36- file_bytes,
35+ purl_versioned,
3736 metadata,
37+ kpar_bytes,
3838 } = prepare_publish_payload ( path) ?;
3939 log:: info!(
40- "{header}{:>12}{header:#} `{name}` {version} to {index}" ,
40+ "{header}{:>12}{header:#} `{purl_versioned}` to {index}" ,
4141 "Publishing" ,
4242 ) ;
4343
@@ -47,17 +47,16 @@ pub fn do_publish<P: AsRef<Utf8Path>>(
4747 let upload_url_for_log = upload_url. to_string ( ) ;
4848
4949 let build_request = move |c : & reqwest_middleware:: ClientWithMiddleware | {
50- let file_part = reqwest:: multipart:: Part :: stream ( file_bytes. clone ( ) )
51- . file_name ( file_name. clone ( ) )
52- . mime_str ( "application/zip" )
53- . expect ( "hard-coded content type must be a valid MIME" ) ;
5450 let metadata_part = reqwest:: multipart:: Part :: text ( metadata. clone ( ) )
5551 . mime_str ( "application/json" )
5652 . expect ( "hard-coded content type must be a valid MIME" ) ;
53+ let kpar_part = reqwest:: multipart:: Part :: stream ( kpar_bytes. clone ( ) )
54+ . mime_str ( "application/zip" )
55+ . expect ( "hard-coded content type must be a valid MIME" ) ;
5756
5857 let form = reqwest:: multipart:: Form :: new ( )
5958 . part ( "metadata" , metadata_part)
60- . part ( "kpar" , file_part ) ;
59+ . part ( "kpar" , kpar_part ) ;
6160
6261 c. post ( upload_url. clone ( ) ) . multipart ( form)
6362 } ;
@@ -104,19 +103,14 @@ pub fn build_upload_url(index: &Url) -> Result<Url, PublishError> {
104103
105104 let mut upload_url = index. to_owned ( ) ;
106105 {
107- let mut segments = upload_url
108- . path_segments_mut ( )
109- . expect ( "http(s) URLs are hierarchical and must support mutable path segments" ) ;
106+ // Guaranteed for validated http(s) URLs.
107+ let mut segments = upload_url. path_segments_mut ( ) . unwrap ( ) ;
110108 // Normalize both `https://host` and `https://host/`.
111109 segments. pop_if_empty ( ) ;
112110 }
113111
114112 // After normalization, reject URLs that already end with the upload path.
115- let path_segments: Vec < _ > = upload_url
116- . path_segments ( )
117- . expect ( "http(s) URLs are hierarchical and must support path segments" )
118- . collect ( ) ;
119- if path_segments. ends_with ( & UPLOAD_ENDPOINT_SEGMENTS ) {
113+ if upload_url. path ( ) . ends_with ( UPLOAD_ENDPOINT_PATH ) {
120114 return Err ( PublishError :: InvalidIndexUrl {
121115 url : index. as_str ( ) . into ( ) ,
122116 reason : "URL must point to the index root; do not include `/api/v1/upload`" . to_string ( ) ,
@@ -125,7 +119,7 @@ pub fn build_upload_url(index: &Url) -> Result<Url, PublishError> {
125119
126120 {
127121 let mut segments = upload_url. path_segments_mut ( ) . unwrap ( ) ;
128- for segment in UPLOAD_ENDPOINT_SEGMENTS {
122+ for segment in UPLOAD_ENDPOINT_PATH . trim_start_matches ( '/' ) . split ( '/' ) {
129123 segments. push ( segment) ;
130124 }
131125 }
@@ -219,11 +213,9 @@ pub enum PublishError {
219213// --- Private helpers ---
220214
221215struct PublishPreparation {
222- name : String ,
223- version : String ,
224- file_name : String ,
216+ purl_versioned : String ,
225217 // Keep upload payload in `Bytes` so request retries clone cheaply.
226- file_bytes : Bytes ,
218+ kpar_bytes : Bytes ,
227219 metadata : String ,
228220}
229221
@@ -267,9 +259,7 @@ fn prepare_publish_payload(path: &Utf8Path) -> Result<PublishPreparation, Publis
267259 } ) ?;
268260 let normalized_publisher = normalize_field ( publisher) ;
269261 let normalized_name = normalize_field ( name) ;
270- let purl = format ! ( "pkg:sysand/{normalized_publisher}/{normalized_name}@{version}" ) ;
271-
272- let file_name = path. file_name ( ) . unwrap_or ( path. as_str ( ) ) . to_string ( ) ;
262+ let purl_versioned = format ! ( "pkg:sysand/{normalized_publisher}/{normalized_name}@{version}" ) ;
273263
274264 let file_size = std:: fs:: metadata ( path)
275265 . map_err ( |e| PublishError :: KparRead ( path. as_str ( ) . into ( ) , e) ) ?
@@ -281,21 +271,19 @@ fn prepare_publish_payload(path: &Utf8Path) -> Result<PublishPreparation, Publis
281271 } ) ;
282272 }
283273
284- let file_bytes =
274+ let kpar_bytes =
285275 std:: fs:: read ( path) . map_err ( |e| PublishError :: KparRead ( path. as_str ( ) . into ( ) , e) ) ?;
286- let sha256_digest = format ! ( "{:x}" , sha2:: Sha256 :: digest( & file_bytes ) ) ;
276+ let sha256_digest = format ! ( "{:x}" , sha2:: Sha256 :: digest( & kpar_bytes ) ) ;
287277 let metadata = serde_json:: json!( {
288- "purl" : purl ,
278+ "purl" : purl_versioned ,
289279 "sha256_digest" : sha256_digest,
290280 } )
291281 . to_string ( ) ;
292282
293283 Ok ( PublishPreparation {
294- name : name. clone ( ) ,
295- version : version. clone ( ) ,
296- file_name,
297- file_bytes : Bytes :: from ( file_bytes) ,
284+ purl_versioned,
298285 metadata,
286+ kpar_bytes : Bytes :: from ( kpar_bytes) ,
299287 } )
300288}
301289
@@ -388,18 +376,22 @@ fn normalize_field(s: &str) -> String {
388376 s. to_ascii_lowercase ( ) . replace ( ' ' , "-" )
389377}
390378
391- fn error_body_to_string ( body_bytes : & [ u8 ] ) -> String {
392- if body_bytes . is_empty ( ) {
393- return "empty response body" . to_string ( ) ;
394- }
379+ # [ derive ( Deserialize ) ]
380+ struct ErrorResponse {
381+ error : String ,
382+ }
395383
384+ fn error_body_to_string ( body_bytes : & [ u8 ] ) -> String {
396385 let text = String :: from_utf8_lossy ( body_bytes) ;
397386 let trimmed = text. trim ( ) ;
387+
398388 if trimmed. is_empty ( ) {
399- "empty response body" . to_string ( )
400- } else {
401- trimmed. to_string ( )
389+ return NO_ERROR_DETAILS . to_string ( ) ;
402390 }
391+
392+ serde_json:: from_str :: < ErrorResponse > ( trimmed)
393+ . map ( |error| error. error )
394+ . unwrap_or_else ( |_| trimmed. to_string ( ) )
403395}
404396
405397#[ cfg( test) ]
0 commit comments