OXYGENHUB
# b0612e7dce30 oxygenhub / crates / hub / src / repos / mod.rs
7.9 KiB · Rust · last changed Function history on the file page 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 the trash (hub spec §4.1); a job empties it later.
134
    pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> {
135
        let dir = self.dir(name);
136
        if !dir.exists() {
137
            return Err(RepoError::NotFound);
138
        }
139
        let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner);
140
        std::fs::create_dir_all(&trash)?;
141
        let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms()));
142
        std::fs::rename(&dir, &dest)?;
143
        Ok(dest)
144
    }
145
146
    fn lock(&self, name: &RepoName) -> Arc<RwLock<()>> {
147
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
148
    }
149
150
    /// Hold the read lock while `f` runs on the blocking pool.
151
    pub async fn read<T: Send + 'static>(
152
        &self,
153
        name: &RepoName,
154
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
155
    ) -> Result<T, RepoError> {
156
        let lock = self.lock(name);
157
        let guard = lock.read_owned().await;
158
        let dir = self.dir(name);
159
        let exists = self.exists(name);
160
        let cache = self.cache.clone();
161
        tokio::task::spawn_blocking(move || {
162
            let _guard = guard;
163
            if !exists {
164
                return Err(RepoError::NotFound);
165
            }
166
            let repo = Repo::open(&dir)?;
167
            f(&repo, &cache)
168
        })
169
        .await
170
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
171
    }
172
173
    /// Hold the write lock while `f` runs on the blocking pool.
174
    pub async fn write<T: Send + 'static>(
175
        &self,
176
        name: &RepoName,
177
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
178
    ) -> Result<T, RepoError> {
179
        let lock = self.lock(name);
180
        let guard = lock.write_owned().await;
181
        let dir = self.dir(name);
182
        let exists = self.exists(name);
183
        let cache = self.cache.clone();
184
        tokio::task::spawn_blocking(move || {
185
            let _guard = guard;
186
            if !exists {
187
                return Err(RepoError::NotFound);
188
            }
189
            let repo = Repo::open(&dir)?;
190
            f(&repo, &cache)
191
        })
192
        .await
193
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
194
    }
195
196
    /// The write lock alone, for the protocol server, whose handler opens
197
    /// the repository itself.
198
    pub async fn write_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockWriteGuard<()> {
199
        self.lock(name).write_owned().await
200
    }
201
202
    pub async fn read_guard(&self, name: &RepoName) -> tokio::sync::OwnedRwLockReadGuard<()> {
203
        self.lock(name).read_owned().await
204
    }
205
}
206
207
/// Hard-link every file under `from` into the same place under `to`,
208
/// copying when a link is not possible (another file system). Temporary
209
/// files of writers in progress are skipped.
210
fn link_tree(from: &Path, to: &Path) -> std::io::Result<()> {
211
    std::fs::create_dir_all(to)?;
212
    for e in std::fs::read_dir(from)? {
213
        let e = e?;
214
        let name = e.file_name();
215
        let n = name.to_string_lossy();
216
        if n.starts_with("tmp-") || n.starts_with("sigs-tmp-") || n.contains(".tmp") {
217
            continue;
218
        }
219
        let (src, dst) = (e.path(), to.join(&name));
220
        if e.file_type()?.is_dir() {
221
            link_tree(&src, &dst)?;
222
        } else if !dst.exists() && std::fs::hard_link(&src, &dst).is_err() {
223
            std::fs::copy(&src, &dst)?;
224
        }
225
    }
226
    Ok(())
227
}