Skip to content

Commit 0e2a591

Browse files
committed
feat: add poper size and single delete route
1 parent f6ed5b4 commit 0e2a591

2 files changed

Lines changed: 84 additions & 5 deletions

File tree

apps/backend/src/routes.rs

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,32 @@ pub struct IngestResponse {
6868
}
6969

7070
#[derive(Serialize)]
71+
#[serde(rename_all = "camelCase")]
7172
pub 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")]
7993
pub 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+
305341
pub 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+
502550
fn 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(
553601
mod 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

apps/backend/src/storage.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub struct Storage {
2020
pub struct StoredObjectMeta {
2121
pub key: String,
2222
pub last_modified_epoch_seconds: Option<i64>,
23+
pub size_bytes: Option<u64>,
2324
}
2425

2526
impl Storage {
@@ -203,6 +204,7 @@ impl Storage {
203204
objects.push(StoredObjectMeta {
204205
key: key.to_string(),
205206
last_modified_epoch_seconds: object.last_modified().map(|dt| dt.secs()),
207+
size_bytes: object.size().and_then(|size| u64::try_from(size).ok()),
206208
});
207209
}
208210
}

0 commit comments

Comments
 (0)