upstream-rs 0.5.5

Fetch package updates directly from the source.
use std::fs;
use std::path::{Path, PathBuf};

use crate::models::common::enums::{Channel, Filetype, Provider};
use crate::models::provider::{Asset, Release};
use crate::models::upstream::Package;
use crate::services::providers::github::{GithubAdapter, GithubClient};
use crate::utils::platform_info::{ArchitectureInfo, CpuArch, format_arch, format_os};

use anyhow::{Result, anyhow};

pub struct ProviderManager {
    github: GithubAdapter,
    architecture_info: ArchitectureInfo,
}

impl ProviderManager {
    pub fn new(github_token: Option<&str>) -> Result<Self> {
        let architecture_info = ArchitectureInfo::new();
        let github_client = GithubClient::new(github_token);
        let github = GithubAdapter::new(github_client?);
        Ok(Self {
            github,
            architecture_info,
        })
    }

    pub async fn get_latest_release(&self, slug: &str, provider: &Provider) -> Result<Release> {
        match provider {
            Provider::Github => self.github.get_latest_release(slug).await,
        }
    }

    pub async fn get_all_releases(
        &self,
        slug: &str,
        provider: Provider,
        per_page: Option<u32>,
    ) -> Result<Vec<Release>> {
        match provider {
            Provider::Github => self.github.get_all_releases(slug, per_page).await,
        }
    }

    pub async fn get_release_by_tag(
        &self,
        slug: &str,
        tag: &str,
        provider: &Provider,
    ) -> Result<Release> {
        match provider {
            Provider::Github => self.github.get_release_by_tag(slug, tag).await,
        }
    }

    pub async fn download_asset<F>(
        &self,
        asset: &Asset,
        provider: &Provider,
        cache_path: &Path,
        dl_progress: &mut Option<F>,
    ) -> Result<PathBuf>
    where
        F: FnMut(u64, u64),
    {
        let file_name = Path::new(&asset.name)
            .file_name()
            .ok_or_else(|| anyhow!("Invalid asset name: {}", asset.name))?;

        fs::create_dir_all(cache_path)?;

        let download_filepath = cache_path.join(file_name);

        match provider {
            Provider::Github => {
                self.github
                    .download_asset(asset, &download_filepath, dl_progress)
                    .await?;
            }
        }

        Ok(download_filepath)
    }

    pub fn find_recommended_asset(&self, release: &Release, package: &Package) -> Result<Asset> {
        let target_filetype = if package.filetype == Filetype::Auto {
            Self::resolve_auto_filetype(release)?
        } else {
            package.filetype
        };

        let compatible_assets: Vec<&Asset> = release
            .assets
            .iter()
            .filter(|a| self.is_potentially_compatible(a))
            .filter(|a| a.filetype == target_filetype)
            .collect();

        compatible_assets
            .into_iter()
            .max_by_key(|a| self.score_asset(a, package))
            .cloned()
            .ok_or_else(|| {
                anyhow!(
                    "No compatible assets found for {} on {}",
                    format_arch(&self.architecture_info.cpu_arch),
                    format_os(&self.architecture_info.os_kind)
                )
            })
    }

    pub fn resolve_auto_filetype(release: &Release) -> Result<Filetype> {
        let priority = [
            Filetype::AppImage,
            Filetype::Archive,
            Filetype::Compressed,
            Filetype::Binary,
        ];

        priority
            .iter()
            .find(|&&filetype| {
                release.assets.iter().any(|asset| asset.filetype == filetype)
            })
            .copied()
            .ok_or_else(|| anyhow!("No compatible filetype found in release assets"))
    }

    // TODO: implement
    fn is_valid_update(package: &Package, release: &Release) -> bool {
        if package.is_pinned {
            return false;
        }

        let consider_release = match package.channel {
            Channel::Stable => !release.is_draft && !release.is_prerelease,
            Channel::Beta | Channel::Nightly => !release.is_draft,
            Channel::All => true,
        };

        consider_release && release.version.is_newer_than(&package.version)
    }

    fn is_potentially_compatible(&self, asset: &Asset) -> bool {
        // OS check
        if let Some(target_os) = &asset.target_os
            && *target_os != self.architecture_info.os_kind
        {
            return false;
        }

        // Architecture check
        if let Some(target_arch) = &asset.target_arch {
            if *target_arch == self.architecture_info.cpu_arch {
                return true;
            }

            // Compatibility fallbacks
            if self.architecture_info.cpu_arch == CpuArch::X86_64 && *target_arch == CpuArch::X86 {
                return true;
            }

            if self.architecture_info.cpu_arch == CpuArch::Aarch64 && *target_arch == CpuArch::Arm {
                return true;
            }

            return *target_arch == self.architecture_info.cpu_arch;
        }

        true
    }

    fn score_asset(&self, asset: &Asset, package: &Package) -> i32 {
        let name = asset.name.to_lowercase();
        let mut score = 0;

        // Architecture match bonus
        if let Some(target_arch) = &asset.target_arch {
            if *target_arch == self.architecture_info.cpu_arch {
                score += 80;
            } else if self.architecture_info.cpu_arch == CpuArch::X86_64
                && *target_arch == CpuArch::X86
            {
                score += 30;
            } else if self.architecture_info.cpu_arch == CpuArch::Aarch64
                && *target_arch == CpuArch::Arm
            {
                score += 30;
            }
        }

        // Archive format preference
        if asset.filetype == Filetype::Archive {
            if name.ends_with(".tar.bz2") || name.ends_with(".tbz") {
                score += 15;
            } else if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
                score += 10;
            } else if name.ends_with(".zip") {
                score += 5;
            }
        }

        // Compression format preference
        if asset.filetype == Filetype::Compressed {
            if name.ends_with(".bz2") {
                score += 10;
            } else if name.ends_with(".gz") {
                score += 5;
            }
        }

        // Binary format preference
        if asset.filetype == Filetype::Binary {
            if Path::new(&name).extension().is_none() {
                score += 10;
            }
        }

        if name.contains("static") {
            score += 5;
        }

        if name.contains("debug") || name.contains("symbols") {
            score -= 20;
        }

        // Package name match
        if !name.contains(&package.name.to_lowercase()) {
            score -= 40;
        }

        // Very small files, or absurdly large files
        if asset.size < 100_000 || asset.size > 500_000_000 {
            score -= 20;
        }

        // TODO: store assethint in package to reuse for upgrade
        if let Some(asset_hint) = &package.pattern {
            if name.contains(asset_hint) {
                score += 50;
            }
        }

        score
    }
}