Skip to content

Commit b17c5c1

Browse files
committed
fix: proper file list
1 parent 31cda76 commit b17c5c1

File tree

2 files changed

+182
-14
lines changed

2 files changed

+182
-14
lines changed

apps/backend/src/mappings/proguard.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use super::OriginalPosition;
1010
use super::s3_key;
1111

1212
const PROGUARD_DIR: &str = "proguard";
13-
const PROGUARD_FILE_NAME: &str = "proguard/mapping.txt";
1413

1514
/// A parsed proguard mapping file.
1615
struct ProguardMapping {
@@ -338,10 +337,14 @@ pub async fn ingest(
338337
storage: &Storage,
339338
project_id: Uuid,
340339
build_id: &str,
341-
mapping: &str,
340+
mappings: &[(String, String)],
342341
) -> Result<(), AppError> {
343-
let key = proguard_s3_key(project_id, build_id);
344-
storage.put(&key, mapping.as_bytes()).await
342+
for (file_name, mapping) in mappings {
343+
let key = proguard_s3_key(project_id, build_id, file_name);
344+
storage.put(&key, mapping.as_bytes()).await?;
345+
}
346+
347+
Ok(())
345348
}
346349

347350
pub fn retrace_stacktrace<'a>(
@@ -364,8 +367,8 @@ pub fn proguard_s3_prefix(project_id: Uuid, build_id: &str) -> String {
364367
s3_key(project_id, build_id, PROGUARD_DIR)
365368
}
366369

367-
pub fn proguard_s3_key(project_id: Uuid, build_id: &str) -> String {
368-
s3_key(project_id, build_id, PROGUARD_FILE_NAME)
370+
pub fn proguard_s3_key(project_id: Uuid, build_id: &str, file_name: &str) -> String {
371+
s3_key(project_id, build_id, &format!("{PROGUARD_DIR}/{file_name}"))
369372
}
370373

371374
#[cfg(test)]
@@ -569,4 +572,15 @@ java.lang.RuntimeException: oops
569572
\tat core.file.Validatable.validate(Validatable.java:26)"
570573
);
571574
}
575+
576+
#[test]
577+
fn proguard_s3_key_uses_uploaded_file_name() {
578+
let project_id = Uuid::nil();
579+
let key = proguard_s3_key(project_id, "build-1", "base.txt");
580+
581+
assert_eq!(
582+
key,
583+
"00000000-0000-0000-0000-000000000000/build-1/proguard/base.txt"
584+
);
585+
}
572586
}

apps/backend/src/routes.rs

Lines changed: 162 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ pub struct SourcemapEntry {
2525
pub sourcemap: String,
2626
}
2727

28+
#[derive(Debug, Deserialize)]
29+
pub struct ProguardEntry {
30+
#[serde(alias = "fileName")]
31+
pub file_name: String,
32+
#[serde(alias = "content")]
33+
pub mapping: String,
34+
}
35+
2836
#[derive(Debug, Deserialize)]
2937
#[serde(tag = "mappingType")]
3038
pub enum IngestPayload {
@@ -43,7 +51,13 @@ pub enum IngestPayload {
4351
build_id: String,
4452
#[serde(alias = "uploadedAt")]
4553
uploaded_at: String,
46-
mapping: String,
54+
#[serde(alias = "fileName")]
55+
file_name: Option<String>,
56+
mapping: Option<String>,
57+
#[serde(default)]
58+
mappings: Vec<ProguardEntry>,
59+
#[serde(default)]
60+
files: Vec<ProguardEntry>,
4761
},
4862
}
4963

@@ -191,17 +205,34 @@ pub async fn ingest(
191205
build_id,
192206
uploaded_at,
193207
mapping,
208+
file_name,
209+
mappings,
210+
files,
194211
} => {
195-
require_non_empty("build_id", build_id)?;
196-
require_non_empty("uploaded_at", uploaded_at)?;
197-
require_non_empty("mapping", mapping)?;
198-
crate::mappings::proguard::ingest(&state.storage, project_id, build_id, mapping)
212+
let mappings = normalize_proguard_entries(
213+
build_id,
214+
uploaded_at,
215+
file_name,
216+
mapping,
217+
mappings,
218+
files,
219+
)?;
220+
crate::mappings::proguard::ingest(&state.storage, project_id, build_id, &mappings)
199221
.await?;
200-
let total_bytes = mapping.len();
222+
let total_bytes: usize = mappings.iter().map(|(_, mapping)| mapping.len()).sum();
223+
let file_names: Vec<&str> = mappings
224+
.iter()
225+
.map(|(file_name, _)| file_name.as_str())
226+
.collect();
227+
info!(
228+
%project_id,
229+
build_id,
230+
files = ?file_names,
231+
);
201232
(
202233
build_id.as_str(),
203234
uploaded_at.as_str(),
204-
1,
235+
mappings.len(),
205236
total_bytes,
206237
"proguard",
207238
)
@@ -412,6 +443,54 @@ fn validate_js_ingest(
412443
Ok(())
413444
}
414445

446+
fn normalize_proguard_entries(
447+
build_id: &str,
448+
uploaded_at: &str,
449+
file_name: &Option<String>,
450+
mapping: &Option<String>,
451+
mappings: &[ProguardEntry],
452+
files: &[ProguardEntry],
453+
) -> Result<Vec<(String, String)>, AppError> {
454+
require_non_empty("build_id", build_id)?;
455+
require_non_empty("uploaded_at", uploaded_at)?;
456+
457+
let mut normalized =
458+
Vec::with_capacity(mappings.len() + files.len() + usize::from(mapping.is_some()));
459+
normalized.extend(
460+
mappings
461+
.iter()
462+
.map(|entry| (entry.file_name.clone(), entry.mapping.clone())),
463+
);
464+
normalized.extend(
465+
files
466+
.iter()
467+
.map(|entry| (entry.file_name.clone(), entry.mapping.clone())),
468+
);
469+
470+
match (file_name.as_deref(), mapping.as_deref()) {
471+
(Some(file_name), Some(mapping)) => {
472+
normalized.push((file_name.to_string(), mapping.to_string()));
473+
}
474+
(Some(_), None) => return Err(AppError::BadRequest("mapping is required".into())),
475+
(None, Some(mapping)) if normalized.is_empty() => {
476+
normalized.push(("mapping.txt".to_string(), mapping.to_string()));
477+
}
478+
(None, Some(_)) => return Err(AppError::BadRequest("file_name is required".into())),
479+
(None, None) => {}
480+
}
481+
482+
if normalized.is_empty() {
483+
return Err(AppError::BadRequest("no proguard mappings provided".into()));
484+
}
485+
486+
for (file_name, mapping) in &normalized {
487+
require_non_empty("file_name", file_name)?;
488+
require_non_empty("mapping", mapping)?;
489+
}
490+
491+
Ok(normalized)
492+
}
493+
415494
fn parse_sourcemap_key(key: &str) -> Option<(String, String)> {
416495
let mut parts = key.splitn(3, '/');
417496
let _project_id = parts.next()?;
@@ -472,7 +551,10 @@ fn select_builds_for_cleanup(
472551

473552
#[cfg(test)]
474553
mod tests {
475-
use super::{normalized_build_ids, parse_sourcemap_key, select_builds_for_cleanup};
554+
use super::{
555+
ProguardEntry, normalize_proguard_entries, normalized_build_ids, parse_sourcemap_key,
556+
select_builds_for_cleanup,
557+
};
476558
use crate::mappings::{javascript::map_file_name, require_non_empty};
477559
use crate::storage::StoredObjectMeta;
478560

@@ -491,6 +573,15 @@ mod tests {
491573
);
492574
}
493575

576+
#[test]
577+
fn parse_sourcemap_key_keeps_nested_proguard_file_name() {
578+
let parsed = parse_sourcemap_key("proj-id/build-42/proguard/base.txt");
579+
assert_eq!(
580+
parsed,
581+
Some(("build-42".to_string(), "proguard/base.txt".to_string()))
582+
);
583+
}
584+
494585
#[test]
495586
fn require_non_empty_rejects_whitespace() {
496587
let err = require_non_empty("build_id", " ").expect_err("value should be invalid");
@@ -512,6 +603,69 @@ mod tests {
512603
);
513604
}
514605

606+
#[test]
607+
fn normalize_proguard_entries_accepts_multiple_named_files() {
608+
let normalized = normalize_proguard_entries(
609+
"build-1",
610+
"2026-03-22T00:00:00Z",
611+
&None,
612+
&None,
613+
&[ProguardEntry {
614+
file_name: "base.txt".to_string(),
615+
mapping: "one".to_string(),
616+
}],
617+
&[ProguardEntry {
618+
file_name: "feature.txt".to_string(),
619+
mapping: "two".to_string(),
620+
}],
621+
)
622+
.expect("entries should normalize");
623+
624+
assert_eq!(
625+
normalized,
626+
vec![
627+
("base.txt".to_string(), "one".to_string()),
628+
("feature.txt".to_string(), "two".to_string()),
629+
]
630+
);
631+
}
632+
633+
#[test]
634+
fn normalize_proguard_entries_keeps_legacy_single_mapping_compatible() {
635+
let normalized = normalize_proguard_entries(
636+
"build-1",
637+
"2026-03-22T00:00:00Z",
638+
&None,
639+
&Some("contents".to_string()),
640+
&[],
641+
&[],
642+
)
643+
.expect("legacy payload should normalize");
644+
645+
assert_eq!(
646+
normalized,
647+
vec![("mapping.txt".to_string(), "contents".to_string())]
648+
);
649+
}
650+
651+
#[test]
652+
fn normalize_proguard_entries_rejects_mixed_unnamed_mapping() {
653+
let err = normalize_proguard_entries(
654+
"build-1",
655+
"2026-03-22T00:00:00Z",
656+
&None,
657+
&Some("contents".to_string()),
658+
&[ProguardEntry {
659+
file_name: "base.txt".to_string(),
660+
mapping: "one".to_string(),
661+
}],
662+
&[],
663+
)
664+
.expect_err("mixed unnamed mapping should be rejected");
665+
666+
assert!(format!("{err}").contains("file_name is required"));
667+
}
668+
515669
#[test]
516670
fn select_builds_for_cleanup_keeps_latest_and_excluded() {
517671
let objects = vec![

0 commit comments

Comments
 (0)