11#! /usr/bin/env bash
22
3+ # Exit codes
4+ EXIT_GENERAL=1
5+ EXIT_DEPENDENCY=2
6+ EXIT_VERSION_FETCH=3
7+ EXIT_DOWNLOAD=4
8+ EXIT_UNSUPPORTED_ARCH=5
9+
10+ # Retry configuration
11+ MAX_RETRIES=5
12+ INITIAL_DELAY=1
13+ MAX_DELAY=60
14+ DOWNLOAD_TIMEOUT=60
15+
316# Check if running as root
417if [[ $EUID -ne 0 ]]; then
518 echo " This script must be run as root (use sudo)"
6- exit 1
19+ exit $EXIT_GENERAL
720fi
821
22+ check_dependencies () {
23+ # Check for curl
24+ if ! command -v curl & > /dev/null; then
25+ echo " ERROR: curl is required but not installed."
26+ echo " Please install curl and try again."
27+ exit $EXIT_DEPENDENCY
28+ fi
29+
30+ # Check for gzip validation tool (in priority order)
31+ if command -v xxd & > /dev/null; then
32+ GZIP_VALIDATOR=" xxd"
33+ elif command -v od & > /dev/null; then
34+ GZIP_VALIDATOR=" od"
35+ elif command -v file & > /dev/null; then
36+ GZIP_VALIDATOR=" file"
37+ else
38+ echo " ERROR: No gzip validation tool found."
39+ echo " Please install one of: xxd, od, or file"
40+ exit $EXIT_DEPENDENCY
41+ fi
42+ }
43+
44+ create_temp_dir () {
45+ TEMP_DIR=$( mktemp -d)
46+ if [ ! -d " $TEMP_DIR " ]; then
47+ echo " ERROR: Failed to create temporary directory."
48+ exit $EXIT_GENERAL
49+ fi
50+ echo " Using temporary directory: $TEMP_DIR "
51+ }
52+
53+ validate_gzip () {
54+ local file=" $1 "
55+
56+ if [ ! -f " $file " ]; then
57+ return 1
58+ fi
59+
60+ case $GZIP_VALIDATOR in
61+ xxd)
62+ local magic=$( xxd -p -l 2 " $file " 2> /dev/null)
63+ [ " $magic " = " 1f8b" ]
64+ ;;
65+ od)
66+ local magic=$( od -A n -t x1 -N 2 " $file " 2> /dev/null | tr -d ' \n' )
67+ [ " $magic " = " 1f8b" ]
68+ ;;
69+ file)
70+ file " $file " 2> /dev/null | grep -q gzip
71+ ;;
72+ * )
73+ return 1
74+ ;;
75+ esac
76+ }
77+
78+ calculate_backoff () {
79+ local attempt=$1
80+ local delay=$(( INITIAL_DELAY << (attempt - 1 )) )
81+ if [ " $delay " -gt " $MAX_DELAY " ]; then
82+ delay=$MAX_DELAY
83+ fi
84+ # Add jitter (0-1 second)
85+ local jitter
86+ jitter=$( awk ' BEGIN{srand(); printf "%.2f", rand()}' )
87+ awk " BEGIN{printf \" %.2f\" , $delay + $jitter }"
88+ }
89+
90+ show_download_error () {
91+ local url=" $1 "
92+ local http_status=" $2 "
93+ local file=" $3 "
94+ local file_size=0
95+ local file_content=" "
96+
97+ if [ -f " $file " ]; then
98+ file_size=$( stat -c%s " $file " 2> /dev/null || stat -f%z " $file " 2> /dev/null || echo " unknown" )
99+ file_content=$( head -c 100 " $file " 2> /dev/null | tr -cd ' [:print:]' )
100+ fi
101+
102+ echo " "
103+ echo " ERROR: Failed to download hostlink after $MAX_RETRIES attempts."
104+ echo " "
105+ echo " Last attempt details:"
106+ echo " - HTTP Status: $http_status "
107+ echo " - File size received: $file_size bytes"
108+ if [ -n " $file_content " ]; then
109+ echo " - File content (first 100 chars): \" $file_content \" "
110+ fi
111+ echo " - Expected: Valid gzip archive (>1MB typically)"
112+ echo " "
113+ echo " Manual download URL:"
114+ echo " $url "
115+ echo " "
116+ echo " You can download this file manually and extract it to /usr/bin/hostlink"
117+ echo " Downloaded file location: $file "
118+ }
119+
9120# Default values
10121TOKEN_ID=" default-token-id"
11122TOKEN_KEY=" default-token-key"
@@ -61,8 +172,63 @@ uninstall_existing() {
61172}
62173
63174latest_version () {
64- local version=$( curl -s https://api.github.com/repos/selfhost-dev/hostlink/releases/latest | grep tag_name | cut -d' "' -f4)
65- echo $version
175+ local api_url=" https://api.github.com/repos/selfhost-dev/hostlink/releases/latest"
176+ local attempt=0
177+ local version=" "
178+ local http_status=" "
179+ local temp_file=" $TEMP_DIR /version_response.json"
180+
181+ echo " Fetching latest version..." >&2
182+
183+ while [ " $attempt " -lt " $MAX_RETRIES " ]; do
184+ attempt=$(( attempt + 1 ))
185+
186+ # Fetch with timeout and capture HTTP status
187+ http_status=$( curl -sS --max-time " $DOWNLOAD_TIMEOUT " -w ' %{http_code}' -o " $temp_file " " $api_url " 2> /dev/null)
188+
189+ # Check for 404 - fail immediately
190+ if [ " $http_status " = " 404" ]; then
191+ echo " ERROR: Could not find release information (HTTP 404)." >&2
192+ echo " The GitHub API endpoint may have changed or the repository may not exist." >&2
193+ exit $EXIT_VERSION_FETCH
194+ fi
195+
196+ # Check for success (2xx status)
197+ if [[ " $http_status " =~ ^2[0-9][0-9]$ ]]; then
198+ version=$( grep tag_name " $temp_file " 2> /dev/null | cut -d' "' -f4)
199+
200+ if [ -n " $version " ]; then
201+ echo " Latest version: $version " >&2
202+ rm -f " $temp_file "
203+ echo " $version "
204+ return 0
205+ fi
206+ fi
207+
208+ # If we get here, we need to retry (unless it's the last attempt)
209+ if [ $attempt -lt $MAX_RETRIES ]; then
210+ local delay
211+ delay=$( calculate_backoff $attempt )
212+ echo " Version fetch failed (HTTP $http_status ). Retry $attempt /$MAX_RETRIES in ${delay} s..." >&2
213+ sleep " $delay "
214+ fi
215+ done
216+
217+ # All retries exhausted
218+ echo " " >&2
219+ echo " ERROR: Failed to fetch latest version after $MAX_RETRIES attempts." >&2
220+ echo " Last HTTP status: $http_status " >&2
221+ if [ -f " $temp_file " ]; then
222+ local content
223+ content=$( head -c 100 " $temp_file " 2> /dev/null | tr -cd ' [:print:]' )
224+ if [ -n " $content " ]; then
225+ echo " Response content: \" $content \" " >&2
226+ fi
227+ fi
228+ echo " " >&2
229+ echo " Please check your internet connection and try again." >&2
230+ rm -f " $temp_file "
231+ exit $EXIT_VERSION_FETCH
66232}
67233
68234detect_arch () {
@@ -75,28 +241,88 @@ detect_arch() {
75241 echo " arm64"
76242 ;;
77243 * )
78- echo " Unsupported architecture: $arch " >&2
79- exit 1
244+ echo " ERROR: Unsupported architecture: $arch " >&2
245+ exit $EXIT_UNSUPPORTED_ARCH
80246 ;;
81247 esac
82248}
83249
84- VERSION=$( latest_version)
250+ # Run dependency checks first
251+ check_dependencies
252+ create_temp_dir
253+
85254ARCH=$( detect_arch)
255+
256+ # latest_version outputs messages to stderr, version to stdout
257+ VERSION=$( latest_version)
258+ if [ -z " $VERSION " ]; then
259+ echo " ERROR: Failed to determine version."
260+ exit $EXIT_VERSION_FETCH
261+ fi
262+
86263HOSTLINK_TAR=hostlink_$VERSION .tar.gz
87264
88265download_tar () {
89- curl -L -o $HOSTLINK_TAR \
90- https://github.com/selfhost-dev/hostlink/releases/download/${VERSION} /hostlink_Linux_${ARCH} .tar.gz
266+ local download_url=" https://github.com/selfhost-dev/hostlink/releases/download/${VERSION} /hostlink_Linux_${ARCH} .tar.gz"
267+ local tar_file=" $TEMP_DIR /$HOSTLINK_TAR "
268+ local attempt=0
269+ local http_status=" "
270+
271+ echo " Downloading hostlink $VERSION ..."
272+
273+ while [ " $attempt " -lt " $MAX_RETRIES " ]; do
274+ attempt=$(( attempt + 1 ))
275+
276+ # Download with timeout and capture HTTP status
277+ http_status=$( curl -L -sS --max-time " $DOWNLOAD_TIMEOUT " -w ' %{http_code}' -o " $tar_file " " $download_url " 2> /dev/null)
278+
279+ # Check for 404 - fail immediately
280+ if [ " $http_status " = " 404" ]; then
281+ echo " "
282+ echo " ERROR: Release not found (HTTP 404)."
283+ echo " Version $VERSION may not exist or the release assets may not be available."
284+ echo " "
285+ echo " Manual download URL:"
286+ echo " $download_url "
287+ exit $EXIT_DOWNLOAD
288+ fi
289+
290+ # Check for success (2xx status) and validate gzip
291+ if [[ " $http_status " =~ ^2[0-9][0-9]$ ]]; then
292+ if validate_gzip " $tar_file " ; then
293+ echo " Download successful."
294+ return 0
295+ else
296+ echo " Download completed but file is not valid gzip (possibly corrupted)."
297+ fi
298+ fi
299+
300+ # If we get here, we need to retry (unless it's the last attempt)
301+ if [ " $attempt " -lt " $MAX_RETRIES " ]; then
302+ local delay
303+ delay=$( calculate_backoff " $attempt " )
304+ if [[ " $http_status " =~ ^2[0-9][0-9]$ ]]; then
305+ echo " Validation failed. Retry $attempt /$MAX_RETRIES in ${delay} s..."
306+ else
307+ echo " Download failed (HTTP $http_status ). Retry $attempt /$MAX_RETRIES in ${delay} s..."
308+ fi
309+ sleep " $delay "
310+ fi
311+ done
312+
313+ # All retries exhausted
314+ show_download_error " $download_url " " $http_status " " $tar_file "
315+ exit $EXIT_DOWNLOAD
91316}
92317
93318extract_tar () {
94- tar -xvf $HOSTLINK_TAR
319+ echo " Extracting archive..."
320+ tar -xvf " $TEMP_DIR /$HOSTLINK_TAR " -C " $TEMP_DIR "
95321}
96322
97323move_bin () {
98- echo " Moving binary to /usr/bin, password prompt might be required ."
99- sudo mv . /hostlink /usr/bin/hostlink
324+ echo " Moving binary to /usr/bin.. ."
325+ sudo mv " $TEMP_DIR /hostlink" /usr/bin/hostlink
100326}
101327
102328create_directories () {
123349
124350install_service () {
125351 echo " Installing systemd service..."
126- sudo cp . /scripts/hostlink.service /etc/systemd/system/
352+ sudo cp " $TEMP_DIR /scripts/hostlink.service" /etc/systemd/system/
127353 sudo systemctl daemon-reload
128354 sudo systemctl enable hostlink
129355 sudo systemctl start hostlink
0 commit comments