443 lines
12 KiB
Plaintext
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
|
|
}
|