252 lines
8.1 KiB
Rust
252 lines
8.1 KiB
Rust
use std::borrow::Cow;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use miette::{IntoDiagnostic, Result, WrapErr};
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
use crate::Context;
|
|
|
|
/// A sequential pipeline of [`Step`]s.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Pipeline<'a> {
|
|
name: &'a str,
|
|
steps: Vec<Step<'a>>,
|
|
}
|
|
|
|
impl<'a> Pipeline<'a> {
|
|
#[inline(always)]
|
|
pub fn new(name: &'a str, steps: impl Into<Vec<Step<'a>>>) -> Self {
|
|
Self { name, steps: steps.into() }
|
|
}
|
|
|
|
pub async fn invoke(&self, ctx: &Context) -> Result<()> {
|
|
println!("Invoking {}...", self.name);
|
|
|
|
for step in self.steps.iter() {
|
|
step.invoke(ctx).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Step<'a> {
|
|
DownloadFile {
|
|
/// Remote resourcee to download from.
|
|
res: RemoteResource<'a>,
|
|
|
|
/// Path to save the downloaded file to.
|
|
file: Cow<'a, Path>,
|
|
},
|
|
|
|
/// Extracts the file using the `tar` binary that ships with Windows.
|
|
ExtractFile {
|
|
file: Cow<'a, Path>,
|
|
dest: Cow<'a, Path>,
|
|
},
|
|
|
|
ExecuteCommand {
|
|
/// Path to the executable file.
|
|
file: Cow<'a, Path>,
|
|
|
|
args: &'a [&'a str],
|
|
},
|
|
|
|
CreateShortcut {
|
|
/// Target of the shortcut (i.e. what is points to).
|
|
target: ShortcutTarget<'a>,
|
|
|
|
/// Optional icon of the shortcut.
|
|
icon: Option<Cow<'a, Path>>,
|
|
|
|
/// Path of the created shortcut file.
|
|
file: Cow<'a, Path>,
|
|
},
|
|
}
|
|
|
|
impl<'a> Step<'a> {
|
|
pub async fn invoke(&self, ctx: &Context) -> Result<()> {
|
|
match self {
|
|
Self::DownloadFile { res, file } => {
|
|
println!("Downloading file {file} from {res:?}...", file = file.to_str().unwrap_or("<NON UTF-8>"));
|
|
const FETCH_FILE_ERROR_MSG: &'static str = "Fetching the remote resource failed.";
|
|
const WRITE_FILE_ERROR_MSG: &'static str =
|
|
"Writing the remote resource to disk failed.";
|
|
let url = match res {
|
|
RemoteResource::Url(url) => url::Url::parse(url)
|
|
.into_diagnostic()
|
|
.wrap_err("Invalid url for download step.")?,
|
|
RemoteResource::GitHubArtifact { repo, pattern } => {
|
|
const FETCH_META_ERROR_MSG: &'static str =
|
|
"Fetching the latest release metadata from GitHub failed.";
|
|
let url = url::Url::parse(&format!(
|
|
"https://api.github.com/repos/{repo}/releases/latest"
|
|
))
|
|
.into_diagnostic()
|
|
.wrap_err("Invalid GitHub repo for download step.")?;
|
|
let mut release: GitHubRelease = ctx.reqwest.get(url)
|
|
.send()
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(FETCH_META_ERROR_MSG)?
|
|
.json()
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(FETCH_META_ERROR_MSG)?;
|
|
let pattern = ramhorns::Template::new(*pattern)
|
|
.into_diagnostic()
|
|
.wrap_err("Invalid pattern for artifact matching in download step.")?;
|
|
release.meta.tag_name_strip_prefix = release
|
|
.meta
|
|
.tag_name
|
|
.strip_prefix('v')
|
|
.unwrap_or(&release.meta.tag_name);
|
|
let asset_name = pattern.render(&release.meta);
|
|
let artifact = release.assets.into_iter().filter(move |asset| asset.name == asset_name).next().ok_or_else(|| miette!("No artifact of the latest release matched the pattern in download step."))?;
|
|
url::Url::parse(&artifact.browser_download_url)
|
|
.into_diagnostic()
|
|
.wrap_err(
|
|
"Invalid url returned by GitHub for latest release artifact.",
|
|
)?
|
|
}
|
|
};
|
|
let mut resp = ctx.reqwest.get(url)
|
|
.send()
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(FETCH_FILE_ERROR_MSG)?;
|
|
let _content_length = resp.content_length();
|
|
mkdir_all(file.parent().ok_or_else(|| miette!("Destination file for download step has no parent."))?).await?;
|
|
let mut writer = tokio::io::BufWriter::new(
|
|
tokio::fs::File::create(file)
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(WRITE_FILE_ERROR_MSG)?,
|
|
);
|
|
while let Some(mut chunk) = resp
|
|
.chunk()
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(FETCH_FILE_ERROR_MSG)?
|
|
{
|
|
writer
|
|
.write_all_buf(&mut chunk)
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(WRITE_FILE_ERROR_MSG)?;
|
|
}
|
|
writer
|
|
.flush()
|
|
.await
|
|
.into_diagnostic()
|
|
.wrap_err(WRITE_FILE_ERROR_MSG)?;
|
|
}
|
|
Self::ExtractFile { file, dest } => {
|
|
mkdir_all(dest).await?;
|
|
todo!();
|
|
}
|
|
Self::ExecuteCommand { file, args } => {
|
|
todo!();
|
|
}
|
|
Self::CreateShortcut { target, icon, file } => {
|
|
mkdir_all(file.parent().ok_or_else(|| miette!("Destination file for shortcut step has no parent."))?).await?;
|
|
todo!();
|
|
}
|
|
}
|
|
println!("-> Done.");
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum RemoteResource<'a> {
|
|
// I'm not using url::Url here because it makes it impossible to make this const.
|
|
Url(&'a str),
|
|
|
|
GitHubArtifact {
|
|
/// In the form `org/repo`
|
|
repo: &'a str,
|
|
|
|
/// Artifact name pattern.
|
|
///
|
|
/// Any of the fields in [`GitHubReleaseMeta`] may be injected via handlebar syntax:
|
|
/// `{{field_name}}`.
|
|
/// with `v` prefix stripped if present.
|
|
pattern: &'a str,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ShortcutTarget<'a> {
|
|
/// An executable shortcut.
|
|
Executable {
|
|
/// The executable the shortcut should open.
|
|
file: Cow<'a, Path>,
|
|
|
|
/// Arguments to the executable.
|
|
arg: &'a [&'a str],
|
|
},
|
|
|
|
/// A file or folder shortcut. Please use [`Self::Executable`] for shortcuts to binaries.
|
|
Path { path: Cow<'a, Path> },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct GitHubRelease<'a> {
|
|
#[serde(flatten)]
|
|
meta: GitHubReleaseMeta<'a>,
|
|
//reactions: todo!(),
|
|
assets: Vec<GitHubReleaseAsset>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ramhorns::Content)]
|
|
struct GitHubReleaseMeta<'a> {
|
|
url: String,
|
|
assets_url: String,
|
|
upload_url: String,
|
|
html_url: String,
|
|
id: u64,
|
|
//author: todo!(),
|
|
node_id: String,
|
|
tag_name: String,
|
|
#[serde(skip_deserializing, default = "empty_str")]
|
|
tag_name_strip_prefix: &'a str,
|
|
target_commitish: String,
|
|
name: String,
|
|
draft: bool,
|
|
prerelease: bool,
|
|
created_at: String,
|
|
published_at: String,
|
|
tarball_url: String,
|
|
zipball_url: String,
|
|
body: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct GitHubReleaseAsset {
|
|
url: String,
|
|
id: u64,
|
|
node_id: String,
|
|
name: String,
|
|
label: Option<String>,
|
|
//uploader: todo!(),
|
|
content_type: String,
|
|
state: String,
|
|
size: u64,
|
|
download_count: u64,
|
|
created_at: String,
|
|
updated_at: String,
|
|
browser_download_url: String,
|
|
}
|
|
|
|
#[inline(always)]
|
|
const fn empty_str<'a>() -> &'a str {
|
|
""
|
|
}
|
|
|
|
async fn mkdir_all(path: impl AsRef<Path>) -> Result<()> {
|
|
tokio::fs::DirBuilder::new().mode(0o775).recursive(true).create(path).await.into_diagnostic().wrap_err("Creating directory and any missing parents failed.")
|
|
}
|