Automate update checks for externally downloaded software (in .tar.gz or .AppImage)

Although i always prefer software from the repos, after some time for different reasons i still ended up having some samples of other types of programs/sources - aur, flatpak, appimage, some archive with executable file (closed source). AUR and Flatpaks have an update mechanism. AppImages - in theory it is possible, but it is not obligatory and dependent from the developer, so a lot of them do not. And the proprietary software distributed as an archive with a directory and executables inside usually does not have an update mechanism at all. Some very popular software comes originally in such way, like Viber, Teamviewer, Anydesk. Yes, some of them have AUR versions, but some do not. And it is a matter of choice, for example if you are on stable and does not need AUR for anything else, it can come as a problem.

So for my own needs i wrote a script, that compares the size of a list of programs in a folder with their download sources sizes and notifies if they do not match. It is still very alpha, and that check is not the most reliable art of comparison (imagine lack of internet connection for a second, a comparison of something with nothing will also differ, ok i thought of that but there are probably other similar cases i did not thought of). So i did not go beyond notification function for such external apps (it could in theory be used to replace the apps).
There are also many limitations in the current version: it covers only some of the most popular cases - direct url without version number in it, and github appimages and zips. And everything should be in one directory, and the zipped versions should be kept which is a waste of space, but named uniquely, etc.

Still i commented the thing heavily and decided to share, might be useful for someone.

Below is a screenshot - a preview of it running, how my directory structure looks, and a sample desktop file for a launcher button

And the source:

script source
#!/bin/bash
# Creator: Todor Uzunov a.k.a. Teo
# License: GNU - free like free speech and free beer for everybody!
# ver. 04.05.2024
# Changelog: first public release

# Welcome! Please read the helpful comments and adjust the variables to your needs, 
# when you are ready set the variable below to 0.

first_run=1

if [[ $first_run == 1 ]] ; then echo "Please open the script with a text editor to set up all the variables and paths inside first."; 
echo "Here is also a quick dependency check to see if you a missing something:"
# assuming an arch based distro, the dependency check will be automated, 
# otherwise do it manually - list of packages in the array below
deps=(wget curl sed xclip libnotify gtk-engine-murrine appimagelauncher); for i in ${deps[@]}; do pacman -Q $i; done
exit 1; 
fi

# This script is build with modular structure, you can enable (1) or disable (0) 
# the different types of update checks in groups below

appimage_builtin_updaters=1
github_and_fixed_url=1

# Insert an space separated array of unique (parts of) appimage  
# names from your appimage folder, for example if you have my_prog1.AppImage
# and my_prog2.AppImage it can be internalupdater=(prog1 prog2)
# Those AppImages will automatically check for and update themselves, you will
# see a gui message about each of them. Only insert here appimages that CAN do it.
# You can check easily provided you integrated them with appimagemanager - 
# open the start menu and rightclick on the app - if you see update, it is capable

internalupdater=(goldendict)

# Set the directory where your AppImage or .gz files are stored

appimagedir=~/Applications/

# The github api that we use for the update check of github projects has 
# a limit of 60 anonymous requests/IP/hour. You can check your remaining ones with
# curl --request GET --url https://api.github.com/rate_limit
# if that is not enough, login in github, go to developer settings and make an 
# REST API key with public access. Then uncomment the line below and insert YOUR
# key in the variable. The limit will now be 5000. You can check with 
# curl --request GET --url https://api.github.com/rate_limit --header "Authorization: Bearer ghp_XXXXXXXXX"

#auth=(--header "Authorization: Bearer ghp_XXXXXXXXXXXXXXX")

# The last option to configure is to enable or disable the notification popups in case 
# an updated version is found. This is needed if you start the script in the background
# from a service or a cron job and do not see the messages in terminal at all

notification_popup=1

# Now is time to fill up the list with all other programs that we want to check for updates.
# We do this by comparing the disk size of the file and the online size.  
# Those will not update automatically, you will only get a message. Here you can put 
# 1) appimages that do not update automatically, or 
# 2) other progs distributed as zip or other archive formats, as long as 
# a) they are on github or 
# b) have a fixed download url like "viber.com/download/viber.gz"
# The links from sites different from github should not contain a changable version in the name.
# c) Also, after you download the archive and put it in the working folder "appimagedir", you
# leave it there after you extract the archive in subfolder. And finally
# d) when you extract the archive, make sure the archive and the subfolder have distinctive names

# So here is our array: a short part of appimage OR zip/tar name, which should be unique,
# and a download url without versions in it, or a github api url. For example, the normal
# latest download url from
# https://github.com/balena-io/etcher/releases/latest
# will become
# https://api.github.com/repos/balena-io/etcher/releases/latest

declare -A size_updates
size_updates["viber"]="https://download.cdn.viber.com/desktop/Linux/viber.AppImage"
size_updates["balena"]="https://api.github.com/repos/balena-io/etcher/releases/latest"
size_updates["rclone"]="https://api.github.com/repos/kapitainsky/RcloneBrowser/releases/latest"
size_updates["exif"]="https://api.github.com/repos/szTheory/exifcleaner/releases/latest"
size_updates["qefi"]="https://api.github.com/repos/Inokinoki/QEFIEntryManager/releases/latest"
size_updates["hfs-linux.zip"]="https://api.github.com/repos/rejetto/hfs/releases/latest"
size_updates["anydesk-"]="https://download.anydesk.com/linux/anydesk-latest-amd64.tar.gz"
size_updates["teamviewer_"]="https://download.teamviewer.com/download/linux/teamviewer_amd64.tar.xz"

# Start of the routine for appimages with internal updater

if [[ $appimage_builtin_updaters == 1 ]] ; then
for i in ${internalupdater[@]} ; do
	echo "updating $i..."
	app=$(cd $appimagedir; ls |grep AppImage |grep -i $i) 
	/usr/lib/appimagelauncher/update $appimagedir$app
done
fi

# Start of the routine for everything else

if [[ $github_and_fixed_url == 1 ]] ; then
for a in ${!size_updates[@]} ; do 	
	echo "comparing sizes for $a..."
	app=$(cd $appimagedir; ls |grep -i $a)
	localfilesize=$(stat $appimagedir$app | sed -n 's/.* Size: \([^ ]*\) .*/\1/p')
	gitty=0
		if [[ $(echo ${size_updates[$a]} | grep "github.com") ]]; then
			gitty=1
			curlie=$(curl -s --request GET --url ${size_updates[$a]} "${auth[@]}")
			if [[ $(echo "$curlie" | grep "browser_download_url.*x86_64.AppImage" | cut -d : -f 2,3 | tr -d \") ]]; then remotefileurl=$(echo "$curlie" | grep "browser_download_url.*x86_64.AppImage" | cut -d : -f 2,3 | tr -d \")
			elif [[ $(echo "$curlie" | grep "browser_download_url.*x64.AppImage" | cut -d : -f 2,3 | tr -d \") ]]; then remotefileurl=$(echo "$curlie" | grep "browser_download_url.*x64.AppImage" | cut -d : -f 2,3 | tr -d \")
			elif [[ $(echo "$curlie" | grep "browser_download_url.*.AppImage" | cut -d : -f 2,3 | tr -d \") ]]; then remotefileurl=$(echo "$curlie" | grep "browser_download_url.*.AppImage" | cut -d : -f 2,3 | tr -d \")
			elif [[ $(echo "$curlie" | grep "browser_download_url.*linux.zip" | cut -d : -f 2,3 | tr -d \") ]]; then remotefileurl=$(echo "$curlie" | grep "browser_download_url.*linux.zip" | cut -d : -f 2,3 | tr -d \")
			fi
		remotefilesize=$(wget $remotefileurl --spider --server-response -O - 2>&1 | sed -ne '/Content-Length/{s/.*: //;p}' | grep -xv 0)		
		else
		remotefilesize=$(wget ${size_updates[$a]} --spider --server-response -O - 2>&1 | sed -ne '/Content-Length/{s/.*: //;p}' | grep -xv 0)		
		fi
	if [[ $localfilesize == $remotefilesize ]]; then 
		echo "$a size match"
	elif [[ $remotefilesize == "" ]];
		then echo "You either have no internet or have hit the github rate limit or the remote file is moved - filesize of remote file $a cannot be obtained." 
	else
		echo "$a size differ, new remote size is $remotefilesize"
		#api conversion back to normal url and differentiate between git and nongit
		if [[ $gitty == 1 ]]; then 
			clip=$(echo "${size_updates[$a]}" | sed 's/api.github.com\/repos/github.com/g')
		else
			clip=$(echo "${size_updates[$a]}")
		fi		
		echo "$clip" | xclip -selection clipboard
		echo "download url for $a copied to clipboard"
		if [ "$notification_popup" == 1 ]; then
			notify-send -u normal "$a updated, url copied to clippboard"
		fi

	fi
done
fi

# pause at the end of execution if you append --launcher as a command line option, 
# useful for making desktop shortcuts instead of starting in terminal

if [[ "$1" == "--launcher" ]]; then
	read -n 1 -s -r -p "Press any key to close the window.";
fi

Make it executable, adjust the list of apps and variables inside and enjoy!

p.s. once again, note the “anydesk-” and “teamviewer_” in the example above. This is to guarantee uniqueness. I can imagine there are probably nicer ways to solve the problem with some more awk, sed and grepping and filtering ls, but for a first try this was the easiest for me.

If you want you can make a .desktop file to run with doubleclick:

[Desktop Entry]
Version=1.0
Type=Application
Name=Updater
Comment=Update notifier for github apps and appimages
Exec=PATH_TO_SCRIPT --launcher
Icon=system-software-update
Path=
Terminal=true
StartupNotify=false
1 Like