2024-08-31 01:03:37 +08:00

572 lines
17 KiB
Bash
Executable File

#! /bin/bash
TYPE=$1
DEFAULT_GH_USER=linuxserver
DEFAULT_GH_REPO=proot-apps
if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then
ROOT_DIR=$PA_REPO_FOLDER
else
ROOT_DIR=$HOME/proot-apps
fi
# Check for deps
if [[ ! -f /usr/bin/curl ]]; then
echo "Curl was not found on this system please install them to continue"
exit 1
fi
#### Functions ####
# Get sha for image to download
get_blob_sha() {
MULTIDIGEST=$(curl -s -f --retry 3 --retry-max-time 20 --retry-connrefused \
--location \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
--header "Accept: application/vnd.oci.image.index.v1+json" \
--header "Authorization: Bearer ${1}" \
--user-agent "${UA}" \
"${2}/${3}")
if $HOME/.local/bin/jq -e '.layers // empty' <<< "${MULTIDIGEST}" >/dev/null 2>&1; then
# If there's a layer element it's a single-arch manifest so just get that digest
$HOME/.local/bin/jq -r '.layers[0].digest' <<< "${MULTIDIGEST}";
else
# Otherwise it's multi-arch or has manifest annotations
if $HOME/.local/bin/jq -e '.manifests[]?.annotations // empty' <<< "${MULTIDIGEST}" >/dev/null 2>&1; then
# Check for manifest annotations and delete if found
MULTIDIGEST=$($HOME/.local/bin/jq 'del(.manifests[] | select(.annotations))' <<< "${MULTIDIGEST}")
fi
if [[ $($HOME/.local/bin/jq '.manifests | length' <<< "${MULTIDIGEST}") -gt 1 ]]; then
# If there's still more than one digest, it's multi-arch
MULTIDIGEST=$($HOME/.local/bin/jq -r ".manifests[] | select(.platform.architecture == \"${4}\").digest?" <<< "${MULTIDIGEST}")
if [[ -z "${MULTIDIGEST}" ]]; then
cleanup
fi
else
# Otherwise it's single arch
MULTIDIGEST=$($HOME/.local/bin/jq -r ".manifests[].digest?" <<< "${MULTIDIGEST}")
fi
if DIGEST=$(curl -s -f --retry 3 --retry-max-time 20 --retry-connrefused \
--location \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
--header "Accept: application/vnd.oci.image.manifest.v1+json" \
--header "Authorization: Bearer ${1}" \
--user-agent "${UA}" \
"${2}/${MULTIDIGEST}"); then
$HOME/.local/bin/jq -r '.layers[0].digest' <<< "${DIGEST}";
fi
fi
}
# Get registry endpoint and auth
registry_setup() {
IMAGE=$1
# Determine endpoints and auth
case "${IMAGE}" in
ghcr.io/* )
GHIMAGE=$(echo ${IMAGE} | sed 's|ghcr.io/||g')
ENDPOINT="${GHIMAGE%%:*}"
USERNAME="${GHIMAGE%%/*}"
TAG="${GHIMAGE#*:}"
REGISTRY="ghcr.io"
AUTH_URL="https://ghcr.io/token?scope=repository%3A${ENDPOINT}%3Apull"
;;
* )
ENDPOINT="${IMAGE%%:*}"
USERNAME="${IMAGE%%/*}"
TAG="${IMAGE#*:}"
REGISTRY="registry-1.docker.io"
AUTH_URL="https://auth.docker.io/token?service=registry.docker.io&scope=repository:${ENDPOINT}:pull"
;;
esac
MANIFEST_URL="https://${REGISTRY}/v2/${ENDPOINT}/manifests"
BLOB_URL="https://${REGISTRY}/v2/${ENDPOINT}/blobs/"
TOKEN="$(
curl -s -f --retry 3 --retry-max-time 20 --retry-connrefused \
"${AUTH_URL}" |
$HOME/.local/bin/jq -r '.token'
)"
}
# Download layer
function dl_layer() {
mkdir -p $ROOT_DIR
# CLI vars
IMAGE=$1
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
ARCH=$(uname -m| sed 's/x86_64/amd64/g'| sed 's/aarch64/arm64/')
UA="Mozilla/5.0 (Linux $(uname -m)) kasmweb.com"
# Destination directory
mkdir -p "${ROOT_DIR}/${IMAGE_FOLDER}"
touch "${ROOT_DIR}/${IMAGE_FOLDER}/DOWNLOADING"
## Functions ##
# Cleanup and exit 1 if something went wrong
cleanup() {
rm -Rf "${ROOT_DIR}/${IMAGE_FOLDER}"
exit 1
}
if [[ -z ${SHALAYER+x} ]]; then
if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ -z ${LOCALREPO+x} ]]; then
SHALAYER=$(cat $PA_REPO_FOLDER/${IMAGE_FOLDER}/SHALAYER)
else
registry_setup ${IMAGE}
SHALAYER=$(get_blob_sha "${TOKEN}" "${MANIFEST_URL}" "${TAG}" "${ARCH}")
fi
fi
if [[ $? -eq 1 ]]; then
echo "No manifest available for ${IMAGE}, cannot fetch"
cleanup
elif [[ -z "${SHALAYER}" ]]; then
echo "${IMAGE} digest could not be fetched from ${REGISTRY}"
cleanup
fi
# Download layer
if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ -z ${LOCALREPO+x} ]]; then
echo "Extracting from local repo"
tar -xf \
$PA_REPO_FOLDER/${IMAGE_FOLDER}/app.tar.gz -C \
"${ROOT_DIR}/${IMAGE_FOLDER}/"
else
if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then
curl -f --retry 3 --retry-max-time 20 \
-o ${ROOT_DIR}/${IMAGE_FOLDER}/app.tar.gz \
--location \
--header "Authorization: Bearer ${TOKEN}" \
--user-agent "${UA}" \
"${BLOB_URL}${SHALAYER}"
else
curl -f --retry 3 --retry-max-time 20 \
--location \
--header "Authorization: Bearer ${TOKEN}" \
--user-agent "${UA}" \
"${BLOB_URL}${SHALAYER}" \
| tar -xzf - -C "${ROOT_DIR}/${IMAGE_FOLDER}/"
fi
if [[ $? -ne 0 ]]; then
echo "Error downloading ${IMAGE}"
cleanup
fi
fi
# Tag image
echo "${SHALAYER}" > "${ROOT_DIR}/${IMAGE_FOLDER}/SHALAYER"
# Cleanup
rm -f "${ROOT_DIR}/${IMAGE_FOLDER}/DOWNLOADING"
}
# Update Icon cache
function update_icon_cache() {
if [ ! -f "$HOME/.local/share/icons/hicolor/index.theme" ]; then
mkdir -p $HOME/.local/share/icons/hicolor
cp \
/usr/share/icons/hicolor/index.theme \
$HOME/.local/share/icons/hicolor/
fi
if which gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache $HOME/.local/share/icons/hicolor >/dev/null 2>&1
elif which update-icon-caches >/dev/null 2>&1; then
update-icon-caches $HOME/.local/share/icons/hicolor >/dev/null 2>&1
fi
}
# Run a unix socket to relay commands to the host
start_system_socket() {
if ! pgrep -f ${1}system.socket; then
rm -f ${1}system.socket
$HOME/.local/bin/ncat -k -U -l ${1}system.socket | bash &
BG_PIDS=$(jobs -p)
chmod 600 ${1}system.socket || :
tail --pid=$2 -f /dev/null
kill -9 $BG_PIDS || :
fi
}
# Run update
function update() {
IMAGE_FOLDER=$1
IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g')
if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then
echo "${IMAGE} not present on system run install or get first"
exit 1
fi
LOCAL_SHA=$(cat "$ROOT_DIR/${IMAGE_FOLDER}/SHALAYER")
if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ -z ${LOCALREPO+x} ]]; then
if [ ! -f $PA_REPO_FOLDER/${IMAGE_FOLDER}/SHALAYER ]; then
echo "${IMAGE} is not available in local repo"
exit 1
fi
# Use local SHA
SHALAYER=$(cat $PA_REPO_FOLDER/${IMAGE_FOLDER}/SHALAYER)
else
# Check remote SHA
ARCH=$(uname -m| sed 's/x86_64/amd64/g'| sed 's/aarch64/arm64/')
UA="Mozilla/5.0 (Linux $(uname -m)) kasmweb.com"
registry_setup ${IMAGE}
SHALAYER=$(get_blob_sha "${TOKEN}" "${MANIFEST_URL}" "${TAG}" "${ARCH}")
fi
if [[ "${SHALAYER}" == "${LOCAL_SHA}" ]]; then
echo "${IMAGE} is up to date: ${LOCAL_SHA}"
else
echo "Updating ${IMAGE_FOLDER}"
# Run remove logic
if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then
echo "Removing app root local"
rm -Rf $ROOT_DIR/${IMAGE_FOLDER}/
dl_layer ${IMAGE}
else
if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/remove" ]; then
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/remove
fi
# Remove app layer
echo "Removing app root"
rm -Rf $ROOT_DIR/${IMAGE_FOLDER}/
dl_layer ${IMAGE}
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/install >/dev/null 2>&1
# Refresh icon cache
update_icon_cache
# Install
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/install
fi
fi
}
# Run remove
function remove() {
IMAGE_FOLDER=$1
IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g')
echo "Removing ${IMAGE}"
if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then
echo "${IMAGE} not present on system run install or get first"
exit 1
fi
if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then
echo "localremove ${IMAGE}"
else
# Run remove logic
if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/remove" ]; then
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/remove
fi
# Remove bin wrapper if defined
if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/bin-name" ]; then
rm -f $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name)
fi
fi
# Remove app layer
echo "Removing app root"
rm -Rf $ROOT_DIR/${IMAGE_FOLDER}/
}
# Run uninstall
function uninstall() {
IMAGE_FOLDER=$1
IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g')
echo "Uninstalling ${IMAGE}"
if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then
echo "${IMAGE} not present on system run install or get first"
exit 1
fi
# Run remove logic
if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/remove" ]; then
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/remove
fi
# Remove bin wrapper if defined
if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/bin-name" ]; then
rm -f $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name)
fi
}
# Install
function install_app() {
IMAGE=$1
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
echo "Installing ${IMAGE}"
# Download if not present
if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}" ]; then
dl_layer ${IMAGE}
fi
# Add bin wrapper if defined
if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/bin-name" ]; then
echo "\$HOME/.local/bin/proot-apps run ${IMAGE} \"\$@\"" > $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name)
chmod +x $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name)
echo "$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name) is now available from the command line"
fi
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/install >/dev/null 2>&1
# Refresh icon cache
update_icon_cache
# Install
$HOME/.local/bin/proot \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/install
}
# Get
function get_app() {
IMAGE=$1
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
echo "Getting ${IMAGE}"
# Download if not present
if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}" ]; then
dl_layer ${IMAGE}
else
echo "${IMAGE} present on this system use update to update"
fi
}
#### Runtime ####
# If a non docker endpoint is passed assume the default repo
if [[ ! -z ${2+x} ]]; then
if ([[ "${2}" != *"/"* ]] || [[ "${2}" != *":"* ]]) && [[ "${2}" != "all" ]]; then
IMAGE=ghcr.io/${DEFAULT_GH_USER}/${DEFAULT_GH_REPO}:${2}
else
IMAGE=${2}
fi
fi
# Run the application
if [ "${TYPE}" == "run" ]; then
if [[ -z ${2+x} ]]; then
echo "The image is required"
echo "Usage: proot-apps run myorg/myimage:tag"
exit 1
fi
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then
echo "${IMAGE} not present on system run install or get first"
exit 1
fi
# Docker shims for known desktop containers
if [ -d "/defaults" ]; then
PULSE_BIND="-b /defaults:/defaults"
elif [ -d "/var/run/pulse" ]; then
PULSE_BIND="-b /var/run/pulse:/var/run/pulse"
fi
# Mount repo location for the gui app
if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ "${IMAGE_FOLDER}" == *gui ]]; then
REPO_BIND="-b ${PA_REPO_FOLDER}:${PA_REPO_FOLDER}"
mkdir -p "$ROOT_DIR/${IMAGE_FOLDER}${PA_REPO_FOLDER}"
fi
# Fonts
if [ -d "/usr/share/fonts" ] && [ -d "/usr/share/fontconfig" ] && [ -d "/etc/fonts" ]; then
FONTS_BIND="-b /usr/share/fonts:/usr/share/fonts -b /usr/share/fontconfig:/usr/share/fontconfig -b /etc/fonts:/etc/fonts"
fi
$HOME/.local/bin/proot \
${PULSE_BIND} ${FONTS_BIND} ${REPO_BIND} -n \
-R $ROOT_DIR/${IMAGE_FOLDER}/ \
/entrypoint "${@:3}" &
start_system_socket "$ROOT_DIR/${IMAGE_FOLDER}/" $!
fi
# Run installation
if [ "${TYPE}" == "install" ]; then
if [[ -z ${2+x} ]]; then
echo "The image is required"
echo "Usage: proot-apps install myorg/myimage:tag"
exit 1
fi
# Install multiple
if [[ ! -z ${3+x} ]]; then
for APP in "${@:2}"; do
if [[ "${APP}" != *"/"* ]] || [[ "${APP}" != *":"* ]]; then
install_app ghcr.io/${DEFAULT_GH_USER}/${DEFAULT_GH_REPO}:${APP}
else
install_app "${APP}"
fi
unset SHALAYER
done
# Install single or all
else
if [[ "${IMAGE}" != "all" ]]; then
install_app "${IMAGE}"
else
if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then
for IMAGE_FOLDER in $(ls $ROOT_DIR); do
IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g')
install_app "${IMAGE}"
done
else
echo "no apps to install"
fi
fi
fi
fi
# Run remove
if [ "${TYPE}" == "remove" ]; then
if [[ -z ${2+x} ]]; then
echo "The image is required"
echo "Usage: proot-apps remove myorg/myimage:tag"
exit 1
fi
# Remove multiple
if [[ ! -z ${3+x} ]]; then
for APP in "${@:2}"; do
if [[ "${APP}" != *"/"* ]] || [[ "${APP}" != *":"* ]]; then
IMAGE=ghcr.io/${DEFAULT_GH_USER}/${DEFAULT_GH_REPO}:${APP}
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
remove "${IMAGE_FOLDER}"
else
IMAGE_FOLDER=$(echo "${APP}"| sed 's|/|_|g'| sed 's|:|_|g')
remove "${IMAGE_FOLDER}"
fi
done
# Uninstall single or all
else
if [[ "${IMAGE}" != "all" ]]; then
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
remove "${IMAGE_FOLDER}"
else
if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then
for IMAGE_FOLDER in $(ls $ROOT_DIR); do
remove "${IMAGE_FOLDER}"
done
else
echo "no apps to remove"
fi
fi
fi
fi
# Run uninstall
if [ "${TYPE}" == "uninstall" ]; then
if [[ -z ${2+x} ]]; then
echo "The image is required"
echo "Usage: proot-apps uninstall myorg/myimage:tag"
exit 1
fi
# Uninstall multiple
if [[ ! -z ${3+x} ]]; then
for APP in "${@:2}"; do
if [[ "${APP}" != *"/"* ]] || [[ "${APP}" != *":"* ]]; then
IMAGE=ghcr.io/${DEFAULT_GH_USER}/${DEFAULT_GH_REPO}:${APP}
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
uninstall "${IMAGE_FOLDER}"
else
IMAGE_FOLDER=$(echo "${APP}"| sed 's|/|_|g'| sed 's|:|_|g')
uninstall "${IMAGE_FOLDER}"
fi
done
# Uninstall single or all
else
if [[ "${IMAGE}" != "all" ]]; then
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
uninstall "${IMAGE_FOLDER}"
else
if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then
for IMAGE_FOLDER in $(ls $ROOT_DIR); do
uninstall "${IMAGE_FOLDER}"
done
else
echo "no apps to uninstall"
fi
fi
fi
fi
# Run update
if [ "${TYPE}" == "update" ]; then
if [[ -z ${2+x} ]]; then
echo "The image is required"
echo "Usage: proot-apps update myorg/myimage:tag"
exit 1
fi
# Update multiple
if [[ ! -z ${3+x} ]]; then
for APP in "${@:2}"; do
if [[ "${APP}" != *"/"* ]] || [[ "${APP}" != *":"* ]]; then
IMAGE=ghcr.io/${DEFAULT_GH_USER}/${DEFAULT_GH_REPO}:${APP}
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
update "${IMAGE_FOLDER}"
else
IMAGE_FOLDER=$(echo "${APP}"| sed 's|/|_|g'| sed 's|:|_|g')
update "${IMAGE_FOLDER}"
fi
done
# Update single or all
else
if [[ "${IMAGE}" != "all" ]]; then
IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g')
update "${IMAGE_FOLDER}"
else
if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then
for IMAGE_FOLDER in $(ls $ROOT_DIR); do
update "${IMAGE_FOLDER}"
done
else
echo "no apps to update"
fi
fi
fi
fi
# Run get
if [ "${TYPE}" == "get" ]; then
if [[ -z ${2+x} ]]; then
echo "The image is required"
echo "Usage: proot-apps get myorg/myimage:tag"
exit 1
fi
# Get multiple
if [[ ! -z ${3+x} ]]; then
for APP in "${@:2}"; do
if [[ "${APP}" != *"/"* ]] || [[ "${APP}" != *":"* ]]; then
IMAGE=ghcr.io/${DEFAULT_GH_USER}/${DEFAULT_GH_REPO}:${APP}
get_app "${IMAGE}"
else
get_app "${IMAGE}"
fi
unset SHALAYER
done
# Get single
else
get_app "${IMAGE}"
fi
fi
# Run localrepo actions
if [ "${TYPE}" == "localrepo" ]; then
if [ -z ${PA_REPO_FOLDER+x} ]; then
echo "error: PA_REPO_FOLDER is not set in env"
exit 1
fi
if [[ -z ${2+x} ]] || [[ -z ${3+x} ]]; then
echo "Usage: proot-apps localrepo [get,remove,update] myorg/myimage:tag"
exit 1
fi
export LOCALREPO=true
if [[ "${2}" =~ ^(get|remove|update)$ ]]; then
"$(realpath "$0")" "${@:2}"
fi
fi
# Usage
if [[ -z ${1+x} ]]; then
echo "+-----------------------------------------------------------+"
echo "| Usage: | proot-apps [type] [image_name] |"
echo "+-----------------------------------------------------------+"
echo "| Run | proot-apps run myorg/myimage:tag |"
echo "| Install | proot-apps install [myorg/myimage:tag|all] |"
echo "| Uninstall | proot-apps uninstall [myorg/myimage:tag|all] |"
echo "| Remove | proot-apps remove [myorg/myimage:tag|all] |"
echo "| Get | proot-apps get myorg/myimage:tag |"
echo "| Update | proot-apps update [myorg/myimage:tag|all] |"
echo "+-----------------------------------------------------------+"
fi