use crate::core::{InternedString, PackageId, SourceId};
use crate::sources::git;
use crate::sources::registry::MaybeLock;
use crate::sources::registry::{RegistryConfig, RegistryData, CRATE_TEMPLATE, VERSION_TEMPLATE};
use crate::util::errors::{CargoResult, CargoResultExt};
use crate::util::paths;
use crate::util::{Config, Filesystem, Sha256};
use lazycell::LazyCell;
use log::{debug, trace};
use std::cell::{Cell, Ref, RefCell};
use std::fmt::Write as FmtWrite;
use std::fs::{File, OpenOptions};
use std::io::prelude::*;
use std::io::SeekFrom;
use std::mem;
use std::path::Path;
use std::str;
pub struct RemoteRegistry<'cfg> {
index_path: Filesystem,
cache_path: Filesystem,
source_id: SourceId,
config: &'cfg Config,
tree: RefCell<Option<git2::Tree<'static>>>,
repo: LazyCell<git2::Repository>,
head: Cell<Option<git2::Oid>>,
current_sha: Cell<Option<InternedString>>,
}
impl<'cfg> RemoteRegistry<'cfg> {
pub fn new(source_id: SourceId, config: &'cfg Config, name: &str) -> RemoteRegistry<'cfg> {
RemoteRegistry {
index_path: config.registry_index_path().join(name),
cache_path: config.registry_cache_path().join(name),
source_id,
config,
tree: RefCell::new(None),
repo: LazyCell::new(),
head: Cell::new(None),
current_sha: Cell::new(None),
}
}
fn repo(&self) -> CargoResult<&git2::Repository> {
self.repo.try_borrow_with(|| {
let path = self.config.assert_package_cache_locked(&self.index_path);
if let Ok(repo) = git2::Repository::open(&path) {
trace!("opened a repo without a lock");
return Ok(repo);
}
trace!("acquiring registry index lock");
match git2::Repository::open(&path) {
Ok(repo) => Ok(repo),
Err(_) => {
drop(paths::remove_dir_all(&path));
paths::create_dir_all(&path)?;
let mut opts = git2::RepositoryInitOptions::new();
opts.external_template(false);
Ok(git2::Repository::init_opts(&path, &opts)
.chain_err(|| "failed to initialize index git repository")?)
}
}
})
}
fn head(&self) -> CargoResult<git2::Oid> {
if self.head.get().is_none() {
let oid = self.repo()?.refname_to_id("refs/remotes/origin/master")?;
self.head.set(Some(oid));
}
Ok(self.head.get().unwrap())
}
fn tree(&self) -> CargoResult<Ref<'_, git2::Tree<'_>>> {
{
let tree = self.tree.borrow();
if tree.is_some() {
return Ok(Ref::map(tree, |s| s.as_ref().unwrap()));
}
}
let repo = self.repo()?;
let commit = repo.find_commit(self.head()?)?;
let tree = commit.tree()?;
let tree = unsafe { mem::transmute::<git2::Tree<'_>, git2::Tree<'static>>(tree) };
*self.tree.borrow_mut() = Some(tree);
Ok(Ref::map(self.tree.borrow(), |s| s.as_ref().unwrap()))
}
fn filename(&self, pkg: PackageId) -> String {
format!("{}-{}.crate", pkg.name(), pkg.version())
}
}
const LAST_UPDATED_FILE: &str = ".last-updated";
impl<'cfg> RegistryData for RemoteRegistry<'cfg> {
fn prepare(&self) -> CargoResult<()> {
self.repo()?; Ok(())
}
fn index_path(&self) -> &Filesystem {
&self.index_path
}
fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
self.config.assert_package_cache_locked(path)
}
fn current_version(&self) -> Option<InternedString> {
if let Some(sha) = self.current_sha.get() {
return Some(sha);
}
let sha = InternedString::new(&self.head().ok()?.to_string());
self.current_sha.set(Some(sha));
Some(sha)
}
fn load(
&self,
_root: &Path,
path: &Path,
data: &mut dyn FnMut(&[u8]) -> CargoResult<()>,
) -> CargoResult<()> {
let repo = self.repo()?;
let tree = self.tree()?;
let entry = tree.get_path(path)?;
let object = entry.to_object(repo)?;
let blob = match object.as_blob() {
Some(blob) => blob,
None => anyhow::bail!("path `{}` is not a blob in the git repo", path.display()),
};
data(blob.content())
}
fn config(&mut self) -> CargoResult<Option<RegistryConfig>> {
debug!("loading config");
self.prepare()?;
self.config.assert_package_cache_locked(&self.index_path);
let mut config = None;
self.load(Path::new(""), Path::new("config.json"), &mut |json| {
config = Some(serde_json::from_slice(json)?);
Ok(())
})?;
trace!("config loaded");
Ok(config)
}
fn update_index(&mut self) -> CargoResult<()> {
if self.config.offline() {
return Ok(());
}
if self.config.cli_unstable().no_index_update {
return Ok(());
}
if self.config.updated_sources().contains(&self.source_id) {
return Ok(());
}
debug!("updating the index");
self.config.http()?;
self.prepare()?;
self.head.set(None);
*self.tree.borrow_mut() = None;
self.current_sha.set(None);
let path = self.config.assert_package_cache_locked(&self.index_path);
self.config
.shell()
.status("Updating", self.source_id.display_index())?;
let url = self.source_id.url();
let refspec = "refs/heads/master:refs/remotes/origin/master";
let repo = self.repo.borrow_mut().unwrap();
git::fetch(repo, url.as_str(), refspec, self.config)
.chain_err(|| format!("failed to fetch `{}`", url))?;
self.config.updated_sources().insert(self.source_id);
File::create(&path.join(LAST_UPDATED_FILE))?;
Ok(())
}
fn download(&mut self, pkg: PackageId, _checksum: &str) -> CargoResult<MaybeLock> {
let filename = self.filename(pkg);
let path = self.cache_path.join(&filename);
let path = self.config.assert_package_cache_locked(&path);
if let Ok(dst) = File::open(&path) {
let meta = dst.metadata()?;
if meta.len() > 0 {
return Ok(MaybeLock::Ready(dst));
}
}
let config = self.config()?.unwrap();
let mut url = config.dl;
if !url.contains(CRATE_TEMPLATE) && !url.contains(VERSION_TEMPLATE) {
write!(url, "/{}/{}/download", CRATE_TEMPLATE, VERSION_TEMPLATE).unwrap();
}
let url = url
.replace(CRATE_TEMPLATE, &*pkg.name())
.replace(VERSION_TEMPLATE, &pkg.version().to_string());
Ok(MaybeLock::Download {
url,
descriptor: pkg.to_string(),
})
}
fn finish_download(
&mut self,
pkg: PackageId,
checksum: &str,
data: &[u8],
) -> CargoResult<File> {
let actual = Sha256::new().update(data).finish_hex();
if actual != checksum {
anyhow::bail!("failed to verify the checksum of `{}`", pkg)
}
let filename = self.filename(pkg);
self.cache_path.create_dir()?;
let path = self.cache_path.join(&filename);
let path = self.config.assert_package_cache_locked(&path);
let mut dst = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&path)?;
let meta = dst.metadata()?;
if meta.len() > 0 {
return Ok(dst);
}
dst.write_all(data)?;
dst.seek(SeekFrom::Start(0))?;
Ok(dst)
}
fn is_crate_downloaded(&self, pkg: PackageId) -> bool {
let filename = format!("{}-{}.crate", pkg.name(), pkg.version());
let path = Path::new(&filename);
let path = self.cache_path.join(path);
let path = self.config.assert_package_cache_locked(&path);
if let Ok(dst) = File::open(path) {
if let Ok(meta) = dst.metadata() {
return meta.len() > 0;
}
}
false
}
}
impl<'cfg> Drop for RemoteRegistry<'cfg> {
fn drop(&mut self) {
self.tree.borrow_mut().take();
}
}