Initial stabilization #1
6 changed files with 1589 additions and 0 deletions
26
.forgejo/workflows/build-action.yml
Normal file
26
.forgejo/workflows/build-action.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: Build and update the action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Build and update the action
|
||||
uses: https://git.dzuchun.ing/actions/anode-build@main
|
||||
with:
|
||||
src-repository: https://git.dzuchun.ing/actions/cache-load-src.git
|
||||
src-ref: ${{ forgejo.ref_name }}
|
||||
src-token: ${{ secrets.GIT_TOKEN }}
|
||||
dst-repository: https://git.dzuchun.ing/actions/cache-load.git
|
||||
dst-ref: ${{ forgejo.ref_name }}
|
||||
dst-token: ${{ secrets.GIT_TOKEN }}
|
||||
dst-email: forgejo-actions@dzuchun.ing
|
||||
opt-release: ${{ forgejo.ref_name == 'main' }}
|
||||
opt-build-std: ${{ forgejo.ref_name == 'main' }}
|
||||
opt-upx: ${{ forgejo.ref_name == 'main' }}
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
action.yml
|
||||
1387
Cargo.lock
generated
Normal file
1387
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
31
Cargo.toml
Normal file
31
Cargo.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "cache-load"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.101"
|
||||
flate2 = "1.1.9"
|
||||
secrecy = "0.10.3"
|
||||
tar = "0.4.44"
|
||||
tracing = { version = "0.1.44", features = ["attributes"] }
|
||||
|
||||
[dependencies.anode]
|
||||
git = "https://git.dzuchun.ing/dzu/anode.git"
|
||||
rev="98addf4d54ae16c21979a1dae63d17738b3a277b"
|
||||
|
||||
[dependencies.anode-cache-common]
|
||||
git = "https://git.dzuchun.ing/actions/cache-common-src.git"
|
||||
rev="5656a3ff6f58a48e3365c221fb2102918db6c284"
|
||||
|
||||
[build-dependencies.anode-build]
|
||||
git = "https://git.dzuchun.ing/dzu/anode.git"
|
||||
rev="98addf4d54ae16c21979a1dae63d17738b3a277b"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
incremental = false
|
||||
3
build.rs
Normal file
3
build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
anode_build::build();
|
||||
}
|
||||
140
src/main.rs
Normal file
140
src/main.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anode_cache_common::{CacheKey, Client, actions_token, key_parser, paths_parser};
|
||||
use anyhow::{Context, Result};
|
||||
use flate2::read::GzDecoder;
|
||||
use secrecy::ExposeSecret;
|
||||
use tar::Archive;
|
||||
use tracing::{debug, info, info_span, warn};
|
||||
|
||||
#[derive(Debug, anode::Input)]
|
||||
#[name = "Cache load"]
|
||||
struct Input {
|
||||
/// A set of paths to load
|
||||
///
|
||||
/// Must be in UTF-8
|
||||
#[parser(paths_parser)]
|
||||
path: Vec<PathBuf>,
|
||||
/// A key to cache the files with
|
||||
///
|
||||
/// Allowed to be at most 512 characters, and should not contain commas
|
||||
///
|
||||
/// Must be in UTF-8
|
||||
#[parser(key_parser)]
|
||||
key: CacheKey,
|
||||
/// If the debug log should be printed
|
||||
///
|
||||
/// If omitted, defined by compiler settings (print debug if `debug_assertions` is enabled)
|
||||
debug: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum PathState {
|
||||
Unused,
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// parse inputs
|
||||
let Input { path, key, debug } = Input::get();
|
||||
anode_cache_common::setup_logger(debug.unwrap_or(cfg!(debug_assertions)));
|
||||
|
||||
let actions_token = actions_token().context("reading actions token")?;
|
||||
|
||||
let cache_url = std::env::var("ACTIONS_CACHE_URL").context("reading action cache url")?;
|
||||
|
||||
// set up http client
|
||||
let client = Client::new(actions_token.expose_secret(), cache_url)
|
||||
.context("intializing cache client")?;
|
||||
debug!("Cache client initialized");
|
||||
|
||||
// download the cache archive
|
||||
let Some(cache) = client.load_cache(&key).context("loading the cache")? else {
|
||||
info!("Cache miss");
|
||||
return Ok(());
|
||||
};
|
||||
let mut cache = &*cache;
|
||||
|
||||
let in_uncomp = GzDecoder::new(&mut cache);
|
||||
let mut archive = Archive::new(in_uncomp);
|
||||
|
||||
debug!(paths = ?path);
|
||||
let mut paths = path
|
||||
.into_iter()
|
||||
.map(|p| (p, PathState::Unused))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// tmp buffers
|
||||
let mut entry_path_buf = PathBuf::new();
|
||||
let mut directory_path_buf = PathBuf::new();
|
||||
for entry in archive.entries().context("reading cache entries")? {
|
||||
let mut entry = entry.context("reading cache entry")?;
|
||||
{
|
||||
let entry_path = entry.path().context("reading entry path")?;
|
||||
entry_path_buf.clear();
|
||||
entry_path_buf.push(&entry_path);
|
||||
}
|
||||
let _span = info_span!("archive-entry", path = %entry_path_buf.display());
|
||||
let _span = _span.enter();
|
||||
if let Some(state) = paths.get_mut(&entry_path_buf) {
|
||||
let new_state = if entry.header().entry_type().is_dir() {
|
||||
PathState::Directory
|
||||
} else {
|
||||
PathState::File
|
||||
};
|
||||
debug!(?new_state, "Exact match on one of the cached paths");
|
||||
// match on (old, new)
|
||||
match (core::mem::replace(state, new_state), new_state) {
|
||||
(PathState::Unused, _) => {
|
||||
entry
|
||||
.unpack(&entry_path_buf)
|
||||
.context("writing cache entry")?;
|
||||
debug!("Unpacked normally");
|
||||
}
|
||||
(PathState::File, PathState::File) => {
|
||||
warn!("Double file in the cache");
|
||||
}
|
||||
(PathState::Directory, PathState::Directory) => {
|
||||
debug!("Directory found again");
|
||||
// entry could be marked as directory because of previous contained file entries; nothing to warn about here
|
||||
}
|
||||
(PathState::Directory, PathState::File) => {
|
||||
warn!("A directory that was previously assumed to be a regular file");
|
||||
}
|
||||
(PathState::File, PathState::Directory) => {
|
||||
warn!("A regular file that was previously assumed to be a directory");
|
||||
}
|
||||
(_, PathState::Unused) => unreachable!(),
|
||||
}
|
||||
} else if let Some(directory) = paths.keys().find(|p| entry_path_buf.starts_with(p)) {
|
||||
debug!(directory = %directory.display(), "File in a directory");
|
||||
directory_path_buf.clone_from(directory);
|
||||
// file in a cached directory
|
||||
let state = paths
|
||||
.get_mut(&directory_path_buf)
|
||||
.expect("paths should contain a path that was just extracted as a key");
|
||||
match core::mem::replace(state, PathState::Directory) {
|
||||
PathState::Unused | PathState::Directory => {
|
||||
entry
|
||||
.unpack(&entry_path_buf)
|
||||
.context("writing cache entry")?;
|
||||
debug!("Unpacked normally");
|
||||
}
|
||||
PathState::File => {
|
||||
warn!("A directory that was previously assumed to be a regular file",);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Unmatched entry in the archive");
|
||||
}
|
||||
}
|
||||
|
||||
for (path, state) in paths {
|
||||
if state == PathState::Unused {
|
||||
warn!(path = %path.display(), "Cached path was not found in the cache",);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue