For now this is just a simple implementation only supporting `put` steps. `in` is a noop, so `no_get: true` is recommended.
286 lines
7.7 KiB
Rust
286 lines
7.7 KiB
Rust
use std::fs;
|
|
use std::io::{self, Read, Write};
|
|
use std::path::Path;
|
|
|
|
use cli_table::format::Justify;
|
|
use cli_table::{Cell, Style, Table};
|
|
use color_eyre::eyre::{self, Context};
|
|
use color_eyre::{Help, Report, Result};
|
|
use globwalk::{DirEntry, GlobWalkerBuilder};
|
|
use reqwest::StatusCode;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
use time::format_description::well_known::Iso8601;
|
|
use url::Url;
|
|
|
|
use crate::types::{Package, PackageType};
|
|
use crate::util::make_client;
|
|
use crate::Action;
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct Source {
|
|
access_token: String,
|
|
owner: String,
|
|
url: Url,
|
|
r#type: PackageType,
|
|
name: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct Version {
|
|
version: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct Params {
|
|
version: String,
|
|
globs: Vec<String>,
|
|
#[serde(default)]
|
|
fail_fast: bool,
|
|
#[serde(default)]
|
|
r#override: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct Config {
|
|
/// Resource configuration.
|
|
/// Passed verbatim from the definition in the pipeline.
|
|
source: Source,
|
|
/// For 'check':
|
|
/// Previous version of this resource.
|
|
/// Will be empty for the very first request.
|
|
/// For 'in':
|
|
/// The version metadata to create this as.
|
|
version: Option<Version>,
|
|
params: Option<Params>,
|
|
}
|
|
|
|
fn action_check(conf: Config) -> Result<()> {
|
|
let client = make_client(&conf.source.access_token)?;
|
|
let url = conf
|
|
.source
|
|
.url
|
|
.join(&format!("api/v1/packages/{}", conf.source.owner))
|
|
.wrap_err("Invalid URL")?;
|
|
|
|
let mut pkgs: Vec<Package> = client
|
|
.get(url)
|
|
.query(&[
|
|
("type", conf.source.r#type.to_string()),
|
|
("q", conf.source.name),
|
|
])
|
|
.send()?
|
|
.json()?;
|
|
|
|
if pkgs.is_empty() {
|
|
return io::stdout().write_all(b"[]").map_err(From::from);
|
|
}
|
|
|
|
pkgs.sort_unstable_by_key(|pkg| pkg.created_at);
|
|
|
|
let table: Vec<_> = pkgs
|
|
.iter()
|
|
.map(|pkg| {
|
|
vec![
|
|
pkg.id.cell().justify(Justify::Center),
|
|
pkg.name.clone().cell(),
|
|
pkg.owner.login.clone().cell(),
|
|
pkg.version.clone().cell(),
|
|
pkg.created_at
|
|
.format(&Iso8601::DEFAULT)
|
|
.unwrap_or_default()
|
|
.cell(),
|
|
]
|
|
})
|
|
.collect();
|
|
|
|
let table = table
|
|
.table()
|
|
.title(vec![
|
|
"#".cell().bold(true),
|
|
"Name".cell().bold(true),
|
|
"Owner".cell().bold(true),
|
|
"Version".cell().bold(true),
|
|
"Created At".cell().bold(true),
|
|
])
|
|
.bold(true);
|
|
|
|
let _ = cli_table::print_stderr(table);
|
|
|
|
let newest = pkgs
|
|
.last()
|
|
.expect("List of packages should not be empty.")
|
|
.version
|
|
.clone();
|
|
|
|
let out = if let Some(prev) = conf.version {
|
|
if prev.version == newest {
|
|
vec![prev]
|
|
} else {
|
|
vec![prev, Version { version: newest }]
|
|
}
|
|
} else {
|
|
vec![Version { version: newest }]
|
|
};
|
|
|
|
serde_json::to_writer_pretty(io::stdout(), &out)
|
|
.wrap_err("Failed to write result to stdout")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn action_out(conf: Config, dir: impl AsRef<Path>) -> Result<()> {
|
|
let dir = dir
|
|
.as_ref()
|
|
.canonicalize()
|
|
.wrap_err_with(|| format!("Invalid file path '{}'", dir.as_ref().display()))?;
|
|
|
|
let Some(params) = conf.params else {
|
|
eyre::bail!("Missing params");
|
|
};
|
|
|
|
if params.globs.is_empty() {
|
|
eyre::bail!("`params.globs` must not be empty");
|
|
}
|
|
|
|
let client = make_client(&conf.source.access_token)?;
|
|
let url = conf
|
|
.source
|
|
.url
|
|
.join(&format!(
|
|
// The trailing slash is required, to make sure this entire URL is used
|
|
// when `join`ing the file name later. Otherwise, the last component
|
|
// would be considered a "file" part itself, and replaced by a future `join`.
|
|
"api/packages/{}/{}/{}/{}/",
|
|
conf.source.owner, conf.source.r#type, conf.source.name, params.version
|
|
))
|
|
.wrap_err("Invalid URL")?;
|
|
|
|
let walker = GlobWalkerBuilder::from_patterns(dir, ¶ms.globs)
|
|
.max_depth(3)
|
|
.file_type(globwalk::FileType::FILE)
|
|
.build()
|
|
.wrap_err("Failed to create glob walker")?;
|
|
|
|
let handle_glob = |entry| {
|
|
let entry: DirEntry = entry?;
|
|
|
|
let name = entry.file_name();
|
|
let path = entry.path();
|
|
let url = url.join(&name.to_string_lossy()).wrap_err("Invalid url")?;
|
|
|
|
if params.r#override {
|
|
client
|
|
.delete(url.clone())
|
|
.send()
|
|
.wrap_err_with(|| format!("Failed request 'DELETE {}'", &url))?;
|
|
}
|
|
|
|
let data = fs::read(path)
|
|
.wrap_err_with(|| format!("Failed to read package file '{}'", path.display()))?;
|
|
|
|
let res = client
|
|
.put(url.clone())
|
|
.body(data)
|
|
.send()
|
|
.wrap_err_with(|| format!("Failed request 'PUT {}'", &url))?;
|
|
|
|
match res.status() {
|
|
StatusCode::CREATED => {
|
|
eprintln!(
|
|
"Uploaded file '{}', version={}, package={}",
|
|
name.to_string_lossy(),
|
|
params.version,
|
|
conf.source.name
|
|
);
|
|
}
|
|
StatusCode::BAD_REQUEST => {
|
|
eyre::bail!(
|
|
"Package, version or file name are invalid. package={}, version={}, file={}",
|
|
conf.source.name,
|
|
params.version,
|
|
name.to_string_lossy(),
|
|
);
|
|
}
|
|
StatusCode::CONFLICT => {
|
|
eyre::bail!(
|
|
"File '{}' already exists. version={}, package={}",
|
|
name.to_string_lossy(),
|
|
params.version,
|
|
conf.source.name
|
|
);
|
|
}
|
|
StatusCode::INTERNAL_SERVER_ERROR => {
|
|
eyre::bail!(
|
|
"Internal server error: {} {}",
|
|
res.status(),
|
|
res.text().unwrap_or_default()
|
|
);
|
|
}
|
|
code => {
|
|
eyre::bail!("Unexpected status code {}\ntext = {:?}", code, res.text());
|
|
}
|
|
}
|
|
|
|
Ok::<_, Report>(())
|
|
};
|
|
|
|
let mut has_entry = false;
|
|
|
|
let mut errors = Vec::new();
|
|
for entry in walker {
|
|
has_entry = true;
|
|
if let Err(err) = handle_glob(entry) {
|
|
errors.push(err);
|
|
if params.fail_fast {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !errors.is_empty() {
|
|
let mut err = eyre::eyre!("Failed to upload package with {} errors", errors.len());
|
|
|
|
for e in errors {
|
|
err = err.report(e);
|
|
}
|
|
|
|
return Err(err);
|
|
}
|
|
|
|
if !has_entry {
|
|
eyre::bail!("Globs didn't produce any files");
|
|
}
|
|
|
|
let out = json!({
|
|
"version": {
|
|
"version": params.version
|
|
}
|
|
});
|
|
|
|
serde_json::to_writer_pretty(io::stdout(), &out)
|
|
.wrap_err("Failed to write result to stdout")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn run(action: &Action) -> Result<()> {
|
|
let config: Config = {
|
|
let mut buf = String::new();
|
|
io::stdin()
|
|
.read_to_string(&mut buf)
|
|
.wrap_err("Failed to read from stdin")?;
|
|
|
|
if buf.is_empty() {
|
|
eyre::bail!("No data received on stdin");
|
|
}
|
|
|
|
serde_json::from_str(&buf).wrap_err("Failed to parse stdin")?
|
|
};
|
|
|
|
match action {
|
|
Action::Check => action_check(config),
|
|
Action::In { dest: _ } => Ok(()),
|
|
Action::Out { src } => action_out(config, src),
|
|
}
|
|
}
|