OXYGENHUB
# de505df5cf8d oxygenhub / crates / hub / src / repos / mod.rs
5.1 KiB · Rust · last changed Milestone 0.3: proposals and review 1 month ago source blame history raw
1
//! Repositories on disk (hub spec §4): naming, the registry of open
2
//! handles and their locks, and the page-ready views built from `oxy-core`.
3
//!
4
//! Every `oxy-core` call is synchronous and runs on the blocking pool under
5
//! the repository's lock (§16.2): pushes and other writes take the write
6
//! half, page reads the read half.
7
8
pub mod cache;
9
pub mod names;
10
pub mod proposals;
11
pub mod view;
12
13
pub use cache::{Loaded, StateCache};
14
pub use names::RepoName;
15
16
use oxy_core::repo::Repo;
17
use std::collections::HashMap;
18
use std::path::{Path, PathBuf};
19
use std::sync::{Arc, Mutex};
20
use tokio::sync::RwLock;
21
22
/// Where repositories live and how their access is serialised.
23
pub struct Repos {
24
    root: PathBuf,
25
    locks: Mutex<HashMap<String, Arc<RwLock<()>>>>,
26
    cache: Arc<StateCache>,
27
}
28
29
#[derive(Debug, thiserror::Error)]
30
pub enum RepoError {
31
    #[error("no such repository")]
32
    NotFound,
33
    #[error("{0} already exists")]
34
    Exists(String),
35
    #[error("{0}")]
36
    Oxy(#[from] oxy_core::repo::RepoError),
37
    #[error("{0}")]
38
    Store(#[from] oxy_core::store::StoreError),
39
    #[error("{0}")]
40
    Io(#[from] std::io::Error),
41
}
42
43
impl Repos {
44
    pub fn new(root: PathBuf) -> Repos {
45
        Repos { root, locks: Mutex::new(HashMap::new()), cache: Arc::new(StateCache::new(64)) }
46
    }
47
48
    pub fn cache(&self) -> &Arc<StateCache> {
49
        &self.cache
50
    }
51
52
    pub fn root(&self) -> &Path {
53
        &self.root
54
    }
55
56
    /// `<root>/<owner>/<name>`, holding `.oxy/` and nothing else.
57
    pub fn dir(&self, name: &RepoName) -> PathBuf {
58
        self.root.join(&name.owner).join(&name.name)
59
    }
60
61
    pub fn exists(&self, name: &RepoName) -> bool {
62
        self.dir(name).join(oxy_core::repo::OXY_DIR).is_dir()
63
    }
64
65
    /// Open a hosted repository; it is always bare.
66
    pub fn open(&self, name: &RepoName) -> Result<Repo, RepoError> {
67
        if !self.exists(name) {
68
            return Err(RepoError::NotFound);
69
        }
70
        Ok(Repo::open(&self.dir(name))?)
71
    }
72
73
    /// Create an empty bare repository. With no `id` it is **unbound**: the
74
    /// first push binds it to the pusher's repository id, so an existing
75
    /// repository keeps its identity when it moves here (hub spec §4.1).
76
    pub fn create(&self, name: &RepoName, id: Option<[u8; 16]>) -> Result<Repo, RepoError> {
77
        let dir = self.dir(name);
78
        if dir.exists() {
79
            return Err(RepoError::Exists(name.to_string()));
80
        }
81
        Ok(match id {
82
            Some(id) => Repo::init_bare_with_id(&dir, id)?,
83
            None => Repo::init_bare_unbound(&dir)?,
84
        })
85
    }
86
87
    /// Move a repository to the trash (hub spec §4.1); a job empties it later.
88
    pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> {
89
        let dir = self.dir(name);
90
        if !dir.exists() {
91
            return Err(RepoError::NotFound);
92
        }
93
        let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner);
94
        std::fs::create_dir_all(&trash)?;
95
        let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms()));
96
        std::fs::rename(&dir, &dest)?;
97
        Ok(dest)
98
    }
99
100
    fn lock(&self, name: &RepoName) -> Arc<RwLock<()>> {
101
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
102
    }
103
104
    /// Hold the read lock while `f` runs on the blocking pool.
105
    pub async fn read<T: Send + 'static>(
106
        &self,
107
        name: &RepoName,
108
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
109
    ) -> Result<T, RepoError> {
110
        let lock = self.lock(name);
111
        let guard = lock.read_owned().await;
112
        let dir = self.dir(name);
113
        let exists = self.exists(name);
114
        let cache = self.cache.clone();
115
        tokio::task::spawn_blocking(move || {
116
            let _guard = guard;
117
            if !exists {
118
                return Err(RepoError::NotFound);
119
            }
120
            let repo = Repo::open(&dir)?;
121
            f(&repo, &cache)
122
        })
123
        .await
124
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
125
    }
126
127
    /// Hold the write lock while `f` runs on the blocking pool.
128
    pub async fn write<T: Send + 'static>(
129
        &self,
130
        name: &RepoName,
131
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
132
    ) -> Result<T, RepoError> {
133
        let lock = self.lock(name);
134
        let guard = lock.write_owned().await;
135
        let dir = self.dir(name);
136
        let exists = self.exists(name);
137
        let cache = self.cache.clone();
138
        tokio::task::spawn_blocking(move || {
139
            let _guard = guard;
140
            if !exists {
141
                return Err(RepoError::NotFound);
142
            }
143
            let repo = Repo::open(&dir)?;
144
            f(&repo, &cache)
145
        })
146
        .await
147
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
148
    }
149
150
    /// The write lock alone, for the protocol server, whose handler opens
151
    /// the repository itself.
152
    pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> {
153
        self.lock(name).write_owned().await
154
    }
155
156
    pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> {
157
        self.lock(name).read_owned().await
158
    }
159
}