Skip to main content

opendal_service_ghac/
backend.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// GitHub Action Cache Services support.
54#[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    /// set the working directory root of backend
70    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    /// set the version that used by cache.
81    ///
82    /// The version is the unique value that provides namespacing.
83    /// It's better to make sure this value is only used by this backend.
84    ///
85    /// If not set, we will use `opendal` as default.
86    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    /// Set the endpoint for ghac service.
95    ///
96    /// For example, this is provided as the `ACTIONS_CACHE_URL` environment variable by the GHA runner.
97    ///
98    /// Default: the value of the `ACTIONS_CACHE_URL` environment variable.
99    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    /// Set the runtime token for ghac service.
107    ///
108    /// For example, this is provided as the `ACTIONS_RUNTIME_TOKEN` environment variable by the GHA
109    /// runner.
110    ///
111    /// Default: the value of the `ACTIONS_RUNTIME_TOKEN` environment variable.
112    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        // ghac requires to use hex digest of Sha256 as version.
139        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/// Backend for github action cache services.
200#[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    /// Some self-hosted GHES instances are backed by AWS S3 services which only returns
234    /// signed url with `GET` method. So we will use `GET` with empty range to simulate
235    /// `HEAD` instead.
236    ///
237    /// In this way, we can support both self-hosted GHES and `github.com`.
238    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}