//! Repositories on disk (hub spec §4): naming, the registry of open //! handles and their locks, and the page-ready views built from `oxy-core`. //! //! Every `oxy-core` call is synchronous and runs on the blocking pool under //! the repository's lock (§16.2): pushes and other writes take the write //! half, page reads the read half. pub mod archive; pub mod cache; pub mod edits; pub mod names; pub mod proposals; pub mod search; pub mod view; pub use cache::{Loaded, StateCache}; pub use names::RepoName; use oxy_core::repo::Repo; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use tokio::sync::RwLock; /// Where repositories live and how their access is serialised. pub struct Repos { root: PathBuf, locks: Mutex>>>, cache: Arc, pub searches: search::SearchCache, } #[derive(Debug, thiserror::Error)] pub enum RepoError { #[error("no such repository")] NotFound, #[error("{0} already exists")] Exists(String), #[error("{0}")] Oxy(#[from] oxy_core::repo::RepoError), #[error("{0}")] Store(#[from] oxy_core::store::StoreError), #[error("{0}")] Io(#[from] std::io::Error), } impl Repos { pub fn new(root: PathBuf) -> Repos { Repos { root, locks: Mutex::new(HashMap::new()), cache: Arc::new(StateCache::new(64)), searches: search::SearchCache::default() } } pub fn cache(&self) -> &Arc { &self.cache } pub fn root(&self) -> &Path { &self.root } /// `//`, holding `.oxy/` and nothing else. pub fn dir(&self, name: &RepoName) -> PathBuf { self.root.join(&name.owner).join(&name.name) } pub fn exists(&self, name: &RepoName) -> bool { self.dir(name).join(oxy_core::repo::OXY_DIR).is_dir() } /// Open a hosted repository; it is always bare. pub fn open(&self, name: &RepoName) -> Result { if !self.exists(name) { return Err(RepoError::NotFound); } Ok(Repo::open(&self.dir(name))?) } /// Create an empty bare repository. With no `id` it is **unbound**: the /// first push binds it to the pusher's repository id, so an existing /// repository keeps its identity when it moves here (hub spec §4.1). pub fn create(&self, name: &RepoName, id: Option<[u8; 16]>) -> Result { let dir = self.dir(name); if dir.exists() { return Err(RepoError::Exists(name.to_string())); } Ok(match id { Some(id) => Repo::init_bare_with_id(&dir, id)?, None => Repo::init_bare_unbound(&dir)?, }) } /// Move a repository to the trash (hub spec §4.1); a job empties it later. pub fn trash(&self, name: &RepoName) -> Result { let dir = self.dir(name); if !dir.exists() { return Err(RepoError::NotFound); } let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner); std::fs::create_dir_all(&trash)?; let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms())); std::fs::rename(&dir, &dest)?; Ok(dest) } fn lock(&self, name: &RepoName) -> Arc> { self.locks.lock().unwrap().entry(name.to_string()).or_default().clone() } /// Hold the read lock while `f` runs on the blocking pool. pub async fn read( &self, name: &RepoName, f: impl FnOnce(&Repo, &StateCache) -> Result + Send + 'static, ) -> Result { let lock = self.lock(name); let guard = lock.read_owned().await; let dir = self.dir(name); let exists = self.exists(name); let cache = self.cache.clone(); tokio::task::spawn_blocking(move || { let _guard = guard; if !exists { return Err(RepoError::NotFound); } let repo = Repo::open(&dir)?; f(&repo, &cache) }) .await .map_err(|e| RepoError::Io(std::io::Error::other(e)))? } /// Hold the write lock while `f` runs on the blocking pool. pub async fn write( &self, name: &RepoName, f: impl FnOnce(&Repo, &StateCache) -> Result + Send + 'static, ) -> Result { let lock = self.lock(name); let guard = lock.write_owned().await; let dir = self.dir(name); let exists = self.exists(name); let cache = self.cache.clone(); tokio::task::spawn_blocking(move || { let _guard = guard; if !exists { return Err(RepoError::NotFound); } let repo = Repo::open(&dir)?; f(&repo, &cache) }) .await .map_err(|e| RepoError::Io(std::io::Error::other(e)))? } /// The write lock alone, for the protocol server, whose handler opens /// the repository itself. pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> { self.lock(name).write_owned().await } pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> { self.lock(name).read_owned().await } }