This commit is contained in:
Caelan Sayler
2025-02-06 20:50:51 +00:00
parent 0557af0e30
commit 7ca98d1f95
2 changed files with 131 additions and 16 deletions

View File

@@ -91,7 +91,7 @@ namespace Velopack.Sources
protected override async Task<GithubRelease[]> GetReleases(bool includePrereleases)
{
// https://docs.github.com/en/rest/reference/releases
const int perPage = 10;
const int perPage = 30;
const int page = 1;
var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}";
var baseUri = GetApiBaseUrl(RepoUri);

View File

@@ -1,46 +1,161 @@
use serde::{Deserialize, Serialize};
use super::{bundle, download, Error, UpdateSource, VelopackAsset, VelopackAssetFeed};
use serde::{Deserialize, Serialize};
use std::sync::mpsc::Sender;
use url::Url;
#[derive(Debug, Serialize, Deserialize)]
pub struct GithubRelease {
struct GithubRelease {
/// The name of this release.
#[serde(default)]
#[serde(rename = "name")]
pub name: Option<String>,
pub name: String,
/// True if this release is a prerelease.
#[serde(default)]
#[serde(rename = "prerelease")]
pub prerelease: bool,
/// The date which this release was published publicly.
///
/// We use `chrono` with an RFC 3339 parser. If your JSON has a date/time
/// in RFC 3339 format (e.g., `"2021-01-01T12:34:56Z"`), then this works.
/// Otherwise, you can remove the `with` attribute and store it as a string.
#[serde(rename = "published_at", with = "chrono::serde::ts_rfc3339_option")]
pub published_at: Option<DateTime<Utc>>,
#[serde(default)]
#[serde(rename = "published_at")]
pub published_at: String,
/// A list of assets (files) uploaded to this release.
#[serde(default)]
#[serde(rename = "assets")]
pub assets: Vec<GithubReleaseAsset>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GithubReleaseAsset {
struct GithubReleaseAsset {
/// The asset URL for this release asset.
#[serde(default)]
#[serde(rename = "url")]
pub url: Option<String>,
pub url: String,
/// The browser URL for this release asset.
#[serde(default)]
#[serde(rename = "browser_download_url")]
pub browser_download_url: Option<String>,
pub browser_download_url: String,
/// The name of this release asset.
#[serde(default)]
#[serde(rename = "name")]
pub name: Option<String>,
pub name: String,
/// The mime type of this release asset.
#[serde(default)]
#[serde(rename = "content_type")]
pub content_type: Option<String>,
pub content_type: String,
}
#[derive(Clone)]
pub struct GithubSource {
url: String,
access_token: Option<String>,
prerelease: bool,
}
impl GithubSource {
/// Create a new GithubSource with the specified repository URL.
pub fn new<S1: AsRef<str>>(url: S1, prerelease: bool, access_token: Option<&str>) -> Self {
GithubSource { url: url.as_ref().to_string(), prerelease: prerelease, access_token: access_token.map(|s| s.to_string()) }
}
fn get_api_base_url(&self) -> Result<String, Error> {
// https://github.com/velopack/velopack/blob/23d27db4b5147a650e24673eaadfe832db50c567/src/Velopack/Sources/GithubSource.cs#L135
let base_url: String;
let url = Url::parse(&self.url)?;
// handle 2 cases:
// if github.com url
if url.host_str() == Some("github.com") {
base_url = String::from("https://api.github.com/");
} else {
// if not github.com url, it's probably an enterprise server
base_url = format!("{}://{}/api/v3/", url.scheme(), url.host_str().unwrap());
}
return Ok(base_url);
}
}
impl UpdateSource for GithubSource {
fn get_release_feed(&self, channel: &str, _: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
let per_page = 30;
let page = 1;
let url = Url::parse(&self.url)?;
let releases_path = format!("repos{}/releases?per_page{per_page}&page={page}", url.path().trim_end_matches('/'));
let base_path = self.get_api_base_url()?;
let get_releases_uri = format!("{base_path}{releases_path}");
let response = download::download_url_as_string(&get_releases_uri)?;
let releases: Vec<GithubRelease> = serde_json::from_str(&response)?;
let releases_name = format!("releases.{channel}.json");
let latest_release_gh_asset: &GithubReleaseAsset = releases
.iter()
.filter(|release| !release.prerelease)
.flat_map(|release| &release.assets)
.filter(|asset| asset.name == releases_name)
.next()
.ok_or(Error::FileNotFound(releases_name))?;
let response = download::download_url_as_string(&latest_release_gh_asset.browser_download_url)?;
let velopack_asset: VelopackAssetFeed = serde_json::from_str(&response)?;
println!("{releases:#?}");
Ok(velopack_asset)
}
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
let url = Url::parse(&self.url)?;
let host = url.host_str().unwrap().trim_end_matches('/');
let path = url.path().trim_end_matches('/');
let asset_url = format!("{}{}/releases/download/{}/{}", host, path, asset.Version, asset.FileName);
println!("{asset_url}");
info!("About to download from URL '{}' to file '{}'", asset_url, local_file);
download::download_url_to_file(asset_url.as_str(), local_file, progress)?;
Ok(())
}
fn clone_boxed(&self) -> Box<dyn UpdateSource> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod test {
use super::*;
use bundle::Manifest;
#[test]
fn get_github_api_base_url() {
let normal_gh_url = "https://github.com/velopack/velopack/";
let enterprise_gh_url = "http://internal.github.server.local/";
let normal_gh_source = GithubSource::new(normal_gh_url, false, None);
let enterprise_gh_source = GithubSource::new(enterprise_gh_url, false, None);
assert_eq!("https://api.github.com/", normal_gh_source.get_api_base_url().unwrap());
assert_eq!("http://internal.github.server.local/api/v3/", enterprise_gh_source.get_api_base_url().unwrap());
}
#[test]
fn get_release_feed() {
let normal_gh_url = "https://github.com/caesay/VelopackHackathonTest";
let normal_gh_source = GithubSource::new(normal_gh_url, false, None);
let _app = Manifest::default();
//write a better assert
println!("{:#?}", normal_gh_source.get_release_feed("win", &_app).unwrap());
}
#[test]
fn get_release_entry() {
let normal_gh_url = "https://github.com/caesay/VelopackHackathonTest";
let normal_gh_source = GithubSource::new(normal_gh_url, false, None);
let _app = Manifest::default();
let asset = normal_gh_source.get_release_feed("win", &_app).unwrap();
normal_gh_source.download_release_entry(&asset.Assets[0], "C:\\Users\\user\\Documents\\velopack.fusion\\for-rust", None);
}
}