use std::collections::HashMap;
use std::io::{BufReader, ErrorKind, 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 github_oauth_id: Option<String>,
}
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())?;
}
git_util::commit_and_push(
&repo,
self.access_token.clone(),
&format!("Publish {}", manifest.package_id()),
&self.path,
&package_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)
}
}
pub fn get_scope_owners(&self, scope: &str) -> anyhow::Result<Vec<u64>> {
let mut path = self.path.clone();
path.push(scope);
path.push("owners.json");
match File::open(path) {
Ok(file) => serde_json::from_reader(file)
.with_context(|| format!("could not parse owner file for scope {}", scope)),
Err(error) => match error.kind() {
ErrorKind::NotFound => Ok(Vec::new()),
_ => Err(error)
.with_context(|| format!("failed to read owner file for scope {}", scope)),
},
}
}
pub fn is_scope_owner(&self, scope: &str, user_id: &u64) -> anyhow::Result<bool> {
let owners = self.get_scope_owners(scope)?;
Ok(owners.iter().any(|owner| owner == user_id))
}
pub fn add_scope_owner(&self, scope: &str, owner_id: &u64) -> anyhow::Result<()> {
let repo = self.repository.lock().unwrap();
let mut path = self.path.clone();
path.push(scope);
create_dir_all(&path)?;
path.push("owners.json");
{
let mut owners = self.get_scope_owners(&scope)?;
let mut file = OpenOptions::new().write(true).create(true).open(&path)?;
owners.push(*owner_id);
file.write_all(serde_json::to_string(&owners)?.as_bytes())?;
}
git_util::commit_and_push(
&repo,
self.access_token.clone(),
&format!("Add owner for {}/*", scope),
&self.path,
&path,
)?;
Ok(())
}
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)
}