use std::collections::HashMap;
use std::io::{BufReader, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Context};
use fs_err::{create_dir_all, File, OpenOptions};
use git2::Repository;
use serde::{Deserialize, Serialize};
use tempfile::TempDir;
use url::Url;
use crate::git_util;
use crate::manifest::Manifest;
use crate::package_name::PackageName;
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageIndexConfig {
pub api: Url,
}
pub struct PackageIndex {
url: Url,
path: PathBuf,
repository: Mutex<Repository>,
package_cache: Mutex<HashMap<PackageName, Arc<PackageMetadata>>>,
access_token: Option<String>,
#[allow(unused)]
temp_dir: Option<TempDir>,
}
impl PackageIndex {
pub fn new(index_url: &Url, access_token: Option<String>) -> anyhow::Result<Self> {
let path = index_path(index_url)?;
let repository = git_util::open_or_clone(access_token.clone(), index_url, &path)?;
let index = Self {
url: index_url.clone(),
path,
repository: Mutex::new(repository),
package_cache: Mutex::new(HashMap::new()),
access_token,
temp_dir: None,
};
index.update()?;
Ok(index)
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn new_temp(index_url: &Url, access_token: Option<String>) -> anyhow::Result<Self> {
let temp_dir = tempfile::tempdir()?;
let path = temp_dir.path().to_owned();
let repository = git_util::open_or_clone(access_token.clone(), index_url, &path)?;
let index = Self {
url: index_url.clone(),
path,
repository: Mutex::new(repository),
package_cache: Mutex::new(HashMap::new()),
access_token,
temp_dir: Some(temp_dir),
};
index.update()?;
Ok(index)
}
pub fn update(&self) -> anyhow::Result<()> {
let repository = self.repository.lock().unwrap();
log::info!("Updating package index...");
git_util::update_index(self.access_token.clone(), &repository)
.with_context(|| format!("could not update package index"))?;
Ok(())
}
pub fn config(&self) -> anyhow::Result<PackageIndexConfig> {
let config_path = self.path.join("config.json");
let contents = fs_err::read_to_string(config_path)?;
Ok(serde_json::from_str(&contents)?)
}
pub fn publish(&self, manifest: &Manifest) -> anyhow::Result<()> {
let repo = self.repository.lock().unwrap();
let package_path = self.package_path(&manifest.package.name);
create_dir_all(package_path.parent().unwrap())?;
{
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(&package_path)?;
let mut entry = serde_json::to_string(&manifest)?;
entry.push('\n');
file.write_all(entry.as_bytes())?;
}
let message = format!("Publish {}", manifest.package_id());
let relative_path = package_path.strip_prefix(&self.path).with_context(|| {
format!(
"Path {} was not relative to package path {}",
package_path.display(),
self.path.display()
)
})?;
git_util::commit_and_push(&repo, self.access_token.clone(), &message, relative_path)?;
let mut package_cache = self.package_cache.lock().unwrap();
package_cache.remove(&manifest.package.name);
Ok(())
}
pub fn get_package_metadata(&self, name: &PackageName) -> anyhow::Result<Arc<PackageMetadata>> {
let mut package_cache = self.package_cache.lock().unwrap();
if package_cache.contains_key(name) {
Ok(Arc::clone(&package_cache[name]))
} else {
let package_path = self.package_path(name);
let file = File::open(&package_path)
.with_context(|| format!("could not open package {} from index", name))?;
let file = BufReader::new(file);
let manifest_stream: Result<Vec<Manifest>, serde_json::Error> =
serde_json::Deserializer::from_reader(file)
.into_iter::<Manifest>()
.collect();
let versions = manifest_stream
.with_context(|| format!("could not parse package index entry for {}", name))?;
let metadata = Arc::new(PackageMetadata { versions });
package_cache.insert(name.clone(), Arc::clone(&metadata));
Ok(metadata)
}
}
fn package_path(&self, name: &PackageName) -> PathBuf {
let mut package_path = self.path.clone();
package_path.push(name.scope());
package_path.push(name.name());
package_path
}
}
#[derive(Default)]
pub struct PackageMetadata {
pub versions: Vec<Manifest>,
}
fn index_path(index_url: &Url) -> anyhow::Result<PathBuf> {
let registry_name = match (index_url.domain(), index_url.scheme()) {
(Some(domain), _) => domain,
(None, "file") => "local-registry",
_ => "unknown",
};
let hash = blake3::hash(index_url.to_string().as_bytes());
let hash_hex = hex::encode(&hash.as_bytes()[..8]);
let ident = format!("{}-{}", registry_name, hash_hex);
let path = dirs::cache_dir()
.ok_or_else(|| anyhow!("could not find cache directory"))?
.join("wally")
.join("index")
.join(ident);
Ok(path)
}