Skip to main content

lfsx_server/
locks.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use time::OffsetDateTime;
6use time::format_description::well_known::Rfc3339;
7use tokio::fs;
8use tokio::io::AsyncWriteExt;
9
10use crate::error::Error;
11use crate::namespace::Namespace;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Lock {
15    pub id: String,
16    pub path: String,
17    pub locked_at: String,
18    pub owner: Owner,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Owner {
23    pub name: String,
24}
25
26pub struct LockStore {
27    root: PathBuf,
28}
29
30impl LockStore {
31    pub fn new(root: impl Into<PathBuf>) -> Self {
32        Self { root: root.into() }
33    }
34
35    pub fn id_of(path: &str) -> String {
36        hex::encode(Sha256::digest(path.as_bytes()))[..32].to_owned()
37    }
38
39    fn directory(&self, ns: &Namespace) -> PathBuf {
40        self.root.join(".locks").join(ns.org()).join(ns.repo())
41    }
42
43    fn path_of(&self, ns: &Namespace, id: &str) -> PathBuf {
44        self.directory(ns).join(format!("{id}.json"))
45    }
46
47    pub async fn create(&self, ns: &Namespace, path: &str, owner: &str) -> Result<Lock, Error> {
48        if path.is_empty() {
49            return Err(Error::MalformedLockPath);
50        }
51
52        let lock = Lock {
53            id: Self::id_of(path),
54            path: path.to_owned(),
55            locked_at: OffsetDateTime::now_utc()
56                .format(&Rfc3339)
57                .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_owned()),
58            owner: Owner {
59                name: owner.to_owned(),
60            },
61        };
62
63        let directory = self.directory(ns);
64        fs::create_dir_all(&directory).await?;
65
66        let file = fs::OpenOptions::new()
67            .write(true)
68            .create_new(true)
69            .open(self.path_of(ns, &lock.id))
70            .await;
71
72        match file {
73            Ok(mut file) => {
74                file.write_all(&serde_json::to_vec(&lock)?).await?;
75                file.sync_all().await?;
76                Ok(lock)
77            }
78            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
79                match self.get(ns, &lock.id).await? {
80                    Some(held) => Err(Error::LockHeld(Box::new(held))),
81                    None => Err(Error::LockHeld(Box::new(lock))),
82                }
83            }
84            Err(error) => Err(error.into()),
85        }
86    }
87
88    pub async fn get(&self, ns: &Namespace, id: &str) -> Result<Option<Lock>, Error> {
89        if !is_well_formed_id(id) {
90            return Ok(None);
91        }
92
93        match fs::read(self.path_of(ns, id)).await {
94            Ok(bytes) => Ok(serde_json::from_slice(&bytes).ok()),
95            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
96            Err(error) => Err(error.into()),
97        }
98    }
99
100    pub async fn list(&self, ns: &Namespace) -> Result<Vec<Lock>, Error> {
101        let Ok(mut entries) = fs::read_dir(self.directory(ns)).await else {
102            return Ok(Vec::new());
103        };
104
105        let mut locks = Vec::new();
106        while let Some(entry) = entries.next_entry().await? {
107            if let Ok(bytes) = fs::read(entry.path()).await
108                && let Ok(lock) = serde_json::from_slice(&bytes)
109            {
110                locks.push(lock);
111            }
112        }
113
114        locks.sort_by(|a: &Lock, b: &Lock| a.path.cmp(&b.path));
115        Ok(locks)
116    }
117
118    pub async fn remove(&self, ns: &Namespace, id: &str) -> Result<(), Error> {
119        if !is_well_formed_id(id) {
120            return Err(Error::LockNotFound);
121        }
122
123        fs::remove_file(self.path_of(ns, id))
124            .await
125            .map_err(|error| match error.kind() {
126                std::io::ErrorKind::NotFound => Error::LockNotFound,
127                _ => error.into(),
128            })
129    }
130}
131
132fn is_well_formed_id(id: &str) -> bool {
133    id.len() == 32 && id.bytes().all(|b| b.is_ascii_hexdigit())
134}
135
136#[cfg(test)]
137mod tests;