|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +# File fetch with verification and cache support |
| 6 | +# Usage: |
| 7 | +# [IGconf_sys_cachedir=/path/to/cache] <script> <metadata.file> </path/to/output.file> |
| 8 | +# |
| 9 | +# Metadata - mirrors supported, use consistent name: |
| 10 | +# <name> <algo:hex> <url or local path> |
| 11 | +# algo: sha256|sha256sum|sha512|sha512sum |
| 12 | + |
| 13 | + |
| 14 | +msg() { |
| 15 | + echo -e "$*" |
| 16 | +} |
| 17 | + |
| 18 | +err (){ |
| 19 | + >&2 msg "Error: $*" |
| 20 | +} |
| 21 | + |
| 22 | +die (){ |
| 23 | + [[ -n "$*" ]] && err "$*" |
| 24 | + exit 1 |
| 25 | +} |
| 26 | + |
| 27 | + |
| 28 | +meta="${1:-}"; dest="${2:-}" |
| 29 | +[[ -n "$meta" && -n "$dest" ]] || die "Expected args: <metadata.file> </path/to/output.file>" |
| 30 | + |
| 31 | + |
| 32 | +# Enforce that dest is a file path, not a directory |
| 33 | +if [[ -d "$dest" || "$dest" == */ ]]; then |
| 34 | + die "Output path must be a file, not a directory" |
| 35 | +fi |
| 36 | + |
| 37 | + |
| 38 | +# Default cache location |
| 39 | +cache_dir="${IGconf_sys_cachedir:-/tmp}" |
| 40 | +mkdir -p -- "$cache_dir" |
| 41 | + |
| 42 | + |
| 43 | +# Output path is always an explicit file path |
| 44 | +out_path="$dest" |
| 45 | +out_dir="$(dirname -- "$out_path")" |
| 46 | +mkdir -p -- "$out_dir" |
| 47 | + |
| 48 | + |
| 49 | +# digest <algo> <file> -> hex |
| 50 | +digest() |
| 51 | +{ |
| 52 | + case "$1" in |
| 53 | + sha256|sha256sum) sha256sum "$2" | sed 's/[[:space:]].*$//' ;; |
| 54 | + sha512|sha512sum) sha512sum "$2" | sed 's/[[:space:]].*$//' ;; |
| 55 | + *) die "Unsupported algo: $1" ;; |
| 56 | + esac |
| 57 | +} |
| 58 | + |
| 59 | + |
| 60 | +# atomic file copy <src> <dst> |
| 61 | +atomic_install_copy() |
| 62 | +{ |
| 63 | + local src="$1" dst="$2" dstdir tmp |
| 64 | + dstdir="$(dirname -- "$dst")" |
| 65 | + tmp="$(mktemp -p "$dstdir" ".fetch.install.XXXXXX")" |
| 66 | + cp -f -- "$src" "$tmp" |
| 67 | + mv -f -- "$tmp" "$dst" |
| 68 | +} |
| 69 | + |
| 70 | + |
| 71 | +# Two-pass approach: |
| 72 | +# pass 1 tries cache against any candidate digest |
| 73 | +# pass 2 downloads per candidate |
| 74 | +target_name="" |
| 75 | + |
| 76 | + |
| 77 | +# Pass 1: determine target, verify cache against all candidate digests |
| 78 | +cache_path="" |
| 79 | + |
| 80 | +while IFS= read -r line || [[ -n "$line" ]]; do |
| 81 | + [[ "$line" =~ ^[[:space:]]*$ ]] && continue |
| 82 | + [[ "$line" =~ ^[[:space:]]*# ]] && continue |
| 83 | + |
| 84 | + set -- $line |
| 85 | + name="${1:-}"; algo_hex="${2:-}"; src="${3:-}" |
| 86 | + [[ -n "$name" && -n "$algo_hex" && -n "$src" ]] || continue |
| 87 | + |
| 88 | + if [[ -z "$target_name" ]]; then |
| 89 | + target_name="$name" |
| 90 | + cache_path="${cache_dir}/${target_name}" |
| 91 | + fi |
| 92 | + [[ "$name" == "$target_name" ]] || continue |
| 93 | + |
| 94 | + algo="${algo_hex%%:*}" |
| 95 | + expected="${algo_hex#*:}" |
| 96 | + expected="${expected,,}" |
| 97 | + if [[ "$algo" == "$expected" || -z "$expected" ]]; then |
| 98 | + die "Malformed metadata for $target_name: expected '<algo>:<hex>'" |
| 99 | + fi |
| 100 | + |
| 101 | + if [[ -f "$cache_path" ]]; then |
| 102 | + got="$(digest "$algo" "$cache_path")" |
| 103 | + if [[ "${got,,}" == "$expected" ]]; then |
| 104 | + atomic_install_copy "$cache_path" "$out_path" |
| 105 | + msg "Using verified cache: $cache_path -> $out_path" |
| 106 | + exit 0 |
| 107 | + fi |
| 108 | + fi |
| 109 | +done < "${meta}" |
| 110 | + |
| 111 | +[[ -n "${target_name}" ]] || die "No metadata entries in $meta" |
| 112 | + |
| 113 | + |
| 114 | +# Pass 2: download each candidate and verify with its own expected digest |
| 115 | +while IFS= read -r line || [[ -n "${line}" ]]; do |
| 116 | + [[ "${line}" =~ ^[[:space:]]*$ ]] && continue |
| 117 | + [[ "${line}" =~ ^[[:space:]]*# ]] && continue |
| 118 | + |
| 119 | + set -- $line |
| 120 | + name="${1:-}"; algo_hex="${2:-}"; src="${3:-}" |
| 121 | + [[ -n "$name" && -n "$algo_hex" && -n "$src" ]] || continue |
| 122 | + [[ "$name" == "$target_name" ]] || continue |
| 123 | + |
| 124 | + algo="${algo_hex%%:*}" |
| 125 | + expected="${algo_hex#*:}" |
| 126 | + expected="${expected,,}" |
| 127 | + if [[ "$algo" == "$expected" || -z "$expected" ]]; then |
| 128 | + die "Malformed metadata for $target_name: expected '<algo>:<hex>'" |
| 129 | + fi |
| 130 | + |
| 131 | + tmp_cache="$(mktemp -p "$cache_dir" ".fetch.${target_name}.XXXXXX")" |
| 132 | + if [[ "$src" == http://* || "$src" == https://* ]]; then |
| 133 | + if ! curl --fail --location --silent --show-error --retry 3 --retry-delay 2 \ |
| 134 | + --connect-timeout 30 --output "$tmp_cache" "$src"; then |
| 135 | + rm -f -- "$tmp_cache" |
| 136 | + continue |
| 137 | + fi |
| 138 | + else |
| 139 | + if ! cp -f -- "$src" "$tmp_cache"; then |
| 140 | + rm -f -- "$tmp_cache" |
| 141 | + continue |
| 142 | + fi |
| 143 | + fi |
| 144 | + |
| 145 | + got="$(digest "$algo" "$tmp_cache")" |
| 146 | + if [[ "${got,,}" != "$expected" ]]; then |
| 147 | + err "Checksum mismatch for ${target_name} from ${src}" |
| 148 | + rm -f -- "${tmp_cache}" |
| 149 | + continue |
| 150 | + fi |
| 151 | + |
| 152 | + atomic_install_copy "$tmp_cache" "$out_path" |
| 153 | + mv -f -- "$tmp_cache" "$cache_path" |
| 154 | + msg "Fetched: $src -> $out_path cached at $cache_path" |
| 155 | + exit 0 |
| 156 | +done < "${meta}" |
| 157 | + |
| 158 | + |
| 159 | +die "Failed: no verified source succeeded for ${target_name}" |
0 commit comments