2025-10-07 10:32:04 +01:00

443 lines
12 KiB
Plaintext

# Extension Cache System
# Manages local caching of extensions from OCI, Gitea, and other sources
use ../config/accessor.nu *
use ../utils/logger.nu *
use ../oci/client.nu *
# Get cache directory for extensions
export def get-cache-dir []: nothing -> string {
let base_cache = ($env.HOME | path join ".provisioning" "cache" "extensions")
if not ($base_cache | path exists) {
mkdir $base_cache
}
$base_cache
}
# Get cache path for specific extension
export def get-cache-path [
extension_type: string
extension_name: string
version: string
]: nothing -> string {
let cache_dir = (get-cache-dir)
$cache_dir | path join $extension_type $extension_name $version
}
# Get cache index file
def get-cache-index-file []: nothing -> string {
let cache_dir = (get-cache-dir)
$cache_dir | path join "index.json"
}
# Load cache index
export def load-cache-index []: nothing -> record {
let index_file = (get-cache-index-file)
if ($index_file | path exists) {
open $index_file | from json
} else {
{
extensions: {}
metadata: {
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
last_updated: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
}
}
}
}
# Save cache index
export def save-cache-index [index: record]: nothing -> nothing {
let index_file = (get-cache-index-file)
$index
| update metadata.last_updated (date now | format date "%Y-%m-%dT%H:%M:%SZ")
| to json
| save -f $index_file
}
# Update cache index for specific extension
export def update-cache-index [
extension_type: string
extension_name: string
version: string
metadata: record
]: nothing -> nothing {
let index = (load-cache-index)
let key = $"($extension_type)/($extension_name)/($version)"
let entry = {
type: $extension_type
name: $extension_name
version: $version
cached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
source_type: ($metadata.source_type? | default "unknown")
metadata: $metadata
}
let updated_index = ($index | update extensions {
$in | insert $key $entry
})
save-cache-index $updated_index
}
# Get extension from cache
export def get-from-cache [
extension_type: string
extension_name: string
version?: string
]: nothing -> record {
let cache_dir = (get-cache-dir)
let extension_cache_dir = ($cache_dir | path join $extension_type $extension_name)
if not ($extension_cache_dir | path exists) {
return {found: false}
}
# If version specified, check exact version
if ($version | is-not-empty) {
let version_path = ($extension_cache_dir | path join $version)
if ($version_path | path exists) {
return {
found: true
path: $version_path
version: $version
metadata: (get-cache-metadata $extension_type $extension_name $version)
}
} else {
return {found: false}
}
}
# If no version specified, get latest cached version
let versions = (ls $extension_cache_dir | where type == dir | get name | path basename)
if ($versions | is-empty) {
return {found: false}
}
# Sort versions and get latest
let latest = ($versions | sort-by-semver | last)
let latest_path = ($extension_cache_dir | path join $latest)
{
found: true
path: $latest_path
version: $latest
metadata: (get-cache-metadata $extension_type $extension_name $latest)
}
}
# Get cache metadata for extension
def get-cache-metadata [
extension_type: string
extension_name: string
version: string
]: nothing -> record {
let index = (load-cache-index)
let key = $"($extension_type)/($extension_name)/($version)"
$index.extensions | get -o $key | default {}
}
# Save OCI artifact to cache
export def save-oci-to-cache [
extension_type: string
extension_name: string
version: string
artifact_path: string
manifest: record
]: nothing -> bool {
try {
let cache_path = (get-cache-path $extension_type $extension_name $version)
log-debug $"Saving OCI artifact to cache: ($cache_path)"
# Create cache directory
mkdir $cache_path
# Copy extracted artifact
let artifact_contents = (ls $artifact_path | get name)
for file in $artifact_contents {
cp -r $file $cache_path
}
# Save OCI manifest
$manifest | to json | save $"($cache_path)/oci-manifest.json"
# Update cache index
update-cache-index $extension_type $extension_name $version {
source_type: "oci"
cached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
oci_digest: ($manifest.config?.digest? | default "")
}
log-info $"Cached ($extension_name):($version) from OCI"
true
} catch { |err|
log-error $"Failed to save OCI artifact to cache: ($err.msg)"
false
}
}
# Get OCI artifact from cache
export def get-oci-from-cache [
extension_type: string
extension_name: string
version?: string
]: nothing -> record {
let cache_entry = (get-from-cache $extension_type $extension_name $version)
if not $cache_entry.found {
return {found: false}
}
# Verify OCI manifest exists
let manifest_path = $"($cache_entry.path)/oci-manifest.json"
if not ($manifest_path | path exists) {
# Cache corrupted, remove it
log-warn $"Cache corrupted for ($extension_name):($cache_entry.version), removing"
remove-from-cache $extension_type $extension_name $cache_entry.version
return {found: false}
}
# Return cache entry with OCI metadata
{
found: true
path: $cache_entry.path
version: $cache_entry.version
metadata: $cache_entry.metadata
oci_manifest: (open $manifest_path | from json)
}
}
# Save Gitea artifact to cache
export def save-gitea-to-cache [
extension_type: string
extension_name: string
version: string
artifact_path: string
gitea_metadata: record
]: nothing -> bool {
try {
let cache_path = (get-cache-path $extension_type $extension_name $version)
log-debug $"Saving Gitea artifact to cache: ($cache_path)"
# Create cache directory
mkdir $cache_path
# Copy extracted artifact
let artifact_contents = (ls $artifact_path | get name)
for file in $artifact_contents {
cp -r $file $cache_path
}
# Save Gitea metadata
$gitea_metadata | to json | save $"($cache_path)/gitea-metadata.json"
# Update cache index
update-cache-index $extension_type $extension_name $version {
source_type: "gitea"
cached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
gitea_url: ($gitea_metadata.url? | default "")
gitea_ref: ($gitea_metadata.ref? | default "")
}
log-info $"Cached ($extension_name):($version) from Gitea"
true
} catch { |err|
log-error $"Failed to save Gitea artifact to cache: ($err.msg)"
false
}
}
# Remove extension from cache
export def remove-from-cache [
extension_type: string
extension_name: string
version: string
]: nothing -> bool {
try {
let cache_path = (get-cache-path $extension_type $extension_name $version)
if ($cache_path | path exists) {
rm -rf $cache_path
log-debug $"Removed ($extension_name):($version) from cache"
}
# Update index
let index = (load-cache-index)
let key = $"($extension_type)/($extension_name)/($version)"
let updated_index = ($index | update extensions {
$in | reject $key
})
save-cache-index $updated_index
true
} catch { |err|
log-error $"Failed to remove from cache: ($err.msg)"
false
}
}
# Clear entire cache
export def clear-cache [
--extension-type: string = ""
--extension-name: string = ""
]: nothing -> nothing {
let cache_dir = (get-cache-dir)
if ($extension_type | is-not-empty) and ($extension_name | is-not-empty) {
# Clear specific extension
let ext_dir = ($cache_dir | path join $extension_type $extension_name)
if ($ext_dir | path exists) {
rm -rf $ext_dir
log-info $"Cleared cache for ($extension_name)"
}
} else if ($extension_type | is-not-empty) {
# Clear all extensions of type
let type_dir = ($cache_dir | path join $extension_type)
if ($type_dir | path exists) {
rm -rf $type_dir
log-info $"Cleared cache for all ($extension_type)"
}
} else {
# Clear all cache
if ($cache_dir | path exists) {
rm -rf $cache_dir
mkdir $cache_dir
log-info "Cleared entire extension cache"
}
}
# Rebuild index
save-cache-index {
extensions: {}
metadata: {
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
last_updated: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
}
}
}
# List cached extensions
export def list-cached [
--extension-type: string = ""
]: nothing -> table {
let index = (load-cache-index)
$index.extensions
| items {|key, value| $value}
| if ($extension_type | is-not-empty) {
where type == $extension_type
} else {
$in
}
| select type name version source_type cached_at
| sort-by type name version
}
# Get cache statistics
export def get-cache-stats []: nothing -> record {
let index = (load-cache-index)
let cache_dir = (get-cache-dir)
let extensions = ($index.extensions | items {|key, value| $value})
let total_size = if ($cache_dir | path exists) {
du -s $cache_dir | get 0.physical?
} else {
0
}
{
total_extensions: ($extensions | length)
by_type: ($extensions | group-by type | items {|k, v| {type: $k, count: ($v | length)}} | flatten)
by_source: ($extensions | group-by source_type | items {|k, v| {source: $k, count: ($v | length)}} | flatten)
total_size_bytes: $total_size
cache_dir: $cache_dir
last_updated: ($index.metadata.last_updated? | default "")
}
}
# Prune old cache entries (older than days)
export def prune-cache [
days: int = 30
]: nothing -> record {
let index = (load-cache-index)
let cutoff = (date now | date format "%Y-%m-%dT%H:%M:%SZ" | into datetime | $in - ($days * 86400sec))
let to_remove = ($index.extensions
| items {|key, value|
let cached_at = ($value.cached_at | into datetime)
if $cached_at < $cutoff {
{key: $key, value: $value}
} else {
null
}
}
| compact
)
let removed = ($to_remove | each {|entry|
remove-from-cache $entry.value.type $entry.value.name $entry.value.version
$entry.value
})
{
removed_count: ($removed | length)
removed_extensions: $removed
freed_space: "unknown"
}
}
# Helper: Sort versions by semver
def sort-by-semver [] {
$in | sort-by --custom {|a, b|
compare-semver-versions $a $b
}
}
# Helper: Compare semver versions
def compare-semver-versions [a: string, b: string]: nothing -> int {
# Simple semver comparison (can be enhanced)
let a_parts = ($a | str replace 'v' '' | split row '.')
let b_parts = ($b | str replace 'v' '' | split row '.')
for i in 0..2 {
let a_num = ($a_parts | get -o $i | default "0" | into int)
let b_num = ($b_parts | get -o $i | default "0" | into int)
if $a_num < $b_num {
return -1
} else if $a_num > $b_num {
return 1
}
}
0
}
# Get temp extraction path for downloads
export def get-temp-extraction-path [
extension_type: string
extension_name: string
version: string
]: nothing -> string {
let temp_base = (mktemp -d)
$temp_base | path join $extension_type $extension_name $version
}