mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
fix(desktop): deduplicate macOS release feeds (#2056)
This commit is contained in:
@@ -6,7 +6,7 @@ import {isJsonRecord} from '../utils/JsonBoundaryUtils';
|
||||
const DESKTOP_BUCKET_PREFIX = 'desktop';
|
||||
const MIN_RELEASE_ROUTE_COUNT = 28;
|
||||
const MAX_RELEASE_ROUTE_COUNT = 128;
|
||||
const MIN_RELEASE_ASSET_COUNT = 26;
|
||||
const MIN_RELEASE_ASSET_COUNT = 24;
|
||||
|
||||
interface DesktopReleaseAsset {
|
||||
storage_key: string;
|
||||
@@ -101,12 +101,14 @@ export function parseDesktopReleaseDescriptor(value: unknown): DesktopReleaseDes
|
||||
const expectedTag = `fluxer-desktop-${value.channel}@${value.version}`;
|
||||
const expectedStoragePrefix = `desktop/${value.channel}/`;
|
||||
const expectedReleasePrefix = `${value.channel === 'canary' ? 'Fluxer-Canary' : 'Fluxer'}-${value.version}-`;
|
||||
const descriptorName = `${expectedReleasePrefix}release-manifest.json`;
|
||||
if (value.release_tag !== expectedTag) {
|
||||
return null;
|
||||
}
|
||||
const storageKeys = new Set<string>();
|
||||
const routeCounts = new Map<string, number>();
|
||||
const releaseAssets = new Map<string, {sha256: string; size: number}>();
|
||||
const releaseAssetNames = new Map<string, string>([[descriptorName.toLowerCase(), descriptorName]]);
|
||||
const assets: Array<DesktopReleaseAsset> = [];
|
||||
for (const rawAsset of value.assets) {
|
||||
const asset = parseDesktopReleaseAsset(rawAsset);
|
||||
@@ -121,12 +123,23 @@ export function parseDesktopReleaseDescriptor(value: unknown): DesktopReleaseDes
|
||||
storageKeys.add(asset.storage_key);
|
||||
const [, , platform, arch, filename] = asset.storage_key.split('/');
|
||||
const platformToken = platform === 'win32' ? 'win' : platform === 'darwin' ? 'mac' : 'linux';
|
||||
const releaseFilename =
|
||||
platform === 'darwin' && filename.toLowerCase() === 'releases.json' ? 'releases.json' : filename;
|
||||
const expectedReleaseAsset = filename.startsWith(expectedReleasePrefix)
|
||||
? filename
|
||||
: `${expectedReleasePrefix}${platformToken}-${arch}-${filename}`;
|
||||
if (asset.release_asset !== expectedReleaseAsset) {
|
||||
: `${expectedReleasePrefix}${platformToken}-${arch}-${releaseFilename}`;
|
||||
if (
|
||||
asset.release_asset !== expectedReleaseAsset ||
|
||||
asset.release_asset.toLowerCase() === descriptorName.toLowerCase()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const caseFoldedReleaseAsset = asset.release_asset.toLowerCase();
|
||||
const existingReleaseAssetName = releaseAssetNames.get(caseFoldedReleaseAsset);
|
||||
if (existingReleaseAssetName && existingReleaseAssetName !== asset.release_asset) {
|
||||
return null;
|
||||
}
|
||||
releaseAssetNames.set(caseFoldedReleaseAsset, asset.release_asset);
|
||||
const scope = `${platform}/${arch}`;
|
||||
routeCounts.set(scope, (routeCounts.get(scope) ?? 0) + 1);
|
||||
const existing = releaseAssets.get(asset.release_asset);
|
||||
|
||||
+18
-15
@@ -12,7 +12,7 @@ use crate::common::{
|
||||
use crate::functions::write_json_pretty;
|
||||
use crate::release::{
|
||||
DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION, DesktopReleaseAsset, DesktopReleaseDescriptor,
|
||||
desktop_release_descriptor_filename, desktop_release_product,
|
||||
desktop_release_asset_name, desktop_release_descriptor_filename, desktop_release_product,
|
||||
validate_desktop_release_descriptor,
|
||||
};
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
@@ -3781,6 +3781,7 @@ struct DesktopReleaseAssetBuilder<'a> {
|
||||
descriptor_assets: Vec<DesktopReleaseAsset>,
|
||||
storage_keys: BTreeSet<String>,
|
||||
release_asset_content: BTreeMap<String, (String, u64)>,
|
||||
release_asset_names: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl<'a> DesktopReleaseAssetBuilder<'a> {
|
||||
@@ -3800,6 +3801,7 @@ impl<'a> DesktopReleaseAssetBuilder<'a> {
|
||||
descriptor_assets: Vec::new(),
|
||||
storage_keys: BTreeSet::new(),
|
||||
release_asset_content: BTreeMap::new(),
|
||||
release_asset_names: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3810,21 +3812,13 @@ impl<'a> DesktopReleaseAssetBuilder<'a> {
|
||||
source.display()
|
||||
);
|
||||
let source_name = file_name_string(source)?;
|
||||
let platform_token = match platform {
|
||||
"win32" => "win",
|
||||
"darwin" => "mac",
|
||||
"linux" => "linux",
|
||||
other => bail!("Unsupported desktop release platform {other:?}"),
|
||||
};
|
||||
let canonical_prefix = format!("{}-{}-", self.product, self.version);
|
||||
let release_asset = if qualify_name && !source_name.starts_with(&canonical_prefix) {
|
||||
format!(
|
||||
"{}-{}-{platform_token}-{arch}-{source_name}",
|
||||
self.product, self.version
|
||||
)
|
||||
} else {
|
||||
source_name.clone()
|
||||
};
|
||||
ensure!(
|
||||
qualify_name || source_name.starts_with(&canonical_prefix),
|
||||
"Shipped desktop artifact name is not canonical: {source_name:?}"
|
||||
);
|
||||
let release_asset =
|
||||
desktop_release_asset_name(self.channel, self.version, platform, arch, &source_name)?;
|
||||
ensure!(
|
||||
release_asset.starts_with(&canonical_prefix)
|
||||
&& release_asset.bytes().all(|byte| {
|
||||
@@ -3832,6 +3826,15 @@ impl<'a> DesktopReleaseAssetBuilder<'a> {
|
||||
}),
|
||||
"Desktop release asset name is not canonical and URL-safe: {release_asset:?}"
|
||||
);
|
||||
if let Some(existing) = self
|
||||
.release_asset_names
|
||||
.insert(release_asset.to_ascii_lowercase(), release_asset.clone())
|
||||
{
|
||||
ensure!(
|
||||
existing == release_asset,
|
||||
"Desktop release asset names differ only by case: {existing:?} and {release_asset:?}"
|
||||
);
|
||||
}
|
||||
let storage_key = format!(
|
||||
"{}/{}/{platform}/{arch}/{source_name}",
|
||||
self.s3_prefix, self.channel
|
||||
|
||||
+61
-15
@@ -15,7 +15,7 @@ const RELEASE_REPOSITORY: &str = "fluxerapp/fluxer";
|
||||
const RELEASE_COMPARE_URL: &str = "https://github.com/fluxerapp/fluxer/compare";
|
||||
pub(crate) const DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION: u8 = 1;
|
||||
pub(crate) const DESKTOP_RELEASE_ROUTE_COUNT: usize = 28;
|
||||
pub(crate) const DESKTOP_RELEASE_ASSET_COUNT: usize = 26;
|
||||
pub(crate) const DESKTOP_RELEASE_ASSET_COUNT: usize = 24;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct DesktopReleaseAsset {
|
||||
@@ -50,6 +50,38 @@ pub(crate) fn desktop_release_descriptor_filename(channel: &str, version: &str)
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn desktop_release_asset_name(
|
||||
channel: &str,
|
||||
version: &str,
|
||||
platform: &str,
|
||||
arch: &str,
|
||||
storage_filename: &str,
|
||||
) -> Result<String> {
|
||||
let release_prefix = format!("{}-{version}-", desktop_release_product(channel)?);
|
||||
let platform_token = match platform {
|
||||
"win32" => "win",
|
||||
"darwin" => "mac",
|
||||
"linux" => "linux",
|
||||
other => bail!("Unsupported desktop release platform {other:?}"),
|
||||
};
|
||||
ensure!(
|
||||
matches!(arch, "x64" | "arm64"),
|
||||
"Unsupported desktop release architecture {arch:?}"
|
||||
);
|
||||
if storage_filename.starts_with(&release_prefix) {
|
||||
return Ok(storage_filename.to_string());
|
||||
}
|
||||
let release_filename =
|
||||
if platform == "darwin" && storage_filename.eq_ignore_ascii_case("releases.json") {
|
||||
"releases.json"
|
||||
} else {
|
||||
storage_filename
|
||||
};
|
||||
Ok(format!(
|
||||
"{release_prefix}{platform_token}-{arch}-{release_filename}"
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_desktop_release_descriptor(
|
||||
descriptor: &DesktopReleaseDescriptor,
|
||||
channel: &str,
|
||||
@@ -101,6 +133,10 @@ pub(crate) fn validate_desktop_release_descriptor(
|
||||
let mut storage_keys = BTreeSet::new();
|
||||
let mut route_counts = BTreeMap::<String, usize>::new();
|
||||
let mut release_assets = BTreeMap::<&str, (&str, u64)>::new();
|
||||
let mut release_asset_names = BTreeMap::from([(
|
||||
descriptor_name.to_ascii_lowercase(),
|
||||
descriptor_name.as_str(),
|
||||
)]);
|
||||
for asset in &descriptor.assets {
|
||||
ensure!(
|
||||
storage_keys.insert(asset.storage_key.as_str()),
|
||||
@@ -125,20 +161,13 @@ pub(crate) fn validate_desktop_release_descriptor(
|
||||
*route_counts
|
||||
.entry(format!("{}/{}", key_segments[2], key_segments[3]))
|
||||
.or_default() += 1;
|
||||
let platform_token = match key_segments[2] {
|
||||
"win32" => "win",
|
||||
"darwin" => "mac",
|
||||
"linux" => "linux",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let expected_release_asset = if key_segments[4].starts_with(&release_prefix) {
|
||||
key_segments[4].to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{release_prefix}{platform_token}-{}-{}",
|
||||
key_segments[3], key_segments[4]
|
||||
)
|
||||
};
|
||||
let expected_release_asset = desktop_release_asset_name(
|
||||
channel,
|
||||
version,
|
||||
key_segments[2],
|
||||
key_segments[3],
|
||||
key_segments[4],
|
||||
)?;
|
||||
ensure!(
|
||||
asset.release_asset.starts_with(&release_prefix)
|
||||
&& asset.release_asset != descriptor_name
|
||||
@@ -149,6 +178,16 @@ pub(crate) fn validate_desktop_release_descriptor(
|
||||
"Desktop release descriptor contains invalid release asset {:?}",
|
||||
asset.release_asset
|
||||
);
|
||||
if let Some(existing) = release_asset_names.insert(
|
||||
asset.release_asset.to_ascii_lowercase(),
|
||||
asset.release_asset.as_str(),
|
||||
) {
|
||||
ensure!(
|
||||
existing == asset.release_asset,
|
||||
"Desktop release asset names differ only by case: {existing:?} and {:?}",
|
||||
asset.release_asset
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
asset.sha256.len() == 64
|
||||
&& asset
|
||||
@@ -613,6 +652,7 @@ fn local_release_assets(
|
||||
);
|
||||
|
||||
let mut assets = Vec::with_capacity(entries.len());
|
||||
let mut case_folded_names = BTreeMap::<String, String>::new();
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
let metadata = fs::symlink_metadata(&path)
|
||||
@@ -640,6 +680,12 @@ fn local_release_assets(
|
||||
name.starts_with(&prefix),
|
||||
"Release asset {name:?} must start with {prefix:?}"
|
||||
);
|
||||
if let Some(existing) = case_folded_names.insert(name.to_ascii_lowercase(), name.clone()) {
|
||||
ensure!(
|
||||
existing == name,
|
||||
"Release asset names differ only by case: {existing:?} and {name:?}"
|
||||
);
|
||||
}
|
||||
assets.push(LocalReleaseAsset {
|
||||
digest: sha256_file(&path)?,
|
||||
path,
|
||||
|
||||
Reference in New Issue
Block a user