# 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 }