Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions src/api/data_types/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;

const IMAGE_FILE_NAME_FIELD: &str = "image_file_name";
const WIDTH_FIELD: &str = "width";
const HEIGHT_FIELD: &str = "height";

/// Response from the create snapshot endpoint.
#[derive(Debug, Deserialize)]
Expand All @@ -22,9 +27,61 @@ pub struct SnapshotsManifest {

// Keep in sync with https://github.com/getsentry/sentry/blob/master/src/sentry/preprod/snapshots/manifest.py
/// Metadata for a single image in a snapshot manifest.
///
/// Serializes as a flat JSON object.
///
/// CLI-managed fields (`image_file_name`, `width`, `height`) override any
/// identically named fields provided by user sidecar metadata.
#[derive(Debug, Serialize)]
pub struct ImageMetadata {
pub image_file_name: String,
pub width: u32,
pub height: u32,
#[serde(flatten)]
data: HashMap<String, Value>,
}

impl ImageMetadata {
pub fn new(
image_file_name: String,
width: u32,
height: u32,
mut extra: HashMap<String, Value>,
) -> Self {
extra.insert(
IMAGE_FILE_NAME_FIELD.to_owned(),
Value::String(image_file_name),
);
extra.insert(WIDTH_FIELD.to_owned(), Value::from(width));
extra.insert(HEIGHT_FIELD.to_owned(), Value::from(height));

Self { data: extra }
}
}

#[cfg(test)]
mod tests {
use super::*;

use serde_json::json;

#[test]
fn cli_managed_fields_override_sidecar_fields() {
let extra = serde_json::from_value(json!({
(IMAGE_FILE_NAME_FIELD): "from-sidecar.png",
(WIDTH_FIELD): 1,
(HEIGHT_FIELD): 2,
"custom": "keep-me"
}))
.unwrap();

let metadata = ImageMetadata::new("from-cli.png".to_owned(), 100, 200, extra);
let serialized = serde_json::to_value(metadata).unwrap();

let expected = json!({
(IMAGE_FILE_NAME_FIELD): "from-cli.png",
(WIDTH_FIELD): 100,
(HEIGHT_FIELD): 200,
"custom": "keep-me"
});

assert_eq!(serialized, expected);
}
}
44 changes: 39 additions & 5 deletions src/commands/build/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
style(images.len()).yellow(),
if images.len() == 1 { "file" } else { "files" }
);

let manifest_entries = upload_images(images, &org, &project)?;

// Build manifest from discovered images
Expand Down Expand Up @@ -188,6 +189,40 @@ fn is_image_file(path: &Path) -> bool {
.unwrap_or(false)
}

/// Reads the companion JSON sidecar for an image, if it exists.
///
/// For an image at `path/to/button.png`, looks for `path/to/button.json`.
/// Returns a map of all key-value pairs from the JSON file.
fn read_sidecar_metadata(image_path: &Path) -> HashMap<String, serde_json::Value> {
let sidecar_path = image_path.with_extension("json");
if !sidecar_path.is_file() {
return HashMap::new();
}

debug!("Reading sidecar metadata: {}", sidecar_path.display());
let contents = match fs::read_to_string(&sidecar_path) {
Ok(c) => c,
Err(err) => {
warn!(
"Failed to read sidecar file {}: {err}",
sidecar_path.display()
);
return HashMap::new();
}
};

match serde_json::from_str(&contents) {
Ok(map) => map,
Err(err) => {
warn!(
"Failed to parse sidecar file {}: {err}",
sidecar_path.display()
);
HashMap::new()
}
}
}

fn upload_images(
images: Vec<ImageInfo>,
org: &str,
Expand Down Expand Up @@ -247,13 +282,12 @@ fn upload_images(
.unwrap_or_default()
.to_string_lossy()
.into_owned();

let extra = read_sidecar_metadata(&image.path);

manifest_entries.insert(
hash,
ImageMetadata {
image_file_name,
width: image.width,
height: image.height,
},
ImageMetadata::new(image_file_name, image.width, image.height, extra),
);
}

Expand Down