Skip to content

Commit 32e8309

Browse files
authored
Improve reliability to download hostlink artifact (#163)
1 parent 9e9ea2a commit 32e8309

File tree

1 file changed

+238
-12
lines changed

1 file changed

+238
-12
lines changed

scripts/linux/install.sh

Lines changed: 238 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,122 @@
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
417
if [[ $EUID -ne 0 ]]; then
518
echo "This script must be run as root (use sudo)"
6-
exit 1
19+
exit $EXIT_GENERAL
720
fi
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
10121
TOKEN_ID="default-token-id"
11122
TOKEN_KEY="default-token-key"
@@ -61,8 +172,63 @@ uninstall_existing() {
61172
}
62173

63174
latest_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

68234
detect_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+
85254
ARCH=$(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+
86263
HOSTLINK_TAR=hostlink_$VERSION.tar.gz
87264

88265
download_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

93318
extract_tar() {
94-
tar -xvf $HOSTLINK_TAR
319+
echo "Extracting archive..."
320+
tar -xvf "$TEMP_DIR/$HOSTLINK_TAR" -C "$TEMP_DIR"
95321
}
96322

97323
move_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

102328
create_directories() {
@@ -123,7 +349,7 @@ EOF
123349

124350
install_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

Comments
 (0)