@@ -68,17 +68,32 @@ pub struct IngestResponse {
6868}
6969
7070#[ derive( Serialize ) ]
71+ #[ serde( rename_all = "camelCase" ) ]
7172pub struct WipeResponse {
7273 pub ok : bool ,
7374 pub deleted_files : u64 ,
7475 pub deleted_rows : u64 ,
7576}
7677
78+ #[ derive( Debug , Deserialize ) ]
79+ #[ serde( rename_all = "camelCase" ) ]
80+ pub struct DeleteSourcemapPayload {
81+ pub s3_key : String ,
82+ }
83+
84+ #[ derive( Serialize ) ]
85+ #[ serde( rename_all = "camelCase" ) ]
86+ pub struct DeleteSourcemapResponse {
87+ pub ok : bool ,
88+ pub deleted_files : u64 ,
89+ }
90+
7791#[ derive( Serialize ) ]
7892#[ serde( rename_all = "camelCase" ) ]
7993pub struct SourcemapListItem {
8094 pub build_id : String ,
8195 pub file_name : String ,
96+ pub size : u64 ,
8297}
8398
8499#[ derive( Serialize ) ]
@@ -153,6 +168,7 @@ pub fn internal_router(state: SharedState) -> Router {
153168 Router :: new ( )
154169 . route ( "/health" , get ( health) )
155170 . route ( "/internal/sourcemaps" , delete ( wipe) )
171+ . route ( "/internal/sourcemaps/object" , delete ( delete_sourcemap) )
156172 . route ( "/internal/sourcemaps/cleanup" , delete ( cleanup_old_builds) )
157173 . route ( "/internal/sourcemaps" , get ( list_sourcemaps) )
158174 . route ( "/internal/sourcemaps/apply" , post ( apply_sourcemap) )
@@ -302,18 +318,39 @@ pub async fn wipe(
302318 } ) )
303319}
304320
321+ pub async fn delete_sourcemap (
322+ auth : AdminAuthenticatedProject ,
323+ State ( state) : State < SharedState > ,
324+ Json ( payload) : Json < DeleteSourcemapPayload > ,
325+ ) -> Result < Json < DeleteSourcemapResponse > , AppError > {
326+ let project_id = auth. project_id ;
327+ let s3_key = validate_project_sourcemap_key ( project_id, & payload. s3_key ) ?;
328+ let deleted_files = state
329+ . storage
330+ . delete_keys ( std:: slice:: from_ref ( & s3_key) )
331+ . await ?;
332+
333+ info ! ( %project_id, s3_key, deleted_files, "deleted sourcemap" ) ;
334+
335+ Ok ( Json ( DeleteSourcemapResponse {
336+ ok : true ,
337+ deleted_files,
338+ } ) )
339+ }
340+
305341pub async fn list_sourcemaps (
306342 auth : AdminAuthenticatedProject ,
307343 State ( state) : State < SharedState > ,
308344) -> Result < Json < SourcemapListResponse > , AppError > {
309345 let prefix = format ! ( "{}/" , auth. project_id) ;
310- let keys = state. storage . list_prefix_keys ( & prefix) . await ?;
311- let mut sourcemaps: Vec < SourcemapListItem > = keys
346+ let objects = state. storage . list_prefix_objects ( & prefix) . await ?;
347+ let mut sourcemaps: Vec < SourcemapListItem > = objects
312348 . into_iter ( )
313- . filter_map ( |key | {
314- parse_sourcemap_key ( & key) . map ( |( build_id, file_name) | SourcemapListItem {
349+ . filter_map ( |object | {
350+ parse_sourcemap_key ( & object . key ) . map ( |( build_id, file_name) | SourcemapListItem {
315351 build_id,
316352 file_name,
353+ size : object. size_bytes . unwrap_or ( 0 ) ,
317354 } )
318355 } )
319356 . collect ( ) ;
@@ -499,6 +536,17 @@ fn parse_sourcemap_key(key: &str) -> Option<(String, String)> {
499536 Some ( ( build_id, file_name) )
500537}
501538
539+ fn validate_project_sourcemap_key ( project_id : Uuid , s3_key : & str ) -> Result < String , AppError > {
540+ require_non_empty ( "s3_key" , s3_key) ?;
541+
542+ let prefix = format ! ( "{project_id}/" ) ;
543+ if !s3_key. starts_with ( & prefix) || parse_sourcemap_key ( s3_key) . is_none ( ) {
544+ return Err ( AppError :: BadRequest ( "invalid sourcemap key" . into ( ) ) ) ;
545+ }
546+
547+ Ok ( s3_key. to_string ( ) )
548+ }
549+
502550fn normalized_build_ids ( input : & [ String ] ) -> Vec < String > {
503551 let mut seen = HashSet :: new ( ) ;
504552 let mut out = Vec :: new ( ) ;
@@ -553,10 +601,11 @@ fn select_builds_for_cleanup(
553601mod tests {
554602 use super :: {
555603 ProguardEntry , normalize_proguard_entries, normalized_build_ids, parse_sourcemap_key,
556- select_builds_for_cleanup,
604+ select_builds_for_cleanup, validate_project_sourcemap_key ,
557605 } ;
558606 use crate :: mappings:: { javascript:: map_file_name, require_non_empty} ;
559607 use crate :: storage:: StoredObjectMeta ;
608+ use uuid:: Uuid ;
560609
561610 #[ test]
562611 fn map_file_name_adds_map_suffix_when_missing ( ) {
@@ -582,6 +631,27 @@ mod tests {
582631 ) ;
583632 }
584633
634+ #[ test]
635+ fn validate_project_sourcemap_key_accepts_owned_key ( ) {
636+ let project_id = Uuid :: parse_str ( "01954b9b-7b1d-72b8-8af3-f8d058f60b79" ) . unwrap ( ) ;
637+ let key = format ! ( "{project_id}/build-42/chunk.js.map" ) ;
638+
639+ let validated = validate_project_sourcemap_key ( project_id, & key) . unwrap ( ) ;
640+
641+ assert_eq ! ( validated, key) ;
642+ }
643+
644+ #[ test]
645+ fn validate_project_sourcemap_key_rejects_foreign_key ( ) {
646+ let project_id = Uuid :: parse_str ( "01954b9b-7b1d-72b8-8af3-f8d058f60b79" ) . unwrap ( ) ;
647+ let foreign_project_id = Uuid :: parse_str ( "01954b9b-8228-7d29-9d18-c97b9fb3f924" ) . unwrap ( ) ;
648+ let key = format ! ( "{foreign_project_id}/build-42/chunk.js.map" ) ;
649+
650+ let err = validate_project_sourcemap_key ( project_id, & key) . expect_err ( "key should fail" ) ;
651+
652+ assert ! ( format!( "{err}" ) . contains( "invalid sourcemap key" ) ) ;
653+ }
654+
585655 #[ test]
586656 fn require_non_empty_rejects_whitespace ( ) {
587657 let err = require_non_empty ( "build_id" , " " ) . expect_err ( "value should be invalid" ) ;
@@ -672,18 +742,22 @@ mod tests {
672742 StoredObjectMeta {
673743 key: "proj/build-001/app.js.map" . to_string( ) ,
674744 last_modified_epoch_seconds: Some ( 100 ) ,
745+ size_bytes: None ,
675746 } ,
676747 StoredObjectMeta {
677748 key: "proj/build-002/app.js.map" . to_string( ) ,
678749 last_modified_epoch_seconds: Some ( 200 ) ,
750+ size_bytes: None ,
679751 } ,
680752 StoredObjectMeta {
681753 key: "proj/build-003/app.js.map" . to_string( ) ,
682754 last_modified_epoch_seconds: Some ( 400 ) ,
755+ size_bytes: None ,
683756 } ,
684757 StoredObjectMeta {
685758 key: "proj/build-004/app.js.map" . to_string( ) ,
686759 last_modified_epoch_seconds: Some ( 300 ) ,
760+ size_bytes: None ,
687761 } ,
688762 ] ;
689763 let excluded = vec ! [ "build-002" . to_string( ) ] ;
@@ -709,14 +783,17 @@ mod tests {
709783 StoredObjectMeta {
710784 key: "proj/build-a/a.js.map" . to_string( ) ,
711785 last_modified_epoch_seconds: Some ( 100 ) ,
786+ size_bytes: None ,
712787 } ,
713788 StoredObjectMeta {
714789 key: "proj/build-a/b.js.map" . to_string( ) ,
715790 last_modified_epoch_seconds: Some ( 500 ) ,
791+ size_bytes: None ,
716792 } ,
717793 StoredObjectMeta {
718794 key: "proj/build-b/a.js.map" . to_string( ) ,
719795 last_modified_epoch_seconds: Some ( 400 ) ,
796+ size_bytes: None ,
720797 } ,
721798 ] ;
722799
0 commit comments