OXYGENHUB
# 8a2dbcb56246 oxygenhub / crates / hub / src / repos / mod.rs
7.9 KiB · Rust · last changed Forks 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
    /// Fork `src` into `dest` (hub spec §4): a new bare repository with its
92
    /// own id, the source's objects hard-linked where the file system allows
93
    /// (copied otherwise; objects are immutable, so sharing them is safe),
94
    /// and the source's bookmarks. The trust policy is not copied: the
95
    /// fork's owner sets their own. Runs under the source's read lock.
96
    pub async fn fork(&self, src: &RepoName, dest: &RepoName) -> Result<(), RepoError> {
97
        let guard = self.lock(src).read_owned().await;
98
        let (from, to) = (self.dir(src), self.dir(dest));
99
        if !self.exists(src) {
100
            return Err(RepoError::NotFound);
101
        }
102
        if to.exists() {
103
            return Err(RepoError::Exists(dest.to_string()));
104
        }
105
        tokio::task::spawn_blocking(move || {
106
            let _guard = guard;
107
            let made = (|| -> Result<(), RepoError> {
108
                drop(Repo::init_bare(&to)?);
109
                let objects = |d: &Path| d.join(oxy_core::repo::OXY_DIR).join("objects");
110
                link_tree(&objects(&from), &objects(&to))?;
111
                let source = Repo::open(&from)?;
112
                let fork = Repo::open(&to)?;
113
                let existing: Vec<String> = fork.bookmarks()?.into_iter().map(|b| b.name).collect();
114
                for b in source.bookmarks()? {
115
                    if existing.contains(&b.name) {
116
                        fork.bookmark_set(&b.name, &b.heads)?;
117
                    } else {
118
                        fork.bookmark_create(&b.name, Some(&b.heads))?;
119
                    }
120
                }
121
                Ok(())
122
            })();
123
            if made.is_err() {
124
                let _ = std::fs::remove_dir_all(&to);
125
            }
126
            made
127
        })
128
        .await
129
        .map_err(|e| RepoError::Io(std::io::Error::other(e.to_string())))?
130
    }
131
132
    /// Move a repository to the trash (hub spec §4.1); a job empties it later.
133
    pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> {
134
        let dir = self.dir(name);
135
        if !dir.exists() {
136
            return Err(RepoError::NotFound);
137
        }
138
        let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner);
139
        std::fs::create_dir_all(&trash)?;
140
        let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms()));
141
        std::fs::rename(&dir, &dest)?;
142
        Ok(dest)
143
    }
144
145
    fn lock(&self, name: &RepoName) -> Arc<RwLock<()>> {
146
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
147
    }
148
149
    /// Hold the read lock while `f` runs on the blocking pool.
150
    pub async fn read<T: Send + 'static>(
151
        &self,
152
        name: &RepoName,
153
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
154
    ) -> Result<T, RepoError> {
155
        let lock = self.lock(name);
156
        let guard = lock.read_owned().await;
157
        let dir = self.dir(name);
158
        let exists = self.exists(name);
159
        let cache = self.cache.clone();
160
        tokio::task::spawn_blocking(move || {
161
            let _guard = guard;
162
            if !exists {
163
                return Err(RepoError::NotFound);
164
            }
165
            let repo = Repo::open(&dir)?;
166
            f(&repo, &cache)
167
        })
168
        .await
169
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
170
    }
171
172
    /// Hold the write lock while `f` runs on the blocking pool.
173
    pub async fn write<T: Send + 'static>(
174
        &self,
175
        name: &RepoName,
176
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
177
    ) -> Result<T, RepoError> {
178
        let lock = self.lock(name);
179
        let guard = lock.write_owned().await;
180
        let dir = self.dir(name);
181
        let exists = self.exists(name);
182
        let cache = self.cache.clone();
183
        tokio::task::spawn_blocking(move || {
184
            let _guard = guard;
185
            if !exists {
186
                return Err(RepoError::NotFound);
187
            }
188
            let repo = Repo::open(&dir)?;
189
            f(&repo, &cache)
190
        })
191
        .await
192
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
193
    }
194
195
    /// The write lock alone, for the protocol server, whose handler opens
196
    /// the repository itself.
197
    pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> {
198
        self.lock(name).write_owned().await
199
    }
200
201
    pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> {
202
        self.lock(name).read_owned().await
203
    }
204
}
205
206
/// Hard-link every file under `from` into the same place under `to`,
207
/// copying when a link is not possible (another file system). Temporary
208
/// files of writers in progress are skipped.
209
fn link_tree(from: &Path, to: &Path) -> std::io::Result<()> {
210
    std::fs::create_dir_all(to)?;
211
    for e in std::fs::read_dir(from)? {
212
        let e = e?;
213
        let name = e.file_name();
214
        let n = name.to_string_lossy();
215
        if n.starts_with("tmp-") || n.starts_with("sigs-tmp-") || n.contains(".tmp") {
216
            continue;
217
        }
218
        let (src, dst) = (e.path(), to.join(&name));
219
        if e.file_type()?.is_dir() {
220
            link_tree(&src, &dst)?;
221
        } else if !dst.exists() && std::fs::hard_link(&src, &dst).is_err() {
222
            std::fs::copy(&src, &dst)?;
223
        }
224
    }
225
    Ok(())
226
}