use crate::docs::{Docs, Versions};
use crate::pkg::api::*;
use colored::Colorize;
use crypto::digest::Digest;
use graphql_client::Response;
use loa::semantics::Analysis;
use loa::HashMap;
use std::io::Error;
use std::path::PathBuf;
pub struct APIClient {
host: String,
client: reqwest::Client,
analysis: Analysis,
config: ManifestFile<config::Config>,
lockfile: ManifestFile<config::Lockfile>,
pkgfile: ManifestFile<config::Pkgfile>,
}
impl APIClient {
pub fn new(host: &str, config_file: &str, analysis: Analysis) -> APIClient {
APIClient {
host: host.into(),
client: reqwest::Client::new(),
analysis,
config: ManifestFile::new(config_file),
lockfile: ManifestFile::new(".pkg.lock"),
pkgfile: ManifestFile::new("pkg.yml"),
}
}
pub fn login(&self, email: &str, password: &str) -> APIResult<()> {
let query_body = LoginMutation::build_query(login_mutation::Variables {
email: email.into(),
password: password.into(),
});
let mut response = self
.client
.post(self.host.as_str())
.json(&query_body)
.send()?;
let response_body: Response<login_mutation::ResponseData> = response.json()?;
if response_body.errors.is_some() {
return Err(APIError::GraphQL(response_body.errors));
} else if (&response_body.data)
.as_ref()
.and_then(|d| d.login.as_ref())
.is_none()
{
return Err(APIError::InvalidCredentials);
}
for cookie in response.cookies() {
if cookie.name() == "LOA_AUTH" {
let token = cookie.value();
self.config.update(|config| {
config.auth_token = Some(token.into());
config.auth_email = Some(response_body.data.unwrap().login.unwrap().email);
Ok(())
})?;
return Ok(());
}
}
Err(APIError::InvalidCredentials)
}
pub fn logout(&self) -> APIResult<()> {
self.config.update(|config| {
config.auth_token = None;
config.auth_email = None;
Ok(())
})
}
pub fn auth_email(&self) -> APIResult<Option<String>> {
Ok(self.config.load()?.auth_email)
}
fn auth_token(&self) -> APIResult<Option<String>> {
Ok(self.config.load()?.auth_token)
}
pub fn add_packages(&self, packages: HashMap<&str, Option<&str>>) -> APIResult<()> {
self.lockfile.update(|lock| {
self.pkgfile.update(|pkgfile| {
self.add_packages_impl(packages, pkgfile, lock)?;
Ok(())
})?;
self.install_from_lockfile(lock)
})
}
pub fn remove_packages(&self, packages: Vec<&str>) -> APIResult<()> {
self.pkgfile.update(|pkgfile| {
if let Some(ref mut d) = pkgfile.dependencies {
for p in packages {
d.remove(p);
}
}
Ok(())
})?;
self.get_from_pkgfile()
}
fn install_from_lockfile(&self, lock: &mut config::Lockfile) -> APIResult<()> {
let pkg_dir = PathBuf::from(".pkg");
if pkg_dir.exists() {
std::fs::remove_dir_all(&pkg_dir)?;
}
for (
name,
config::LockfilePackageRegistration {
version,
checksum,
url,
},
) in lock.0.iter()
{
let mut response = reqwest::get(url.as_str())?;
let mut buf = vec![];
response.copy_to(&mut buf)?;
let mut checksum_verify = crypto::sha1::Sha1::new();
checksum_verify.input(buf.as_slice());
if checksum_verify.result_str() != *checksum {
return Err(APIError::ChecksumMismatch);
}
let mut archive = tar::Archive::new(buf.as_slice());
let mut package_dir = pkg_dir.clone();
package_dir.extend(name.split("/"));
std::fs::create_dir_all(&package_dir)?;
for entry in archive.entries()? {
let mut entry = entry?;
if !entry.unpack_in(&package_dir)? {
println!(
"{} {}{}{}",
"Failed to unpack".bright_black(),
package_dir.to_str().unwrap().green(),
"/".green(),
entry.path()?.to_str().unwrap().green()
);
}
}
println!(
"{} {} {}",
"Installed".bright_black(),
name.green(),
format!(" {} ", version).bold().black().on_bright_yellow()
);
}
Ok(())
}
fn add_packages_impl(
&self,
packages: HashMap<&str, Option<&str>>,
pkgfile: &mut config::Pkgfile,
lock: &mut config::Lockfile,
) -> APIResult<()> {
let no_deps = HashMap::new();
let requested_packages = pkgfile
.dependencies
.as_ref()
.unwrap_or(&no_deps)
.iter()
.map(|(name, version)| resolve_packages_query::RequestedPackage {
name: name.clone(),
version: Some(version.clone()),
})
.chain(packages.iter().map(|(name, version)| {
resolve_packages_query::RequestedPackage {
name: (*name).into(),
version: version.map(String::from),
}
}))
.collect::<Vec<_>>();
let query_body = ResolvePackagesQuery::build_query(resolve_packages_query::Variables {
packages: requested_packages,
});
let mut response = self
.client
.post(self.host.as_str())
.json(&query_body)
.send()?;
let response_body: Response<resolve_packages_query::ResponseData> = response.json()?;
if response_body.errors.is_some() {
return Err(APIError::GraphQL(response_body.errors));
}
if let Some(data) = response_body.data {
lock.0.clear();
if let None = pkgfile.dependencies {
pkgfile.dependencies = Some(HashMap::new());
}
for package in packages.keys() {
match data
.resolve_packages
.iter()
.filter(|r| r.package.name == *package)
.next()
{
None => return Err(APIError::PackageNotFound),
Some(r) => {
pkgfile
.dependencies
.as_mut()
.unwrap()
.insert(r.package.name.clone(), r.version.clone());
}
}
}
for release in data.resolve_packages {
let name = release.package.name;
let version = release.version;
let url = release.url;
let checksum = release.checksum;
lock.0.insert(
name,
config::LockfilePackageRegistration {
checksum,
version,
url,
},
);
}
return Ok(());
}
Err(APIError::PackageNotFound)
}
fn walk() -> ignore::Walk {
let mut builder = ignore::WalkBuilder::new("./");
builder.add_ignore(".pkgignore");
builder.add_custom_ignore_filename(".pkg/");
builder.build()
}
fn pack(&self, checksum: &mut ChecksumWriter) -> APIResult<Vec<u8>> {
let mut buf = vec![];
let mut builder = tar::Builder::<DualWriter<_, _>>::new((&mut buf, checksum).into());
for entry in Self::walk() {
let entry = entry?.into_path();
if entry.is_file() {
println!(
"{} {}",
"Packing".bright_black(),
entry.to_str().unwrap_or("<<unknown>>").green()
);
builder.append_path(entry)?;
}
}
{
let mut docs: Docs = self.analysis.clone().into();
let pkgfile = self.pkgfile.load()?;
if let Some(ref name) = pkgfile.name {
docs.retain_package(name.as_ref());
}
docs.apply_versions(&Versions {
pkgfile,
lockfile: self.lockfile.load()?,
});
let docs_bytes = serde_json::to_string(&docs)?.into_bytes();
let mut header = tar::Header::new_gnu();
header.set_size(docs_bytes.len() as u64);
header.set_cksum();
builder.append_data(&mut header, ".docs.json", docs_bytes.as_slice())?;
}
builder.finish()?;
drop(builder);
Ok(buf)
}
pub fn publish_package(&self, name: &str, version: &str) -> APIResult<()> {
let mut checksum = ChecksumWriter::new();
let package = self.pack(&mut checksum)?;
let query_body = UploadPackageMutation::build_query(upload_package_mutation::Variables {
name: name.into(),
version: version.into(),
package: Upload,
checksum: checksum.result(),
dependencies: self
.pkgfile
.load()
.ok()
.and_then(|p| p.dependencies)
.map(|deps| {
deps.into_iter()
.map(
|(package, version)| upload_package_mutation::PublicationDependency {
package,
version,
development: None,
},
)
.collect::<Vec<_>>()
})
.unwrap_or(vec![]),
});
let form = reqwest::multipart::Form::new()
.text("operations", serde_json::to_string(&query_body)?)
.text("map", r##"{ "0": ["variables.package"] }"##)
.part(
"0",
reqwest::multipart::Part::bytes(package)
.file_name("package.tar.gz")
.mime_str("application/tar+gzip")
.unwrap(),
);
let mut request = self.client.post(self.host.as_str()).multipart(form);
if let Some(token) = self.auth_token()? {
request = request.header("Cookie", format!("LOA_AUTH={}", token));
}
let mut response = request.send()?;
let response_body: Response<upload_package_mutation::ResponseData> = response.json()?;
match response_body.data.and_then(|data| data.publish_package) {
None => Err(APIError::GraphQL(response_body.errors)),
Some(_) => Ok(()),
}
}
pub fn get_from_pkgfile(&self) -> APIResult<()> {
self.add_packages(HashMap::new())
}
pub fn get_from_lockfile(&self) -> APIResult<()> {
self.lockfile
.update(|lock| self.install_from_lockfile(lock))
}
}
struct ChecksumWriter {
inner: crypto::sha1::Sha1,
}
impl ChecksumWriter {
pub fn new() -> ChecksumWriter {
ChecksumWriter {
inner: crypto::sha1::Sha1::new(),
}
}
pub fn result(&mut self) -> String {
self.inner.result_str()
}
}
impl std::io::Write for ChecksumWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
let len = buf.len();
self.inner.input(buf);
Ok(len)
}
fn flush(&mut self) -> Result<(), Error> {
Ok(())
}
}
struct DualWriter<A, B> {
a: A,
b: B,
}
impl<A, B> From<(A, B)> for DualWriter<A, B> {
fn from((a, b): (A, B)) -> Self {
DualWriter { a, b }
}
}
impl<A: std::io::Write, B: std::io::Write> std::io::Write for DualWriter<A, B> {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
self.a.write(buf).and(self.b.write(buf))
}
fn flush(&mut self) -> Result<(), Error> {
self.a.flush().and(self.b.flush())
}
}