1use std::path::{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;
12use crate::storage::s3::S3Store;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct Lock {
16 pub id: String,
17 pub path: String,
18 pub locked_at: String,
19 pub owner: Owner,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Owner {
24 pub name: String,
25}
26
27pub struct LockStore(Backend);
34
35enum Backend {
36 Local { root: PathBuf },
37 Bucket(Box<S3Store>),
38}
39
40impl LockStore {
41 pub fn local(root: impl Into<PathBuf>) -> Self {
42 Self(Backend::Local { root: root.into() })
43 }
44
45 pub fn bucket(bucket: S3Store) -> Self {
46 Self(Backend::Bucket(Box::new(bucket)))
47 }
48
49 pub fn id_of(path: &str) -> String {
50 hex::encode(Sha256::digest(path.as_bytes()))[..32].to_owned()
51 }
52
53 fn prefix(ns: &Namespace) -> String {
56 format!(".locks/{}/{}/", ns.org(), ns.repo())
57 }
58
59 fn key_of(ns: &Namespace, id: &str) -> String {
60 format!("{}{id}.json", Self::prefix(ns))
61 }
62
63 pub async fn create(&self, ns: &Namespace, path: &str, owner: &str) -> Result<Lock, Error> {
64 if path.is_empty() {
65 return Err(Error::MalformedLockPath);
66 }
67
68 let lock = Lock {
69 id: Self::id_of(path),
70 path: path.to_owned(),
71 locked_at: OffsetDateTime::now_utc()
72 .format(&Rfc3339)
73 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_owned()),
74 owner: Owner {
75 name: owner.to_owned(),
76 },
77 };
78 let encoded = serde_json::to_vec(&lock)?;
79
80 let taken = match &self.0 {
81 Backend::Local { root } => {
82 Self::write_new(&Self::path_in(root, ns, &lock.id), &encoded).await?
83 }
84 Backend::Bucket(bucket) => {
85 bucket
86 .put_if_absent(&Self::key_of(ns, &lock.id), encoded)
87 .await?
88 }
89 };
90
91 if taken {
92 return Ok(lock);
93 }
94
95 match self.get(ns, &lock.id).await? {
100 Some(held) => Err(Error::LockHeld(Box::new(held))),
101 None => Err(Error::LockHeld(Box::new(lock))),
102 }
103 }
104
105 async fn write_new(path: &Path, encoded: &[u8]) -> Result<bool, Error> {
106 let parent = path.parent().expect("lock paths have a parent");
107 fs::create_dir_all(parent).await?;
108
109 match fs::OpenOptions::new()
110 .write(true)
111 .create_new(true)
112 .open(path)
113 .await
114 {
115 Ok(mut file) => {
116 file.write_all(encoded).await?;
117 file.sync_all().await?;
118 Ok(true)
119 }
120 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
121 Err(error) => Err(error.into()),
122 }
123 }
124
125 pub async fn get(&self, ns: &Namespace, id: &str) -> Result<Option<Lock>, Error> {
126 if !is_well_formed_id(id) {
127 return Ok(None);
128 }
129
130 let encoded = match &self.0 {
131 Backend::Local { root } => match fs::read(Self::path_in(root, ns, id)).await {
132 Ok(bytes) => Some(bytes),
133 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
134 Err(error) => return Err(error.into()),
135 },
136 Backend::Bucket(bucket) => bucket.get_bytes(&Self::key_of(ns, id)).await?,
137 };
138
139 Ok(encoded.and_then(|bytes| serde_json::from_slice(&bytes).ok()))
140 }
141
142 pub async fn list(&self, ns: &Namespace) -> Result<Vec<Lock>, Error> {
143 let mut locks = match &self.0 {
144 Backend::Local { root } => Self::list_local(&Self::directory_in(root, ns)).await?,
145 Backend::Bucket(bucket) => Self::list_bucket(bucket, ns).await?,
146 };
147
148 locks.sort_by(|a: &Lock, b: &Lock| a.path.cmp(&b.path));
149 Ok(locks)
150 }
151
152 async fn list_local(directory: &Path) -> Result<Vec<Lock>, Error> {
153 let Ok(mut entries) = fs::read_dir(directory).await else {
154 return Ok(Vec::new());
155 };
156
157 let mut locks = Vec::new();
158 while let Some(entry) = entries.next_entry().await? {
159 if let Ok(bytes) = fs::read(entry.path()).await
160 && let Ok(lock) = serde_json::from_slice(&bytes)
161 {
162 locks.push(lock);
163 }
164 }
165
166 Ok(locks)
167 }
168
169 async fn list_bucket(bucket: &S3Store, ns: &Namespace) -> Result<Vec<Lock>, Error> {
174 let mut locks = Vec::new();
175
176 for key in bucket.keys(&Self::prefix(ns)).await? {
177 if let Some(bytes) = bucket.get_bytes(&key).await?
178 && let Ok(lock) = serde_json::from_slice(&bytes)
179 {
180 locks.push(lock);
181 }
182 }
183
184 Ok(locks)
185 }
186
187 pub async fn remove(&self, ns: &Namespace, id: &str) -> Result<(), Error> {
188 if !is_well_formed_id(id) {
189 return Err(Error::LockNotFound);
190 }
191
192 let removed = match &self.0 {
193 Backend::Local { root } => match fs::remove_file(Self::path_in(root, ns, id)).await {
194 Ok(()) => true,
195 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
196 Err(error) => return Err(error.into()),
197 },
198 Backend::Bucket(bucket) => bucket.delete(&Self::key_of(ns, id)).await?,
199 };
200
201 removed.then_some(()).ok_or(Error::LockNotFound)
202 }
203
204 fn directory_in(root: &Path, ns: &Namespace) -> PathBuf {
205 root.join(".locks").join(ns.org()).join(ns.repo())
206 }
207
208 fn path_in(root: &Path, ns: &Namespace, id: &str) -> PathBuf {
209 Self::directory_in(root, ns).join(format!("{id}.json"))
210 }
211}
212
213fn is_well_formed_id(id: &str) -> bool {
214 id.len() == 32 && id.bytes().all(|b| b.is_ascii_hexdigit())
215}
216
217#[cfg(test)]
218mod tests;