Skip to content

Instantly share code, notes, and snippets.

@akrez
Last active November 30, 2025 21:30
Show Gist options
  • Select an option

  • Save akrez/01b38fc760e6269a023058accaa9d96b to your computer and use it in GitHub Desktop.

Select an option

Save akrez/01b38fc760e6269a023058accaa9d96b to your computer and use it in GitHub Desktop.
#!/bin/bash
function error() {
echo -e "$1" 1>&2
exit 1
}
function change_owner() {
chown -R _apt:root "$1" || error "FAILED TO CHANGE OWNER : $1"
}
function make_directory() {
mkdir -p -m755 "$1" || error "FAILED TO MAKE DIRECTORY : $1"
}
function help() {
error "Usage:\n\tdownload <package1> <package2> ...\n\tinstall <package1> <package2> ..."
}
if [ $# -gt 0 ]; then
COMMAND="$1"
shift
else
help
fi
if [[ $EUID -ne 0 ]]; then
error "RUN AS ROOT"
fi
OS_RELEASE=$(source /etc/os-release && echo $ID-$VERSION_ID)
ABSOLUTE_BASE_PATH=$(realpath $(dirname ${BASH_SOURCE[0]}))/$OS_RELEASE
make_directory "$ABSOLUTE_BASE_PATH"
change_owner "$ABSOLUTE_BASE_PATH"
cd "$ABSOLUTE_BASE_PATH"
if [[ "$COMMAND" == "download" ]]; then
for PACKAGE in "$@"; do
if ! apt-cache show "$PACKAGE" >/dev/null 2>&1; then
error "πŸ”΄ [$PACKAGE] APT-CACHE NOT FOUND PACKAGE"
fi
done
for PACKAGE in "$@"; do
mapfile -t ALL_PACKAGE_AND_DEPENDS < <(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances --no-pre-depends -o APT::Architectures="amd64" "$PACKAGE" | grep -E "^\w" | sort -u)
mapfile -t PACKAGE_AND_DEPENDS < <(apt-cache policy "${ALL_PACKAGE_AND_DEPENDS[@]}" | awk '/^[a-zA-Z]/ {pkg=$1} /\*\*\*/ {sub(":", "", pkg); print pkg}')
mapfile -t LINES < <(apt-get install -o Dir::Cache::archives=/dev/null --print-uris --reinstall "${PACKAGE_AND_DEPENDS[@]}" 2>&1 | grep "'http")
FILENAMES=()
for LINE in "${LINES[@]}"; do
URI=$(echo "$LINE" | sed "s/^'\([^']*\)'.*/\1/")
DECODED_FILENAME=$(echo "$LINE" | sed "s/.*'[^']*' \([^ ]*\.deb\) .*/\1/")
EXPECTED_SIZE=$(echo "$LINE" | cut -d' ' -f3)
FILEPATH="$ABSOLUTE_BASE_PATH/$DECODED_FILENAME"
if [[ -f "$FILEPATH" && "$EXPECTED_SIZE" == $(stat -c%s "$FILEPATH" 2>/dev/null || wc -c < "$FILEPATH") ]]; then
echo "⬇️ [$PACKAGE] DOWNLOADED $DECODED_FILENAME"
else
echo "⬇️ [$PACKAGE] DOWNLOADING $DECODED_FILENAME"
wget -O "$FILEPATH" "$URI"
fi
if [[ ! -f "$FILEPATH" ]]; then
error "πŸ”΄ [$PACKAGE] ($DECODED_FILENAME) IS NOT EXIST: $URI"
elif [[ "$EXPECTED_SIZE" != $(stat -c%s "$FILEPATH" 2>/dev/null || wc -c < "$FILEPATH") ]]; then
error "πŸ”΄ [$PACKAGE] ($DECODED_FILENAME) FILE SIZE MISMATCH: $URI"
else
FILENAMES+=("$DECODED_FILENAME")
fi
done
MANIFESTPATH="$ABSOLUTE_BASE_PATH/${PACKAGE}.txt"
echo "πŸ“„ [$PACKAGE] WRITE MANIFEST $MANIFESTPATH"
printf '%s\n' "${FILENAMES[@]}" > "$MANIFESTPATH"
echo
done
change_owner "$ABSOLUTE_BASE_PATH"
elif [[ "$COMMAND" == "install" ]]; then
for PACKAGE in "$@"; do
LISTFILE="$ABSOLUTE_BASE_PATH/${PACKAGE}.txt"
if [[ ! -f "$LISTFILE" ]]; then
error "πŸ”΄ [$PACKAGE] NOT FOUND"
fi
DEB_FILES=()
while IFS= read -r LINE; do
[[ -z "$LINE" ]] && continue
FILEPATH="$ABSOLUTE_BASE_PATH/$LINE"
if [[ ! -f "$FILEPATH" ]]; then
error "πŸ”΄ [$PACKAGE] MISSING DEB FILE ($LINE)"
fi
DEB_FILES+=("$FILEPATH")
done < "$LISTFILE"
if [[ ${#DEB_FILES[@]} -eq 0 ]]; then
error "πŸ”΄ [$PACKAGE] NO PACKAGES TO INSTALL"
fi
dpkg -i "${DEB_FILES[@]}" || error "πŸ”΄ [$PACKAGE] FAILED TO INSTALL" -o APT::Architectures="amd64"
done
elif [[ "$COMMAND" == "cleanup" ]]; then
declare -A VALID_DEBS
for MANIFEST_FILE in "$ABSOLUTE_BASE_PATH"/*.txt; do
while IFS= read -r DEB_FILE; do
[[ -z "$DEB_FILE" ]] && continue
VALID_DEBS["$DEB_FILE"]=1
done < "$MANIFEST_FILE"
done
for DEB_FILE in "$ABSOLUTE_BASE_PATH"/*.deb; do
[[ ! -f "$DEB_FILE" ]] && continue
FILENAME=$(basename "$DEB_FILE")
if [[ -z "${VALID_DEBS[$FILENAME]}" ]]; then
echo "πŸ—‘οΈ REMOVE ($FILENAME)"
rm -f "$DEB_FILE"
fi
done
else
help
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment