Initial stabilization #1

Merged
dzu merged 10 commits from stabilize-v1 into main 2026-02-20 16:23:43 +01:00
4 changed files with 1269 additions and 0 deletions
Showing only changes of commit 254f547719 - Show all commits

initial common version

Artem Diachenko 2026-02-19 10:04:36 +00:00

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1086
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "anode-cache-common"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.101"
isahc = { version = "1.7.2", features = ["static-ssl", "static-curl"] }
serde_json = "1.0.149"
secrecy = "0.10.3"
thiserror = "2.0.18"

171
src/lib.rs Normal file
View file

@ -0,0 +1,171 @@
use std::{ffi::OsString, io::Read, path::PathBuf, str::FromStr, string::FromUtf8Error};
use anyhow::{Context, Result, bail};
use isahc::{
HttpClient, HttpClientBuilder, Request,
auth::{Authentication, Credentials},
config::Configurable,
http::{Method, StatusCode, header},
};
use secrecy::{ExposeSecret, SecretString};
pub fn paths_parser(input: OsString) -> Result<Vec<PathBuf>, FromUtf8Error> {
let input = String::from_utf8(input.into_encoded_bytes())?;
let mut paths = Vec::new();
for path in input.split('\n').filter(|s| !s.is_empty()) {
paths.push(match PathBuf::from_str(path) {
Ok(ok) => ok,
});
}
Ok(paths)
}
#[derive(Debug)]
pub struct CacheKey(String);
#[derive(Debug, thiserror::Error)]
pub enum CacheKeyError {
#[error(transparent)]
Utf8(#[from] FromUtf8Error),
#[error("Cache key is at most 512 characters long. {len} characters long key provided")]
Length { len: usize },
#[error("Cache key cannot contain commas. Comma found at offset {offset}")]
Comma { offset: usize },
}
pub fn key_parser(input: OsString) -> Result<CacheKey, CacheKeyError> {
let input = String::from_utf8(input.into_encoded_bytes())?;
let mut len = 0;
for (offset, ch) in input.char_indices() {
if ch == ',' {
return Err(CacheKeyError::Comma { offset });
}
len += 1;
}
if len > 512 {
return Err(CacheKeyError::Length { len });
}
Ok(CacheKey(input))
}
#[derive(Debug, thiserror::Error)]
pub enum ActionsTokenError {
#[error("Could not find action runtime token")]
NotPresent,
#[error("Action runtime token was not UTF-8 -encoded")]
NotUtf8,
}
pub fn actions_token() -> Result<SecretString, ActionsTokenError> {
let os_str = std::env::var_os("ACTIONS_RUNTIME_TOKEN").ok_or(ActionsTokenError::NotPresent)?;
let str =
String::from_utf8(os_str.into_encoded_bytes()).map_err(|_| ActionsTokenError::NotUtf8)?;
Ok(SecretString::new(str.into_boxed_str()))
}
pub struct Client {
http_client: HttpClient,
base_url: String,
}
#[derive(Debug, Clone, Copy)]
pub struct CacheId(u64);
impl Client {
pub fn new(actions_token: SecretString, base_url: String) -> Result<Self> {
let http_client = HttpClientBuilder::new()
.authentication(Authentication::basic())
.credentials(Credentials::new("Bearer", actions_token.expose_secret()))
.build()
.context("initializing the client")?;
Ok(Self {
http_client,
base_url,
})
}
pub fn save_cache(&self, key: &CacheKey, cache: Vec<u8>) -> Result<()> {
let cache_id = self.reserve(key, cache.len()).context("reserving cache")?;
self.save(cache_id, cache).context("saving the cache")?;
Ok(())
}
fn reserve(&self, key: &CacheKey, cache_size: usize) -> Result<CacheId> {
let uri = format!("{}_apis/artifactcache/caches", self.base_url);
let request = Request::builder()
.uri(uri)
.method(Method::POST)
.body(
serde_json::json! {{
"key": &key.0,
"version": "0",
"cacheSize": cache_size,
}}
.to_string(),
)
.context("building the request")?;
let mut response = self
.http_client
.send(request)
.context("sending the request")?;
let status = response.status();
let mut body = Vec::with_capacity(100);
response
.body_mut()
.read_to_end(&mut body)
.context("reading response body")?;
if status != StatusCode::OK {
bail!(
"Failed reserving a cache (status {status}): {:?}",
String::from_utf8_lossy(&body)
);
}
let body_value =
serde_json::from_slice::<serde_json::Value>(&body).context("parsing response body")?;
println!("{body_value:#?}");
let cache_id = body_value
.get("cacheId")
.context("response.cacheId")?
.as_u64()
.context("cacheId as u64")?;
Ok(CacheId(cache_id))
}
fn save(&self, id: CacheId, archive: Vec<u8>) -> Result<()> {
let uri = format!("{}_apis/artifactcache/caches/{}", self.base_url, id.0);
let request = Request::builder()
.uri(uri)
.method(Method::PATCH)
.header(header::CONTENT_TYPE, "application/octet-stream")
.header(
header::CONTENT_RANGE,
format!("bytes {}-{}/*", 0, archive.len()),
)
.body(archive)
.context("building the request")?;
let mut response = self
.http_client
.send(request)
.context("sending the request")?;
let status = response.status();
let mut body = Vec::with_capacity(100);
response
.body_mut()
.read_to_end(&mut body)
.context("reading response body")?;
if status != StatusCode::OK {
bail!(
"Failed saving a cache (status {status}): {:?}",
String::from_utf8_lossy(&body)
);
}
Ok(())
}
}