fj/src/api/release.rs

164 lines
4.9 KiB
Rust
Raw Normal View History

use anyhow::Result;
use chrono::{DateTime, Utc};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::client::Client;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Release {
pub id: u64,
pub tag_name: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub body: String,
pub target_commitish: String,
pub draft: bool,
pub prerelease: bool,
pub html_url: String,
pub created_at: DateTime<Utc>,
pub published_at: Option<DateTime<Utc>>,
#[serde(default)]
pub assets: Vec<Asset>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Asset {
pub id: u64,
pub name: String,
pub size: u64,
pub browser_download_url: String,
pub created_at: DateTime<Utc>,
}
pub async fn list(client: &Client, owner: &str, name: &str, limit: u32) -> Result<Vec<Release>> {
let path = format!("/api/v1/repos/{owner}/{name}/releases");
let query = vec![
("limit".into(), limit.clamp(1, 50).to_string()),
("page".into(), "1".into()),
];
client.json(Method::GET, &path, &query, None::<&()>).await
}
pub async fn get_by_tag(client: &Client, owner: &str, name: &str, tag: &str) -> Result<Release> {
let path = format!("/api/v1/repos/{owner}/{name}/releases/tags/{tag}");
client.json(Method::GET, &path, &[], None::<&()>).await
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateRelease<'a> {
pub tag_name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_commitish: Option<&'a str>,
#[serde(default)]
pub draft: bool,
#[serde(default)]
pub prerelease: bool,
}
pub async fn create(
client: &Client,
owner: &str,
name: &str,
body: &CreateRelease<'_>,
) -> Result<Release> {
let path = format!("/api/v1/repos/{owner}/{name}/releases");
client.json(Method::POST, &path, &[], Some(body)).await
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct EditRelease<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub tag_name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerelease: Option<bool>,
}
pub async fn edit(
client: &Client,
owner: &str,
name: &str,
id: u64,
body: &EditRelease<'_>,
) -> Result<Release> {
let path = format!("/api/v1/repos/{owner}/{name}/releases/{id}");
client.json(Method::PATCH, &path, &[], Some(body)).await
}
pub async fn delete(client: &Client, owner: &str, name: &str, id: u64) -> Result<()> {
let path = format!("/api/v1/repos/{owner}/{name}/releases/{id}");
let res = client.request(Method::DELETE, &path, &[], None).await?;
res.error_for_status()?;
Ok(())
}
/// Upload an asset (binary) to a release.
pub async fn upload_asset(
client: &Client,
owner: &str,
name: &str,
release_id: u64,
asset_name: &str,
bytes: Vec<u8>,
) -> Result<Asset> {
let path = format!(
"/api/v1/repos/{owner}/{name}/releases/{release_id}/assets?name={}",
urlencode(asset_name)
);
// Build a multipart form with the file payload.
let url = client.url(&path)?;
let part = reqwest::multipart::Part::bytes(bytes)
.file_name(asset_name.to_string())
.mime_str("application/octet-stream")?;
let form = reqwest::multipart::Form::new().part("attachment", part);
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("token {}", client.token()))?,
);
let res = client
.http()
.post(url)
.headers(headers)
.multipart(form)
.send()
.await?;
let res = res.error_for_status()?;
Ok(res.json().await?)
}
#[allow(dead_code)]
pub async fn delete_asset(client: &Client, owner: &str, name: &str, asset_id: u64) -> Result<()> {
let path = format!("/api/v1/repos/{owner}/{name}/releases/assets/{asset_id}");
let res = client.request(Method::DELETE, &path, &[], None).await?;
res.error_for_status()?;
Ok(())
}
fn urlencode(s: &str) -> String {
// The asset name appears in a query parameter; URL-encode special chars
// conservatively. `url` crate provides a Serializer for full forms.
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~') {
out.push(c);
} else {
for b in c.to_string().bytes() {
out.push_str(&format!("%{b:02X}"));
}
}
}
out
}