diff --git a/server-rs/crates/spacetime-module/src/game_distribution.rs b/server-rs/crates/spacetime-module/src/game_distribution.rs index c8b8b6f46..c86356477 100644 --- a/server-rs/crates/spacetime-module/src/game_distribution.rs +++ b/server-rs/crates/spacetime-module/src/game_distribution.rs @@ -4208,23 +4208,48 @@ fn game_distribution_game_is_publicly_visible(game: &GameDistributionGame) -> bo && game.deleted_at.is_none() } +/// 从发布媒体 objectKey 里反解 game_id;布局不符(含非本前缀的 key)返回 `None`。 +/// +/// 媒体 key 由 api-server 按 `{prefix}{game_id}/{kind}-{uuid}.{ext}` 生成,这里只做字符串切分, +/// 不信任调用方传入的任意 key。 +fn game_distribution_media_object_key_game_id(object_key: &str) -> Option<&str> { + let rest = object_key + .strip_prefix(shared_contracts::game_distribution::GAME_DISTRIBUTION_MEDIA_PREFIX)?; + let (game_id, file_name) = rest.split_once('/')?; + if game_id.is_empty() || file_name.is_empty() { + return None; + } + Some(game_id) +} + /// 已公开游戏的封面/截图 OSS objectKey 允许匿名读者换取签名读地址。 +/// +/// 媒体 key 自带 game_id,直接按主键读那一行再核对 key 是否属于它的封面/截图,避免逐行扫描全部游戏。 pub(crate) fn game_distribution_media_has_public_read_grant( ctx: &ReducerContext, object_key: &str, ) -> bool { - ctx.db.game_distribution_game().iter().any(|game| { - if !game_distribution_game_is_publicly_visible(&game) { - return false; - } - if game.cover_object_key.as_deref() == Some(object_key) { - return true; - } - game.screenshots_json - .as_deref() - .and_then(|json| serde_json::from_str::>(json).ok()) - .is_some_and(|screenshots| screenshots.iter().any(|key| key == object_key)) - }) + let Some(game_id) = game_distribution_media_object_key_game_id(object_key) else { + return false; + }; + let Some(game) = ctx + .db + .game_distribution_game() + .game_id() + .find(&game_id.to_string()) + else { + return false; + }; + if !game_distribution_game_is_publicly_visible(&game) { + return false; + } + if game.cover_object_key.as_deref() == Some(object_key) { + return true; + } + game.screenshots_json + .as_deref() + .and_then(|json| serde_json::from_str::>(json).ok()) + .is_some_and(|screenshots| screenshots.iter().any(|key| key == object_key)) } fn game_distribution_version_snapshot( @@ -4488,4 +4513,33 @@ mod tests { apply_game_distribution_frozen_metadata(&mut game, &priced); assert_eq!(game.price_mud_points, 100); } + + #[test] + fn media_object_key_game_id_is_derived_without_scanning_all_games() { + const PREFIX: &str = shared_contracts::game_distribution::GAME_DISTRIBUTION_MEDIA_PREFIX; + assert_eq!( + game_distribution_media_object_key_game_id(&format!("{PREFIX}game_abc/cover-1.png")), + Some("game_abc") + ); + assert_eq!( + game_distribution_media_object_key_game_id(&format!( + "{PREFIX}game_abc/screenshot-2.webp" + )), + Some("game_abc") + ); + // 非本前缀、缺 gameId、缺文件名、以及多级路径都不认。 + assert_eq!( + game_distribution_media_object_key_game_id("generated/assets/cover.png"), + None + ); + assert_eq!(game_distribution_media_object_key_game_id(PREFIX), None); + assert_eq!( + game_distribution_media_object_key_game_id(&format!("{PREFIX}game_abc/")), + None + ); + assert_eq!( + game_distribution_media_object_key_game_id(&format!("{PREFIX}/cover-1.png")), + None + ); + } }