pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/store.rs

225 lines7,198 bytesCodeBlame
1//! The storage that actually holds git repositories.
2//!
3//! The service depends on the [`GitStore`] and [`GitRepo`] ports;
4//! [`ArtifactsStore`] is the adapter for Cloudflare Artifacts.
5
6use g1t_contracts::repos::{Commit, EntryKind, GitAccess, Signature, TreeEntry};
7use g1t_kit::js;
8use serde::Deserialize;
9use worker::js_sys::{Reflect, Uint8Array};
10use worker::wasm_bindgen::{JsCast, JsValue};
11use worker::{Env, Result};
12
13/// How long a credential handed to git stays valid.
14const TOKEN_TTL_SECONDS: u32 = 300;
15
16#[derive(Clone, Copy)]
17pub enum Scope {
18 Read,
19 Write,
20}
21
22/// A place repositories live. `key` is the store's own name for a repo.
23#[allow(async_fn_in_trait)]
24pub trait GitStore {
25 type Repo: GitRepo;
26
27 /// Creates an empty repository. Succeeds if it already exists.
28 async fn create(
29 &self,
30 key: &str,
31 description: Option<&str>,
32 default_branch: &str,
33 ) -> Result<()>;
34 async fn open(&self, key: &str) -> Result<Self::Repo>;
35}
36
37/// One open repository.
38#[allow(async_fn_in_trait)]
39pub trait GitRepo {
40 /// A remote URL and short-lived credential for git itself.
41 async fn access(&self, scope: Scope) -> Result<GitAccess>;
42 /// Newest first along the first-parent chain; empty for an unknown ref.
43 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>>;
44 /// The parents of a commit, or `None` if the commit does not exist.
45 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>>;
46 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>>;
47 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>>;
48 /// `None` when the ref or path does not resolve to a file.
49 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>>;
50 /// Makes a copy-on-write copy of this repository under `target_key`.
51 async fn fork(&self, target_key: &str) -> Result<()>;
52}
53
54pub struct ArtifactsStore {
55 binding: JsValue,
56}
57
58impl ArtifactsStore {
59 pub fn new(env: &Env) -> Result<Self> {
60 Ok(Self {
61 binding: js::binding(env, "ARTIFACTS")?,
62 })
63 }
64}
65
66impl GitStore for ArtifactsStore {
67 type Repo = ArtifactsRepo;
68
69 async fn create(
70 &self,
71 key: &str,
72 description: Option<&str>,
73 default_branch: &str,
74 ) -> Result<()> {
75 let options = js::to_js(&serde_json::json!({
76 "description": description,
77 "setDefaultBranch": default_branch,
78 }))?;
79 match js::call(&self.binding, "create", &[key.into(), options]).await {
80 // Left behind by an earlier failed attempt; adopt it.
81 Err(thrown) if !thrown.is("ALREADY_EXISTS") => Err(thrown.into()),
82 _ => Ok(()),
83 }
84 }
85
86 async fn open(&self, key: &str) -> Result<ArtifactsRepo> {
87 Ok(ArtifactsRepo {
88 handle: js::call(&self.binding, "get", &[key.into()]).await?,
89 })
90 }
91}
92
93/// A handle to one Artifacts repository. It is an RPC stub, so it is
94/// released when dropped.
95pub struct ArtifactsRepo {
96 handle: JsValue,
97}
98
99impl Drop for ArtifactsRepo {
100 fn drop(&mut self) {
101 let symbol = js::get(&worker::js_sys::global(), "Symbol");
102 let dispose = js::get(&symbol, "dispose");
103 if let Ok(function) = Reflect::get(&self.handle, &dispose)
104 .and_then(|value| value.dyn_into::<worker::js_sys::Function>())
105 {
106 let _ = function.call0(&self.handle);
107 }
108 }
109}
110
111#[derive(Deserialize)]
112#[serde(rename_all = "camelCase")]
113struct RawCommit {
114 hash: String,
115 tree_hash: String,
116 message: String,
117 author: Signature,
118 parents: Vec<String>,
119 /// Seconds since the epoch.
120 authored_at: u64,
121}
122
123#[derive(Deserialize)]
124struct RawEntry {
125 name: String,
126 hash: String,
127 #[serde(rename = "type")]
128 kind: EntryKind,
129}
130
131#[derive(Deserialize)]
132struct RawInfo {
133 remote: String,
134}
135
136#[derive(Deserialize)]
137struct RawToken {
138 plaintext: String,
139}
140
141/// The bytes of a `Blob`, or `None` for null.
142async fn blob_bytes(blob: JsValue) -> Result<Option<Vec<u8>>> {
143 if blob.is_null() || blob.is_undefined() {
144 return Ok(None);
145 }
146 let buffer = js::call(&blob, "arrayBuffer", &[]).await?;
147 Ok(Some(Uint8Array::new(&buffer).to_vec()))
148}
149
150impl GitRepo for ArtifactsRepo {
151 async fn access(&self, scope: Scope) -> Result<GitAccess> {
152 let scope = match scope {
153 Scope::Read => "read",
154 Scope::Write => "write",
155 };
156 let info: RawInfo = js::from_js(&js::call(&self.handle, "info", &[]).await?)?;
157 let token: RawToken = js::from_js(
158 &js::call(
159 &self.handle,
160 "createToken",
161 &[scope.into(), TOKEN_TTL_SECONDS.into()],
162 )
163 .await?,
164 )?;
165 Ok(GitAccess {
166 remote: info.remote,
167 token: token.plaintext,
168 })
169 }
170
171 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
172 let options = js::to_js(&serde_json::json!({ "ref": git_ref, "limit": limit }))?;
173 let commits: Vec<RawCommit> =
174 js::from_js(&js::call(&self.handle, "log", &[options]).await?)?;
175 Ok(commits
176 .into_iter()
177 .map(|commit| Commit {
178 hash: commit.hash,
179 tree_hash: commit.tree_hash,
180 message: commit.message,
181 author: commit.author,
182 parents: commit.parents,
183 authored_at: commit.authored_at * 1000,
184 })
185 .collect())
186 }
187
188 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
189 let commit: Option<RawCommit> =
190 js::from_js(&js::call(&self.handle, "readCommit", &[commit_hash.into()]).await?)?;
191 Ok(commit.map(|commit| commit.parents))
192 }
193
194 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
195 let entries: Option<Vec<RawEntry>> =
196 js::from_js(&js::call(&self.handle, "readTree", &[tree_hash.into()]).await?)?;
197 Ok(entries.map(|entries| {
198 entries
199 .into_iter()
200 .map(|entry| TreeEntry {
201 name: entry.name,
202 hash: entry.hash,
203 kind: entry.kind,
204 })
205 .collect()
206 }))
207 }
208
209 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
210 blob_bytes(js::call(&self.handle, "readBlob", &[blob_hash.into()]).await?).await
211 }
212
213 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>> {
214 let args = js::to_js(&serde_json::json!({ "ref": git_ref, "path": path }))?;
215 blob_bytes(js::call(&self.handle, "readFile", &[args]).await?).await
216 }
217
218 async fn fork(&self, target_key: &str) -> Result<()> {
219 let options = js::to_js(&serde_json::json!({ "defaultBranchOnly": true }))?;
220 match js::call(&self.handle, "fork", &[target_key.into(), options]).await {
221 Err(thrown) if !thrown.is("ALREADY_EXISTS") => Err(thrown.into()),
222 _ => Ok(()),
223 }
224 }
225}