1pub use anyhow::Error;
2
3mod hash;
4mod path;
5
6pub use hash::{Commit, Hash};
7pub use path::Path;
8
9#[derive(Clone)]
10pub struct Client {
11 url: reqwest::Url,
12 client: reqwest::Client,
13 version: Version,
14 auth: Option<String>,
15 branch: Option<String>,
16}
17
18#[derive(serde::Deserialize)]
19pub struct CommitInfo {
20 pub hash: Commit,
21 pub parents: Option<Vec<Commit>>,
22 pub date: i64,
23 pub author: String,
24 pub message: String,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Version {
29 V1,
30}
31
32impl Client {
33 pub fn new(url: impl reqwest::IntoUrl, version: Version) -> Result<Client, Error> {
34 let url = url.into_url()?;
35
36 let client = reqwest::Client::new();
37
38 Ok(Client {
39 url,
40 client,
41 version,
42 auth: None,
43 branch: None,
44 })
45 }
46
47 pub fn with_auth(mut self, auth: String) -> Client {
48 self.auth = Some(auth);
49 self
50 }
51
52 pub fn with_branch(mut self, branch: String) -> Client {
53 self.branch = Some(branch);
54 self
55 }
56
57 pub async fn request(
58 &self,
59 method: reqwest::Method,
60 endpoint: impl AsRef<str>,
61 body: Option<Vec<u8>>,
62 ) -> Result<reqwest::Response, Error> {
63 let url = self.url.join(endpoint.as_ref())?;
64 let mut builder = self.client.request(method, url);
65
66 if let Some(auth) = &self.auth {
67 builder = builder.header("Wasmstore-Auth", auth);
68 }
69
70 if let Some(branch) = &self.branch {
71 builder = builder.header("Wasmstore-Branch", branch);
72 }
73
74 if let Some(body) = body {
75 builder = builder.body(body);
76 }
77
78 let res = builder.send().await?;
79 Ok(res)
80 }
81
82 fn endpoint(&self, endpoint: &str) -> String {
83 let v = match self.version {
84 Version::V1 => "v1",
85 };
86 format!("/api/{v}{endpoint}")
87 }
88
89 pub async fn find(&self, path: impl Into<Path>) -> Result<Option<(Vec<u8>, Hash)>, Error> {
90 let path = path.into();
91 let p = format!("/module/{}", path.to_string());
92 let res = self
93 .request(reqwest::Method::GET, self.endpoint(&p), None)
94 .await?;
95 if res.status() == reqwest::StatusCode::NOT_FOUND {
96 return Ok(None);
97 }
98
99 if !res.status().is_success() {
100 return Err(Error::msg(res.text().await?));
101 }
102 let hash = res
103 .headers()
104 .get("Wasmstore-Hash")
105 .expect("Wasmstore-Hash header is unset in find response")
106 .to_str()?
107 .to_string();
108 let b = res.bytes().await?;
109 Ok(Some((b.to_vec(), Hash(hash))))
110 }
111
112 pub async fn hash(&self, path: impl Into<Path>) -> Result<Option<Hash>, Error> {
113 let path = path.into();
114 let p = format!("/hash/{}", path.to_string());
115 let res = self
116 .request(reqwest::Method::GET, self.endpoint(&p), None)
117 .await?;
118 if res.status() == reqwest::StatusCode::NOT_FOUND {
119 return Ok(None);
120 }
121
122 if !res.status().is_success() {
123 return Err(Error::msg(res.text().await?));
124 }
125
126 let b = res.text().await?;
127 Ok(Some(Hash(b)))
128 }
129
130 pub async fn add(&self, path: impl Into<Path>, data: Vec<u8>) -> Result<Hash, Error> {
131 let path = path.into();
132 let p = format!("/module/{}", path.to_string());
133 let res = self
134 .request(reqwest::Method::POST, self.endpoint(&p), Some(data))
135 .await?;
136
137 if !res.status().is_success() {
138 return Err(Error::msg(res.text().await?));
139 }
140
141 let b = res.text().await?;
142 Ok(Hash(b))
143 }
144
145 pub async fn remove(&self, path: impl Into<Path>) -> Result<(), Error> {
146 let path = path.into();
147 let p = format!("/module/{}", path.to_string());
148 let res = self
149 .request(reqwest::Method::DELETE, self.endpoint(&p), None)
150 .await?;
151 if !res.status().is_success() {
152 return Err(Error::msg(res.text().await?));
153 }
154 Ok(())
155 }
156
157 pub async fn gc(&self) -> Result<(), Error> {
158 let res = self
159 .request(reqwest::Method::POST, self.endpoint("/gc"), None)
160 .await?;
161 if !res.status().is_success() {
162 return Err(Error::msg(res.text().await?));
163 }
164 Ok(())
165 }
166
167 pub async fn list(
168 &self,
169 path: impl Into<Path>,
170 ) -> Result<std::collections::BTreeMap<Path, Hash>, Error> {
171 let path = path.into();
172 let p = format!("/modules/{}", path.to_string());
173 let res = self
174 .request(reqwest::Method::GET, self.endpoint(&p), None)
175 .await?;
176 if !res.status().is_success() {
177 return Err(Error::msg(res.text().await?));
178 }
179 let res: std::collections::BTreeMap<String, String> = res.json().await?;
180 Ok(res
181 .into_iter()
182 .map(|(k, v)| (Path::String(k), Hash(v)))
183 .collect())
184 }
185
186 pub async fn versions(&self, path: impl Into<Path>) -> Result<Vec<(Hash, Commit)>, Error> {
187 let url = format!("/versions/{}", path.into().to_string());
188 let res = self
189 .request(reqwest::Method::GET, self.endpoint(&url), None)
190 .await?;
191 if !res.status().is_success() {
192 return Err(Error::msg(res.text().await?));
193 }
194 Ok(res.json().await?)
195 }
196
197 pub async fn branches(&self) -> Result<Vec<String>, Error> {
198 let res = self
199 .request(reqwest::Method::GET, self.endpoint("/branches"), None)
200 .await?;
201 if !res.status().is_success() {
202 return Err(Error::msg(res.text().await?));
203 }
204 Ok(res.json().await?)
205 }
206
207 pub async fn create_branch(&self, name: impl AsRef<str>) -> Result<(), Error> {
208 let p = format!("/branch/{}", name.as_ref());
209 let res = self
210 .request(reqwest::Method::POST, self.endpoint(&p), None)
211 .await?;
212 if !res.status().is_success() {
213 return Err(Error::msg(res.text().await?));
214 }
215 Ok(())
216 }
217
218 pub async fn delete_branch(&self, name: impl AsRef<str>) -> Result<(), Error> {
219 let p = format!("/branch/{}", name.as_ref());
220 let res = self
221 .request(reqwest::Method::DELETE, self.endpoint(&p), None)
222 .await?;
223 if !res.status().is_success() {
224 return Err(Error::msg(res.text().await?));
225 }
226 Ok(())
227 }
228
229 pub async fn snapshot(&self) -> Result<Commit, Error> {
230 let res = self
231 .request(reqwest::Method::GET, self.endpoint("/snapshot"), None)
232 .await?;
233 if !res.status().is_success() {
234 return Err(Error::msg(res.text().await?));
235 }
236 let hash = res.text().await?;
237 Ok(Commit(hash))
238 }
239
240 pub async fn restore(&self, hash: &Commit) -> Result<(), Error> {
241 let res = self
242 .request(
243 reqwest::Method::POST,
244 self.endpoint(&format!("/restore/{}", hash.0)),
245 None,
246 )
247 .await?;
248 if !res.status().is_success() {
249 return Err(Error::msg(res.text().await?));
250 }
251 Ok(())
252 }
253
254 pub async fn restore_path(&self, hash: &Commit, path: impl Into<Path>) -> Result<(), Error> {
255 let res = self
256 .request(
257 reqwest::Method::POST,
258 self.endpoint(&format!("/restore/{}/{}", hash.0, path.into().to_string())),
259 None,
260 )
261 .await?;
262 if !res.status().is_success() {
263 return Err(Error::msg(res.text().await?));
264 }
265 Ok(())
266 }
267
268 pub async fn rollback(&self, path: impl Into<Path>) -> Result<(), Error> {
269 let res = self
270 .request(
271 reqwest::Method::POST,
272 self.endpoint(&format!("/rollback/{}", path.into().to_string())),
273 None,
274 )
275 .await?;
276 if !res.status().is_success() {
277 return Err(Error::msg(res.text().await?));
278 }
279 Ok(())
280 }
281
282 pub async fn contains(&self, path: impl Into<Path>) -> Result<bool, Error> {
283 let res = self
284 .request(
285 reqwest::Method::HEAD,
286 self.endpoint(&format!("/module/{}", path.into().to_string())),
287 None,
288 )
289 .await?;
290 Ok(res.status().is_success())
291 }
292
293 pub async fn set(&self, path: impl Into<Path>, hash: &Hash) -> Result<Hash, Error> {
294 let path = path.into();
295 let p = format!("/hash/{}/{}", hash.0, path.to_string());
296 let res = self
297 .request(reqwest::Method::POST, self.endpoint(&p), None)
298 .await?;
299
300 if !res.status().is_success() {
301 return Err(Error::msg(res.text().await?));
302 }
303
304 let b = res.text().await?;
305 Ok(Hash(b))
306 }
307
308 pub async fn commit_info(&self, commit: &Commit) -> Result<CommitInfo, Error> {
309 let p = format!("/commit/{}", commit.0);
310 let res = self
311 .request(reqwest::Method::GET, self.endpoint(&p), None)
312 .await?;
313 if !res.status().is_success() {
314 return Err(Error::msg(res.text().await?));
315 }
316 let res: CommitInfo = res.json().await?;
317 Ok(res)
318 }
319
320 pub async fn auth(&self, method: reqwest::Method) -> Result<bool, Error> {
321 let res = self.request(method, self.endpoint("/auth"), None).await?;
322 Ok(res.status() == 200)
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use crate::*;
329
330 #[tokio::test]
331 async fn basic_test() {
332 let client = Client::new("http://127.0.0.1:6384", Version::V1).unwrap();
333
334 let data = std::fs::read("../../test/a.wasm").unwrap();
335 let hash = client.add("test.wasm", data).await;
336 println!("HASH: {hash:?}");
337
338 let data = client.find(hash.unwrap()).await.unwrap();
339 let data1 = client.find("test.wasm").await.unwrap();
340
341 let hash = client.snapshot().await.unwrap();
342 client.commit_info(&hash).await.unwrap();
343
344 client.remove("test.wasm").await.unwrap();
345
346 client.restore(&hash).await.unwrap();
347
348 assert!(data.is_some());
349 assert!(data1.is_some());
350 assert_eq!(data, data1);
351 }
352}