Publish app

This commit is contained in:
2025-02-13 15:08:22 +00:00
parent 1122c536a1
commit f1c245971d
4 changed files with 188 additions and 13 deletions

View File

@ -8,29 +8,45 @@ use async_zip::tokio::read::seek::ZipFileReader;
use async_zip::ZipFile;
use log::{debug, info, warn};
use nostr_sdk::async_utility::futures_util::TryStreamExt;
use nostr_sdk::prelude::{hex, StreamExt};
use nostr_sdk::prelude::{hex, Coordinate, StreamExt};
use nostr_sdk::{Event, EventBuilder, Kind, NostrSigner, Tag};
use reqwest::Url;
use semver::Version;
use serde::Deserialize;
use sha2::Digest;
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};
use std::env::temp_dir;
use std::fmt::{write, Display, Formatter};
use std::io::Cursor;
use std::path::{Path, PathBuf};
use tokio::fs::File;
use tokio::io::{AsyncBufRead, AsyncRead, AsyncSeek, AsyncWriteExt, BufReader};
use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt, AsyncSeek, AsyncWriteExt, BufReader};
mod github;
/// Since artifact binary / image
#[derive(Debug, Clone)]
pub struct RepoArtifact {
/// Artifact name (filename)
pub name: String,
/// The size of the artifact in bytes
pub size: u64,
/// Where the artifact is located
pub location: RepoResource,
/// MIME type
pub content_type: String,
/// Platform this artifact runs on
pub platform: Platform,
/// Artifact metadata
pub metadata: ArtifactMetadata,
/// SHA-256 hash of the artifact
pub hash: Option<Vec<u8>>,
}
impl Display for RepoArtifact {
@ -43,6 +59,47 @@ impl Display for RepoArtifact {
}
}
/// Converts a repo artifact into a NIP-94 event
impl TryInto<EventBuilder> for RepoArtifact {
type Error = anyhow::Error;
fn try_into(self) -> Result<EventBuilder, Self::Error> {
let mut b = EventBuilder::new(Kind::FileMetadata, "").tags([
Tag::parse(["f", self.platform.to_string().as_str()])?,
Tag::parse(["m", self.content_type.as_str()])?,
Tag::parse(["size", self.size.to_string().as_str()])?,
]);
if let RepoResource::Remote(u) = self.location {
b = b.tag(Tag::parse(["url", u.as_str()])?);
}
match self.metadata {
ArtifactMetadata::APK { manifest } => {
if let Some(vn) = manifest.version_name {
b = b.tag(Tag::parse(["version", vn.as_str()])?);
}
if let Some(vc) = manifest.version_code {
b = b.tag(Tag::parse(["version_code", vc.to_string().as_str()])?);
}
if let Some(min_sdk) = manifest.sdk.min_sdk_version {
b = b.tag(Tag::parse([
"min_sdk_version",
min_sdk.to_string().as_str(),
])?);
}
if let Some(target_sdk) = manifest.sdk.target_sdk_version {
b = b.tag(Tag::parse([
"target_sdk_version",
target_sdk.to_string().as_str(),
])?);
}
//TODO: apk sig
}
}
Ok(b)
}
}
#[derive(Debug, Clone)]
pub enum ArtifactMetadata {
APK { manifest: AndroidManifest },
}
@ -63,6 +120,7 @@ impl Display for ArtifactMetadata {
}
}
#[derive(Debug, Clone)]
pub enum Platform {
Android { arch: Architecture },
IOS { arch: Architecture },
@ -128,6 +186,7 @@ impl Display for Platform {
}
}
#[derive(Debug, Clone)]
pub enum Architecture {
ARMv7,
ARM64,
@ -146,18 +205,79 @@ impl Display for Architecture {
}
}
#[derive(Debug, Clone)]
/// A local/remote location where the artifact is located
pub enum RepoResource {
Remote(String),
Local(PathBuf),
}
#[derive(Debug, Clone)]
/// A single release with one or more artifacts
pub struct RepoRelease {
/// Release version (semver)
pub version: Version,
/// Release changelog/notes
pub description: Option<String>,
/// URL of the release (github release page etc)
pub url: Option<String>,
/// List of artifacts in this release
pub artifacts: Vec<RepoArtifact>,
}
impl RepoRelease {
pub fn app_id(&self) -> Result<String> {
self.artifacts
.iter()
.find_map(|a| match &a.metadata {
ArtifactMetadata::APK { manifest } if manifest.package.is_some() => {
Some(manifest.package.as_ref().unwrap().to_string())
}
_ => None,
})
.ok_or(anyhow!("no app_id found"))
}
/// [app_id]@[version]
pub fn release_tag(&self) -> Result<String> {
Ok(format!("{}@{}", self.app_id()?, self.version.to_string()))
}
/// Create nostr release artifact list event
pub async fn into_release_list_event<T: NostrSigner>(
self,
signer: &T,
app_coord: Coordinate,
) -> Result<Vec<Event>> {
let mut ret = vec![];
let mut b = EventBuilder::new(
Kind::Custom(30063),
self.description.as_ref().map(|s| s.as_str()).unwrap_or(""),
)
.tags([
Tag::coordinate(app_coord),
Tag::parse(["d", &self.release_tag()?])?,
]);
for a in &self.artifacts {
let eb: Result<EventBuilder> = a.clone().try_into();
match eb {
Ok(a) => {
let e_build = a.sign(signer).await?;
b = b.tag(Tag::event(e_build.id));
ret.push(e_build);
}
Err(e) => warn!("Failed to convert artifact: {} {}", a, e),
}
}
ret.push(b.sign(signer).await?);
Ok(ret)
}
}
/// Generic artifact repository
#[async_trait::async_trait]
pub trait Repo {
@ -205,7 +325,10 @@ async fn load_artifact_url(url: &str) -> Result<RepoArtifact> {
}
}
}
load_artifact(&tmp).await
let mut a = load_artifact(&tmp).await?;
// replace location back to URL for publishing
a.location = RepoResource::Remote(url.to_string());
Ok(a)
}
async fn load_artifact(path: &Path) -> Result<RepoArtifact> {
@ -243,7 +366,8 @@ async fn load_apk_artifact(path: &Path) -> Result<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(),
hash: Some(hash_file(&path).await?),
content_type: "application/vnd.android.package-archive".to_string(),
platform: Platform::Android {
arch: match lib_arch.iter().next().unwrap().as_str() {
"arm64-v8a" => Architecture::ARM64,
@ -257,6 +381,19 @@ async fn load_apk_artifact(path: &Path) -> Result<RepoArtifact> {
})
}
async fn hash_file(path: &Path) -> Result<Vec<u8>> {
let mut file = File::open(path).await?;
let mut hash = Sha256::default();
let mut buf = Vec::with_capacity(4096);
while let Ok(r) = file.read(&mut buf).await {
if r == 0 {
break;
}
hash.update(&buf[..r]);
}
Ok(hash.finalize().to_vec())
}
async fn load_manifest<T>(zip: &mut ZipFileReader<T>) -> Result<AndroidManifest>
where
T: AsyncBufRead + AsyncSeek + Unpin,