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