use serde::{Deserialize, Serialize};
use crate::http::{get_json, JsonGetError};
use crate::url::encode_uri_component;
pub const GITLAB_BASE: &str = "https://gitlab.com";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitLabPackageFile {
pub version: String,
pub filename: String,
pub url: String,
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sha256: Option<String>,
pub created_at: String,
}
#[derive(Debug, thiserror::Error)]
pub enum GitLabError {
#[error(transparent)]
Fetch(#[from] JsonGetError),
#[error("Package '{package}' has no version '{version}' in {project}. Available: {available}")]
NoVersion {
package: String,
version: String,
project: String,
available: String,
},
#[error("Package {project}/{package}@{version} has no {wanted}")]
NoFile {
project: String,
package: String,
version: String,
wanted: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileSelector<'a> {
Named(&'a str),
Suffix(&'a str),
}
#[derive(Deserialize)]
pub(crate) struct PackageWire {
id: u64,
name: String,
version: String,
package_type: String,
status: String,
created_at: String,
}
#[derive(Deserialize)]
pub(crate) struct PackageFileWire {
file_name: String,
size: u64,
#[serde(default)]
file_sha256: Option<String>,
created_at: String,
}
fn newest<T>(items: impl IntoIterator<Item = T>, created_at: impl Fn(&T) -> &str) -> Option<T> {
items.into_iter().reduce(|latest, item| {
if created_at(&item) > created_at(&latest) {
item
} else {
latest
}
})
}
pub fn resolve_gitlab_package_file(
base: &str,
project: &str,
package: &str,
version: &str,
file: FileSelector<'_>,
token: Option<&str>,
) -> Result<GitLabPackageFile, GitLabError> {
let base = base.trim_end_matches('/');
let project_segment = encode_uri_component(project);
let headers: Vec<(&str, &str)> = token
.map(|token| ("PRIVATE-TOKEN", token))
.into_iter()
.collect();
let listed: Vec<PackageWire> = get_json(
&format!(
"{base}/api/v4/projects/{project_segment}/packages?package_type=generic&package_name={}&per_page=100",
encode_uri_component(package)
),
&headers,
)?;
let packages: Vec<PackageWire> = listed
.into_iter()
.filter(|p| p.name == package && p.package_type == "generic" && p.status == "default")
.collect();
let mut available: Vec<&str> = Vec::new();
for candidate in &packages {
if !available.contains(&candidate.version.as_str()) {
available.push(&candidate.version);
}
}
let available = if available.is_empty() {
"(none)".to_owned()
} else {
available.into_iter().take(8).collect::<Vec<_>>().join(", ")
};
let found = newest(packages.into_iter().filter(|p| p.version == version), |p| {
&p.created_at
})
.ok_or_else(|| GitLabError::NoVersion {
package: package.to_owned(),
version: version.to_owned(),
project: project.to_owned(),
available,
})?;
let files: Vec<PackageFileWire> = get_json(
&format!(
"{base}/api/v4/projects/{project_segment}/packages/{}/package_files?per_page=100",
found.id
),
&headers,
)?;
let chosen = newest(
files.into_iter().filter(|f| match file {
FileSelector::Named(name) => f.file_name == name,
FileSelector::Suffix(suffix) => f.file_name.ends_with(suffix),
}),
|f| &f.created_at,
)
.ok_or_else(|| GitLabError::NoFile {
project: project.to_owned(),
package: package.to_owned(),
version: found.version.clone(),
wanted: match file {
FileSelector::Named(name) => format!("file '{name}'"),
FileSelector::Suffix(suffix) => format!("{suffix} file"),
},
})?;
Ok(GitLabPackageFile {
url: format!(
"{base}/api/v4/projects/{project_segment}/packages/generic/{}/{}/{}",
encode_uri_component(package),
encode_uri_component(&found.version),
encode_uri_component(&chosen.file_name),
),
version: found.version,
filename: chosen.file_name,
size: chosen.size,
sha256: chosen.file_sha256,
created_at: found.created_at,
})
}