OXYGENHUB
# 71e65fcf95ab oxygenhub / crates / hub / src / repos / mod.rs
8.6 KiB · Rust · last changed Advanced settings: transfer, archive, delete, wiki placeholder 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 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
        Repos { root, locks: Mutex::new(HashMap::new()), cache: Arc::new(StateCache::new(64)), searches: search::SearchCache::default() }
51
    }
52
53
    pub fn cache(&self) -> &Arc<StateCache> {
54
        &self.cache
55
    }
56
57
    pub fn root(&self) -> &Path {
58
        &self.root
59
    }
60
61
    /// `<root>/<owner>/<name>`, holding `.oxy/` and nothing else.
62
    pub fn dir(&self, name: &RepoName) -> PathBuf {
63
        self.root.join(&name.owner).join(&name.name)
64
    }
65
66
    pub fn exists(&self, name: &RepoName) -> bool {
67
        self.dir(name).join(oxy_core::repo::OXY_DIR).is_dir()
68
    }
69
70
    /// Open a hosted repository; it is always bare.
71
    pub fn open(&self, name: &RepoName) -> Result<Repo, RepoError> {
72
        if !self.exists(name) {
73
            return Err(RepoError::NotFound);
74
        }
75
        Ok(Repo::open(&self.dir(name))?)
76
    }
77
78
    /// Create an empty bare repository. With no `id` it is **unbound**: the
79
    /// first push binds it to the pusher's repository id, so an existing
80
    /// repository keeps its identity when it moves here (hub spec §4.1).
81
    pub fn create(&self, name: &RepoName, id: Option<[u8; 16]>) -> Result<Repo, RepoError> {
82
        let dir = self.dir(name);
83
        if dir.exists() {
84
            return Err(RepoError::Exists(name.to_string()));
85
        }
86
        Ok(match id {
87
            Some(id) => Repo::init_bare_with_id(&dir, id)?,
88
            None => Repo::init_bare_unbound(&dir)?,
89
        })
90
    }
91
92
    /// Fork `src` into `dest` (hub spec §4): a new bare repository with its
93
    /// own id, the source's objects hard-linked where the file system allows
94
    /// (copied otherwise; objects are immutable, so sharing them is safe),
95
    /// and the source's bookmarks. The trust policy is not copied: the
96
    /// fork's owner sets their own. Runs under the source's read lock.
97
    pub async fn fork(&self, src: &RepoName, dest: &RepoName) -> Result<(), RepoError> {
98
        let guard = self.lock(src).read_owned().await;
99
        let (from, to) = (self.dir(src), self.dir(dest));
100
        if !self.exists(src) {
101
            return Err(RepoError::NotFound);
102
        }
103
        if to.exists() {
104
            return Err(RepoError::Exists(dest.to_string()));
105
        }
106
        tokio::task::spawn_blocking(move || {
107
            let _guard = guard;
108
            let made = (|| -> Result<(), RepoError> {
109
                drop(Repo::init_bare(&to)?);
110
                let objects = |d: &Path| d.join(oxy_core::repo::OXY_DIR).join("objects");
111
                link_tree(&objects(&from), &objects(&to))?;
112
                let source = Repo::open(&from)?;
113
                let fork = Repo::open(&to)?;
114
                let existing: Vec<String> = fork.bookmarks()?.into_iter().map(|b| b.name).collect();
115
                for b in source.bookmarks()? {
116
                    if existing.contains(&b.name) {
117
                        fork.bookmark_set(&b.name, &b.heads)?;
118
                    } else {
119
                        fork.bookmark_create(&b.name, Some(&b.heads))?;
120
                    }
121
                }
122
                Ok(())
123
            })();
124
            if made.is_err() {
125
                let _ = std::fs::remove_dir_all(&to);
126
            }
127
            made
128
        })
129
        .await
130
        .map_err(|e| RepoError::Io(std::io::Error::other(e.to_string())))?
131
    }
132
133
    /// Move a repository to another name (a transfer, #108): the directory,
134
    /// and the lock the old name held.
135
    pub fn rename(&self, from: &RepoName, to: &RepoName) -> Result<(), RepoError> {
136
        let (src, dest) = (self.dir(from), self.dir(to));
137
        if !src.exists() {
138
            return Err(RepoError::NotFound);
139
        }
140
        if dest.exists() {
141
            return Err(RepoError::Exists(to.to_string()));
142
        }
143
        if let Some(parent) = dest.parent() {
144
            std::fs::create_dir_all(parent)?;
145
        }
146
        std::fs::rename(&src, &dest)?;
147
        let mut locks = self.locks.lock().unwrap();
148
        if let Some(l) = locks.remove(&from.to_string()) {
149
            locks.insert(to.to_string(), l);
150
        }
151
        Ok(())
152
    }
153
154
    /// Move a repository to the trash (hub spec §4.1); a job empties it later.
155
    pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> {
156
        let dir = self.dir(name);
157
        if !dir.exists() {
158
            return Err(RepoError::NotFound);
159
        }
160
        let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner);
161
        std::fs::create_dir_all(&trash)?;
162
        let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms()));
163
        std::fs::rename(&dir, &dest)?;
164
        Ok(dest)
165
    }
166
167
    fn lock(&self, name: &RepoName) -> Arc<RwLock<()>> {
168
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
169
    }
170
171
    /// Hold the read lock while `f` runs on the blocking pool.
172
    pub async fn read<T: Send + 'static>(
173
        &self,
174
        name: &RepoName,
175
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
176
    ) -> Result<T, RepoError> {
177
        let lock = self.lock(name);
178
        let guard = lock.read_owned().await;
179
        let dir = self.dir(name);
180
        let exists = self.exists(name);
181
        let cache = self.cache.clone();
182
        tokio::task::spawn_blocking(move || {
183
            let _guard = guard;
184
            if !exists {
185
                return Err(RepoError::NotFound);
186
            }
187
            let repo = Repo::open(&dir)?;
188
            f(&repo, &cache)
189
        })
190
        .await
191
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
192
    }
193
194
    /// Hold the write lock while `f` runs on the blocking pool.
195
    pub async fn write<T: Send + 'static>(
196
        &self,
197
        name: &RepoName,
198
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
199
    ) -> Result<T, RepoError> {
200
        let lock = self.lock(name);
201
        let guard = lock.write_owned().await;
202
        let dir = self.dir(name);
203
        let exists = self.exists(name);
204
        let cache = self.cache.clone();
205
        tokio::task::spawn_blocking(move || {
206
            let _guard = guard;
207
            if !exists {
208
                return Err(RepoError::NotFound);
209
            }
210
            let repo = Repo::open(&dir)?;
211
            f(&repo, &cache)
212
        })
213
        .await
214
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
215
    }
216
217
    /// The write lock alone, for the protocol server, whose handler opens
218
    /// the repository itself.
219
    pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> {
220
        self.lock(name).write_owned().await
221
    }
222
223
    pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> {
224
        self.lock(name).read_owned().await
225
    }
226
}
227
228
/// Hard-link every file under `from` into the same place under `to`,
229
/// copying when a link is not possible (another file system). Temporary
230
/// files of writers in progress are skipped.
231
fn link_tree(from: &Path, to: &Path) -> std::io::Result<()> {
232
    std::fs::create_dir_all(to)?;
233
    for e in std::fs::read_dir(from)? {
234
        let e = e?;
235
        let name = e.file_name();
236
        let n = name.to_string_lossy();
237
        if n.starts_with("tmp-") || n.starts_with("sigs-tmp-") || n.contains(".tmp") {
238
            continue;
239
        }
240
        let (src, dst) = (e.path(), to.join(&name));
241
        if e.file_type()?.is_dir() {
242
            link_tree(&src, &dst)?;
243
        } else if !dst.exists() && std::fs::hard_link(&src, &dst).is_err() {
244
            std::fs::copy(&src, &dst)?;
245
        }
246
    }
247
    Ok(())
248
}