1use std::env;
19use std::fmt::Debug;
20use std::sync::Arc;
21
22use http::StatusCode;
23use log::debug;
24use sha2::Digest;
25
26use super::GHAC_SCHEME;
27use super::config::GhacConfig;
28use super::core::GhacCore;
29use super::core::parse_error;
30use super::core::*;
31use super::reader::*;
32use super::writer::GhacLazyWriter;
33use opendal_core::raw::*;
34use opendal_core::*;
35
36fn value_or_env(
37 explicit_value: Option<String>,
38 env_var_name: &str,
39 operation: &'static str,
40) -> Result<String> {
41 if let Some(value) = explicit_value {
42 return Ok(value);
43 }
44
45 env::var(env_var_name).map_err(|err| {
46 let text = format!("{env_var_name} not found, maybe not in github action environment?");
47 Error::new(ErrorKind::ConfigInvalid, text)
48 .with_operation(operation)
49 .set_source(err)
50 })
51}
52
53#[doc = include_str!("docs.md")]
55#[derive(Default)]
56pub struct GhacBuilder {
57 pub(super) config: GhacConfig,
58}
59
60impl Debug for GhacBuilder {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("GhacBuilder")
63 .field("config", &self.config)
64 .finish_non_exhaustive()
65 }
66}
67
68impl GhacBuilder {
69 pub fn root(mut self, root: &str) -> Self {
71 self.config.root = if root.is_empty() {
72 None
73 } else {
74 Some(root.to_string())
75 };
76
77 self
78 }
79
80 pub fn version(mut self, version: &str) -> Self {
87 if !version.is_empty() {
88 self.config.version = Some(version.to_string())
89 }
90
91 self
92 }
93
94 pub fn endpoint(mut self, endpoint: &str) -> Self {
100 if !endpoint.is_empty() {
101 self.config.endpoint = Some(endpoint.to_string())
102 }
103 self
104 }
105
106 pub fn runtime_token(mut self, runtime_token: &str) -> Self {
113 if !runtime_token.is_empty() {
114 self.config.runtime_token = Some(runtime_token.to_string())
115 }
116 self
117 }
118}
119
120impl Builder for GhacBuilder {
121 type Config = GhacConfig;
122
123 fn build(self) -> Result<impl Service> {
124 debug!("backend build started: {self:?}");
125
126 let root = normalize_root(&self.config.root.unwrap_or_default());
127 debug!("backend use root {root}");
128
129 let service_version = get_cache_service_version();
130 debug!("backend use service version {service_version:?}");
131
132 let mut version = self
133 .config
134 .version
135 .clone()
136 .unwrap_or_else(|| "opendal".to_string());
137 debug!("backend use version {version}");
138 if matches!(service_version, GhacVersion::V2) {
140 let hash = sha2::Sha256::digest(&version);
141 version = format_digest_hex(hash);
142 }
143
144 let cache_url = self
145 .config
146 .endpoint
147 .unwrap_or_else(|| get_cache_service_url(service_version));
148 if cache_url.is_empty() {
149 return Err(Error::new(
150 ErrorKind::ConfigInvalid,
151 "cache url for ghac not found, maybe not in github action environment?".to_string(),
152 ));
153 }
154
155 let core = GhacCore {
156 info: ServiceInfo::new(GHAC_SCHEME, &root, &version),
157 capability: Capability {
158 stat: true,
159
160 read: true,
161
162 write: true,
163 write_can_multi: true,
164
165 shared: true,
166
167 ..Default::default()
168 },
169 root,
170
171 cache_url,
172 catch_token: value_or_env(
173 self.config.runtime_token,
174 ACTIONS_RUNTIME_TOKEN,
175 "Builder::build",
176 )?,
177 version,
178
179 service_version,
180 };
181
182 Ok(GhacBackend {
183 core: Arc::new(core),
184 })
185 }
186}
187
188fn format_digest_hex(digest: impl AsRef<[u8]>) -> String {
189 use std::fmt::Write;
190
191 let digest = digest.as_ref();
192 let mut output = String::with_capacity(digest.len() * 2);
193 for byte in digest {
194 write!(&mut output, "{byte:02x}").expect("writing to String must succeed");
195 }
196 output
197}
198
199#[derive(Debug, Clone)]
201pub struct GhacBackend {
202 pub(crate) core: Arc<GhacCore>,
203}
204
205impl Service for GhacBackend {
206 type Reader = oio::StreamReader<GhacReader>;
207 type Writer = GhacLazyWriter;
208 type Lister = ();
209 type Deleter = ();
210 type Copier = ();
211 type Composer = ();
212
213 fn info(&self) -> ServiceInfo {
214 self.core.info.clone()
215 }
216
217 fn capability(&self) -> Capability {
218 self.core.capability
219 }
220
221 async fn create_dir(
222 &self,
223 _ctx: &OperationContext,
224 _path: &str,
225 _args: OpCreateDir,
226 ) -> Result<RpCreateDir> {
227 Err(Error::new(
228 ErrorKind::Unsupported,
229 "operation is not supported",
230 ))
231 }
232
233 async fn stat(&self, ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
239 let resp = self.core.ghac_stat(ctx, path).await?;
240
241 let status = resp.status();
242 match status {
243 StatusCode::OK | StatusCode::PARTIAL_CONTENT | StatusCode::RANGE_NOT_SATISFIABLE => {
244 let meta = parse_into_metadata(path, resp.headers())?;
245 Ok(RpStat::new(meta))
246 }
247 _ => Err(parse_error(
248 ErrorContext::new(ServiceOperation("GetCacheEntry")),
249 resp,
250 )),
251 }
252 }
253 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
254 let output: oio::StreamReader<GhacReader> = {
255 Ok(oio::StreamReader::new(GhacReader::new(
256 self.clone(),
257 ctx.clone(),
258 path,
259 args,
260 )))
261 }?;
262
263 Ok(output)
264 }
265
266 fn write(&self, ctx: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> {
267 Ok(GhacLazyWriter::new(
268 self.core.clone(),
269 ctx.clone(),
270 ctx.executor().clone(),
271 path.to_string(),
272 ))
273 }
274
275 fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
276 Err(Error::new(
277 ErrorKind::Unsupported,
278 "operation is not supported",
279 ))
280 }
281
282 fn list(&self, _ctx: &OperationContext, _path: &str, _args: OpList) -> Result<Self::Lister> {
283 Err(Error::new(
284 ErrorKind::Unsupported,
285 "operation is not supported",
286 ))
287 }
288
289 fn copy(
290 &self,
291 _ctx: &OperationContext,
292 _from: &str,
293 _to: &str,
294 _args: OpCopy,
295 ) -> Result<Self::Copier> {
296 Err(Error::new(
297 ErrorKind::Unsupported,
298 "operation is not supported",
299 ))
300 }
301
302 async fn rename(
303 &self,
304 _ctx: &OperationContext,
305 _from: &str,
306 _to: &str,
307 _args: OpRename,
308 ) -> Result<RpRename> {
309 Err(Error::new(
310 ErrorKind::Unsupported,
311 "operation is not supported",
312 ))
313 }
314
315 async fn presign(
316 &self,
317 _ctx: &OperationContext,
318 _path: &str,
319 _args: OpPresign,
320 ) -> Result<RpPresign> {
321 Err(Error::new(
322 ErrorKind::Unsupported,
323 "operation is not supported",
324 ))
325 }
326}