try parse manifest

This commit is contained in:
2025-02-12 16:51:55 +00:00
parent 63cff68bdd
commit f90187c914
5 changed files with 304 additions and 232 deletions

View File

@ -20,12 +20,11 @@ struct Args {
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
// Set default log level to info
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
env_logger::init();
let args = Args::parse();

View File

@ -1,4 +1,6 @@
use crate::repo::{Repo, RepoArtifact, RepoRelease, RepoResource};
use crate::repo::{
load_artifact, load_artifact_url, Repo, RepoArtifact, RepoRelease, RepoResource,
};
use anyhow::{anyhow, Result};
use log::{info, warn};
use nostr_sdk::Url;
@ -60,46 +62,6 @@ struct GithubReleaseArtifact {
pub browser_download_url: String,
}
impl TryFrom<&GithubRelease> for RepoRelease {
type Error = anyhow::Error;
fn try_from(value: &GithubRelease) -> std::result::Result<Self, Self::Error> {
Ok(RepoRelease {
version: Version::parse(if value.tag_name.starts_with("v") {
&value.tag_name[1..]
} else {
&value.tag_name
})?,
artifacts: value
.assets
.iter()
.filter_map(|v| match RepoArtifact::try_from(v) {
Ok(art) => Some(art),
Err(e) => {
warn!("Failed to parse artifact {}: {}", &v.name, e);
None
}
})
.collect(),
})
}
}
impl TryFrom<&GithubReleaseArtifact> for RepoArtifact {
type Error = anyhow::Error;
fn try_from(value: &GithubReleaseArtifact) -> std::result::Result<Self, Self::Error> {
Ok(RepoArtifact {
name: value.name.clone(),
size: value.size,
content_type: value.content_type.clone(),
platform: Platform::IOS,
location: RepoResource::Remote(value.browser_download_url.clone()),
metadata: (),
})
}
}
#[async_trait::async_trait]
impl Repo for GithubRepo {
async fn get_releases(&self) -> Result<Vec<RepoRelease>> {
@ -115,16 +77,33 @@ impl Repo for GithubRepo {
))
.build()?;
let rsp: Vec<GithubRelease> = self.client.execute(req).await?.json().await?;
Ok(rsp
.into_iter()
.filter_map(|v| match RepoRelease::try_from(&v) {
Ok(r) => Some(r),
Err(e) => {
warn!("Failed to parse release: {} {}", v.tag_name, e);
None
let gh_release: Vec<GithubRelease> = self.client.execute(req).await?.json().await?;
let mut releases = vec![];
for release in gh_release {
let mut artifacts = vec![];
for gh_artifact in release.assets {
match load_artifact_url(&gh_artifact.browser_download_url).await {
Ok(a) => artifacts.push(a),
Err(e) => warn!(
"Failed to load artifact {}: {}",
gh_artifact.browser_download_url, e
),
}
})
.collect())
}
if artifacts.is_empty() {
warn!("No artifacts found for {}", release.tag_name);
continue;
}
releases.push(RepoRelease {
version: Version::parse(if release.tag_name.starts_with("v") {
&release.tag_name[1..]
} else {
&release.tag_name
})?,
artifacts,
});
}
Ok(releases)
}
}

View File

@ -1,9 +1,20 @@
use std::path::{Path, PathBuf};
use crate::manifest::Manifest;
use crate::repo::github::GithubRepo;
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail, Context, Result};
use apk::AndroidManifest;
use async_zip::tokio::read::seek::ZipFileReader;
use async_zip::ZipFile;
use log::info;
use nostr_sdk::async_utility::futures_util::TryStreamExt;
use nostr_sdk::prelude::{hex, StreamExt};
use reqwest::Url;
use semver::Version;
use serde::Deserialize;
use sha2::Digest;
use std::env::temp_dir;
use std::path::{Path, PathBuf};
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufReader};
mod github;
@ -14,16 +25,11 @@ pub struct RepoArtifact {
pub location: RepoResource,
pub content_type: String,
pub platform: Platform,
pub metadata: ArtifactMetadata
pub metadata: ArtifactMetadata,
}
pub enum ArtifactMetadata {
APK {
version_code: u32,
min_sdk_version: u32,
target_sdk_version: u32,
sig_hash: String,
}
APK { manifest: AndroidManifest },
}
pub enum Platform {
@ -79,10 +85,78 @@ impl TryInto<Box<dyn Repo>> for &Manifest {
}
}
/// Download an artifact and create a [RepoArtifact]
async fn load_artifact_url(url: &str) -> Result<RepoArtifact> {
info!("Downloading artifact {}", url);
let u = Url::parse(url)?;
let rsp = reqwest::get(u.clone()).await?;
let id = hex::encode(sha2::Sha256::digest(url.as_bytes()));
let mut tmp = temp_dir().join(id);
tmp.set_extension(
PathBuf::from(u.path())
.extension()
.ok_or(anyhow!("Missing extension in URL"))?
.to_str()
.unwrap(),
);
if !tmp.exists() {
let mut tmp_file = File::create(&tmp).await?;
let mut rsp_stream = rsp.bytes_stream();
while let Some(data) = rsp_stream.next().await {
if let Ok(data) = data {
tmp_file.write_all(&data).await?;
}
}
}
load_artifact(&tmp).await
}
async fn load_artifact(path: &Path) -> Result<RepoArtifact> {
match path
.extension()
.ok_or(anyhow!("missing file extension"))?
.to_str()
.unwrap()
{
"apk" => load_apk_artifact(path).await,
_ => bail!("unknown file extension"),
}
}
}
async fn load_apk_artifact(path: &Path) -> Result<RepoArtifact> {
let file = File::open(path).await?;
let mut zip = ZipFileReader::with_tokio(BufReader::new(file)).await?;
const ANDROID_MANIFEST: &'static str = "AndroidManifest.xml";
let idx = zip
.file()
.entries()
.iter()
.enumerate()
.find_map(|(i, entry)| {
if entry.filename().as_bytes() == ANDROID_MANIFEST.as_bytes() {
Some(i)
} else {
None
}
})
.ok_or(anyhow!("missing AndroidManifest file"))?;
let mut manifest = zip.reader_with_entry(idx).await?;
let mut manifest_data = String::with_capacity(8192);
manifest.read_to_string_checked(&mut manifest_data).await?;
info!("Successfully loaded AndroidManifest: {}", &manifest_data);
let manifest: AndroidManifest = quick_xml::de::from_str(&manifest_data)?;
Ok(RepoArtifact {
name: path.file_name().unwrap().to_str().unwrap().to_string(),
size: path.metadata()?.len(),
location: RepoResource::Local(path.to_path_buf()),
content_type: "application/apk".to_string(),
platform: Platform::Android {
arch: Architecture::ARMv8,
},
metadata: ArtifactMetadata::APK { manifest },
})
}