OXYGENHUB
# 32cfb089f5e6 oxygenhub / crates / hub / src / repos / mod.rs
10.3 KiB · Rust · last changed Keep bookmark briefs across restarts, and log slow requests 24 days 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 functions;
12
pub mod names;
13
pub mod proposals;
14
pub mod search;
15
pub mod view;
16
17
pub use cache::{Loaded, StateCache};
18
pub use names::RepoName;
19
20
use oxy_core::repo::Repo;
21
use std::collections::HashMap;
22
use std::path::{Path, PathBuf};
23
use std::sync::{Arc, Mutex};
24
use tokio::sync::RwLock;
25
26
/// Where repositories live and how their access is serialised.
27
pub struct Repos {
28
    root: PathBuf,
29
    locks: Mutex<HashMap<String, Arc<RwLock<()>>>>,
30
    cache: Arc<StateCache>,
31
    pub searches: search::SearchCache,
32
}
33
34
#[derive(Debug, thiserror::Error)]
35
pub enum RepoError {
36
    #[error("no such repository")]
37
    NotFound,
38
    #[error("{0} already exists")]
39
    Exists(String),
40
    #[error("{0}")]
41
    Oxy(#[from] oxy_core::repo::RepoError),
42
    #[error("{0}")]
43
    Store(#[from] oxy_core::store::StoreError),
44
    #[error("{0}")]
45
    Io(#[from] std::io::Error),
46
}
47
48
impl Repos {
49
    pub fn new(root: PathBuf) -> Repos {
50
        // States are bounded by their estimated bytes (`cache::budget`); the
51
        // count only stops many tiny states piling up. At 64 it was always
52
        // full and evicting long before the bytes were (#145).
53
        let briefs = root.parent().unwrap_or(&root).join("cache").join("summaries").join(root.file_name().unwrap_or_default());
54
        let cache = Arc::new(StateCache::new(256).with_briefs(briefs));
55
        Repos { root, locks: Mutex::new(HashMap::new()), cache, searches: search::SearchCache::default() }
56
    }
57
58
    pub fn cache(&self) -> &Arc<StateCache> {
59
        &self.cache
60
    }
61
62
    pub fn root(&self) -> &Path {
63
        &self.root
64
    }
65
66
    /// `<root>/<owner>/<name>`, holding `.oxy/` and nothing else.
67
    pub fn dir(&self, name: &RepoName) -> PathBuf {
68
        self.root.join(&name.owner).join(&name.name)
69
    }
70
71
    pub fn exists(&self, name: &RepoName) -> bool {
72
        self.dir(name).join(oxy_core::repo::OXY_DIR).is_dir()
73
    }
74
75
    /// Open a hosted repository; it is always bare.
76
    pub fn open(&self, name: &RepoName) -> Result<Repo, RepoError> {
77
        if !self.exists(name) {
78
            return Err(RepoError::NotFound);
79
        }
80
        Ok(Repo::open(&self.dir(name))?)
81
    }
82
83
    /// Create an empty bare repository. With no `id` it is **unbound**: the
84
    /// first push binds it to the pusher's repository id, so an existing
85
    /// repository keeps its identity when it moves here (hub spec §4.1).
86
    pub fn create(&self, name: &RepoName, id: Option<[u8; 16]>) -> Result<Repo, RepoError> {
87
        let dir = self.dir(name);
88
        if dir.exists() {
89
            return Err(RepoError::Exists(name.to_string()));
90
        }
91
        Ok(match id {
92
            Some(id) => Repo::init_bare_with_id(&dir, id)?,
93
            None => Repo::init_bare_unbound(&dir)?,
94
        })
95
    }
96
97
    /// Create an empty, unbound bare repository whose objects and
98
    /// operation log are kept in the store at `url` (oxygenhub#96); the
99
    /// store's credentials must already be known to the opener.
100
    pub fn create_on(&self, name: &RepoName, url: &str) -> Result<Repo, RepoError> {
101
        let dir = self.dir(name);
102
        if dir.exists() {
103
            return Err(RepoError::Exists(name.to_string()));
104
        }
105
        let made = (|| -> Result<Repo, RepoError> {
106
            drop(Repo::init_bare_on(&dir, [0; 16], Some(url))?);
107
            // Unbound as `create` makes them: oxy-core's marker file in
108
            // place of the id, so the first push gives the id.
109
            let oxy = dir.join(oxy_core::repo::OXY_DIR);
110
            std::fs::remove_file(oxy.join("id"))?;
111
            std::fs::write(oxy.join("unbound"), b"")?;
112
            Ok(Repo::open(&dir)?)
113
        })();
114
        if made.is_err() {
115
            let _ = std::fs::remove_dir_all(&dir);
116
        }
117
        made
118
    }
119
120
    /// Fork `src` into `dest` (hub spec §4): a new bare repository with its
121
    /// own id, the source's objects hard-linked where the file system allows
122
    /// (copied otherwise; objects are immutable, so sharing them is safe),
123
    /// and the source's bookmarks. The trust policy is not copied: the
124
    /// fork's owner sets their own. Runs under the source's read lock.
125
    pub async fn fork(&self, src: &RepoName, dest: &RepoName) -> Result<(), RepoError> {
126
        let guard = self.lock(src).read_owned().await;
127
        let (from, to) = (self.dir(src), self.dir(dest));
128
        if !self.exists(src) {
129
            return Err(RepoError::NotFound);
130
        }
131
        if to.exists() {
132
            return Err(RepoError::Exists(dest.to_string()));
133
        }
134
        if Repo::open(&from).is_ok_and(|r| r.config.storage_url.is_some()) {
135
            return Err(RepoError::Io(std::io::Error::other(
136
                "a repository kept in a bucket cannot be forked yet: clone it and push the clone to a new repository",
137
            )));
138
        }
139
        tokio::task::spawn_blocking(move || {
140
            let _guard = guard;
141
            let made = (|| -> Result<(), RepoError> {
142
                drop(Repo::init_bare(&to)?);
143
                let objects = |d: &Path| d.join(oxy_core::repo::OXY_DIR).join("objects");
144
                link_tree(&objects(&from), &objects(&to))?;
145
                let source = Repo::open(&from)?;
146
                let fork = Repo::open(&to)?;
147
                let existing: Vec<String> = fork.bookmarks()?.into_iter().map(|b| b.name).collect();
148
                for b in source.bookmarks()? {
149
                    if existing.contains(&b.name) {
150
                        fork.bookmark_set(&b.name, &b.heads)?;
151
                    } else {
152
                        fork.bookmark_create(&b.name, Some(&b.heads))?;
153
                    }
154
                }
155
                Ok(())
156
            })();
157
            if made.is_err() {
158
                let _ = std::fs::remove_dir_all(&to);
159
            }
160
            made
161
        })
162
        .await
163
        .map_err(|e| RepoError::Io(std::io::Error::other(e.to_string())))?
164
    }
165
166
    /// Move a repository to another name (a transfer, #108): the directory,
167
    /// and the lock the old name held.
168
    pub fn rename(&self, from: &RepoName, to: &RepoName) -> Result<(), RepoError> {
169
        let (src, dest) = (self.dir(from), self.dir(to));
170
        if !src.exists() {
171
            return Err(RepoError::NotFound);
172
        }
173
        if dest.exists() {
174
            return Err(RepoError::Exists(to.to_string()));
175
        }
176
        if let Some(parent) = dest.parent() {
177
            std::fs::create_dir_all(parent)?;
178
        }
179
        std::fs::rename(&src, &dest)?;
180
        let mut locks = self.locks.lock().unwrap();
181
        if let Some(l) = locks.remove(&from.to_string()) {
182
            locks.insert(to.to_string(), l);
183
        }
184
        Ok(())
185
    }
186
187
    /// Move a repository to the trash (hub spec §4.1); a job empties it later.
188
    pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> {
189
        let dir = self.dir(name);
190
        if !dir.exists() {
191
            return Err(RepoError::NotFound);
192
        }
193
        let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner);
194
        std::fs::create_dir_all(&trash)?;
195
        let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms()));
196
        std::fs::rename(&dir, &dest)?;
197
        Ok(dest)
198
    }
199
200
    fn lock(&self, name: &RepoName) -> Arc<RwLock<()>> {
201
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
202
    }
203
204
    /// Hold the read lock while `f` runs on the blocking pool.
205
    pub async fn read<T: Send + 'static>(
206
        &self,
207
        name: &RepoName,
208
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
209
    ) -> Result<T, RepoError> {
210
        let lock = self.lock(name);
211
        let guard = lock.read_owned().await;
212
        let dir = self.dir(name);
213
        let exists = self.exists(name);
214
        let cache = self.cache.clone();
215
        tokio::task::spawn_blocking(move || {
216
            let _guard = guard;
217
            if !exists {
218
                return Err(RepoError::NotFound);
219
            }
220
            let repo = Repo::open(&dir)?;
221
            f(&repo, &cache)
222
        })
223
        .await
224
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
225
    }
226
227
    /// Hold the write lock while `f` runs on the blocking pool.
228
    pub async fn write<T: Send + 'static>(
229
        &self,
230
        name: &RepoName,
231
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
232
    ) -> Result<T, RepoError> {
233
        let lock = self.lock(name);
234
        let guard = lock.write_owned().await;
235
        let dir = self.dir(name);
236
        let exists = self.exists(name);
237
        let cache = self.cache.clone();
238
        tokio::task::spawn_blocking(move || {
239
            let _guard = guard;
240
            if !exists {
241
                return Err(RepoError::NotFound);
242
            }
243
            let repo = Repo::open(&dir)?;
244
            f(&repo, &cache)
245
        })
246
        .await
247
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
248
    }
249
250
    /// The write lock alone, for the protocol server, whose handler opens
251
    /// the repository itself.
252
    pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> {
253
        self.lock(name).write_owned().await
254
    }
255
256
    pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> {
257
        self.lock(name).read_owned().await
258
    }
259
}
260
261
/// Hard-link every file under `from` into the same place under `to`,
262
/// copying when a link is not possible (another file system). Temporary
263
/// files of writers in progress are skipped.
264
fn link_tree(from: &Path, to: &Path) -> std::io::Result<()> {
265
    std::fs::create_dir_all(to)?;
266
    for e in std::fs::read_dir(from)? {
267
        let e = e?;
268
        let name = e.file_name();
269
        let n = name.to_string_lossy();
270
        if n.starts_with("tmp-") || n.starts_with("sigs-tmp-") || n.contains(".tmp") {
271
            continue;
272
        }
273
        let (src, dst) = (e.path(), to.join(&name));
274
        if e.file_type()?.is_dir() {
275
            link_tree(&src, &dst)?;
276
        } else if !dst.exists() && std::fs::hard_link(&src, &dst).is_err() {
277
            std::fs::copy(&src, &dst)?;
278
        }
279
    }
280
    Ok(())
281
}