1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
|
#!/bin/sh
# backup.sh - OpenBSD Server Backup Script
# Author: Biswa Kalyan Bhuyan ([email protected])
#
# DEPLOYMENT INSTRUCTIONS:
# 1. Copy this script to your OpenBSD server: /usr/local/bin/backup.sh
# 2. Make it executable: chmod +x /usr/local/bin/backup.sh
# 3. Install dependencies: pkg_add rclone mailx
# 4. Configure rclone: rclone config (setup 'cf' remote)
# 5. Test: backup.sh --dir /etc --dry-run
# 6. Run: backup.sh --dir /etc,/var/www,/var/log
# 7. Add to crontab for automation
#
# Usage: ./backup.sh <tarball_file>
# ./backup.sh --dir /etc,/var/www,/home
# ./backup.sh --dir /etc,/var/log --dry-run
set -e
# Color codes for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Configuration variables
RCLONE_REMOTE="cf:backups/"
COMPRESSION_LEVEL=9
EMAIL_RECIPIENT="[email protected]"
LOG_FILE="/var/log/backup-log-$(date +%Y%m%d).log"
HOSTNAME=$(hostname -s)
BACKUP_BASE_DIR="/tmp/backups-$(date +%Y%m%d)"
DRY_RUN=false
# Server settings
BACKUP_USER="root"
MIN_FREE_SPACE_MB=1024
MAX_LOG_FILES=10
# Write log message with timestamp
log_with_timestamp() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$LOG_FILE"
}
# Print status message with blue color and log it
print_status() {
local message="${BLUE}[INFO]${NC} $1"
log_with_timestamp "[INFO] $1"
printf "%s\n" "$message" >&2
}
# Print success message with green color and log it
print_success() {
local message="${GREEN}[SUCCESS]${NC} $1"
log_with_timestamp "[SUCCESS] $1"
printf "%s\n" "$message" >&2
}
# Print warning message with yellow color and log it
print_warning() {
local message="${YELLOW}[WARNING]${NC} $1"
log_with_timestamp "[WARNING] $1"
printf "%s\n" "$message" >&2
}
# Print error message with red color and log it
print_error() {
local message="${RED}[ERROR]${NC} $1"
log_with_timestamp "[ERROR] $1"
printf "%s\n" "$message" >&2
}
# Send email with backup log details
send_log_email() {
local exit_status="$1"
local subject_prefix="SUCCESS"
if [ "$exit_status" -ne 0 ]; then
subject_prefix="FAILED"
fi
local subject="[$subject_prefix] Backup Script Log - $HOSTNAME - $(date '+%Y-%m-%d %H:%M:%S')"
if command -v mail >/dev/null 2>&1; then
{
echo "Backup script execution log from $HOSTNAME"
echo "=========================================="
echo "Execution time: $(date)"
echo "Exit status: $exit_status"
echo "Script arguments: $ORIGINAL_ARGS"
echo ""
echo "Full log output:"
echo "----------------"
cat "$LOG_FILE" 2>/dev/null || echo "Log file not found"
} | mail -s "$subject" "$EMAIL_RECIPIENT"
echo "Log email sent to $EMAIL_RECIPIENT" >&2
else
echo "Warning: mail command not available. Log saved to: $LOG_FILE" >&2
fi
}
# Clean up temporary files and send email on script exit
cleanup_and_email() {
local exit_code=$?
send_log_email "$exit_code"
if [ -d "$BACKUP_BASE_DIR" ]; then
rm -rf "$BACKUP_BASE_DIR"
print_status "Cleaned up temporary directory: $BACKUP_BASE_DIR"
fi
if [ "$exit_code" -eq 0 ] && [ -f "$LOG_FILE" ]; then
rm -f "$LOG_FILE"
fi
exit $exit_code
}
trap cleanup_and_email EXIT
# Check if script is running with appropriate permissions
check_user_permissions() {
if [ "$(id -u)" -ne 0 ] && [ "$DRY_RUN" = "false" ]; then
print_warning "Not running as root. Some system directories may not be accessible."
print_warning "For complete server backups, consider running as root: sudo $0 $ORIGINAL_ARGS"
fi
local log_dir=$(dirname "$LOG_FILE")
if [ ! -w "$log_dir" ]; then
print_warning "Cannot write to log directory: $log_dir"
LOG_FILE="/tmp/backup-log-$(date +%Y%m%d).log"
print_status "Using alternative log location: $LOG_FILE"
fi
}
# Remove old log files keeping only the latest MAX_LOG_FILES
cleanup_old_logs() {
local log_dir=$(dirname "$LOG_FILE")
local log_pattern="backup-log-*.log"
if [ -d "$log_dir" ] && [ -w "$log_dir" ]; then
find "$log_dir" -name "$log_pattern" -type f 2>/dev/null | \
sort -r | \
tail -n +$((MAX_LOG_FILES + 1)) | \
while read -r old_log; do
rm -f "$old_log" 2>/dev/null
print_status "Cleaned up old log file: $(basename "$old_log")"
done
fi
}
# Initialize log file with header information
initialize_log() {
local log_dir=$(dirname "$LOG_FILE")
if [ ! -d "$log_dir" ]; then
mkdir -p "$log_dir" 2>/dev/null || {
LOG_FILE="/tmp/backup-log-$(date +%Y%m%d).log"
print_warning "Created log in /tmp instead: $LOG_FILE"
}
fi
echo "==================================================" > "$LOG_FILE"
echo "Server Backup Script Execution Log" >> "$LOG_FILE"
echo "==================================================" >> "$LOG_FILE"
echo "Start time: $(date)" >> "$LOG_FILE"
echo "Hostname: $HOSTNAME" >> "$LOG_FILE"
echo "Script: $0" >> "$LOG_FILE"
echo "Arguments: $ORIGINAL_ARGS" >> "$LOG_FILE"
echo "User: $(whoami) (UID: $(id -u))" >> "$LOG_FILE"
echo "Log file: $LOG_FILE" >> "$LOG_FILE"
echo "Server info: $(uname -a)" >> "$LOG_FILE"
echo "==================================================" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
cleanup_old_logs
}
# Verify required tools are installed and available
check_dependencies() {
print_status "Checking dependencies..."
if ! command -v tar >/dev/null 2>&1; then
print_error "tar is not installed or not in PATH"
exit 1
fi
if ! command -v gzip >/dev/null 2>&1; then
print_error "gzip is not installed or not in PATH"
exit 1
fi
if [ "$DRY_RUN" = "false" ]; then
if ! command -v rclone >/dev/null 2>&1; then
print_error "rclone is not installed or not in PATH"
print_error "Please install rclone: pkg_add rclone"
print_error "Or use --dry-run flag to test without uploading"
exit 1
fi
else
print_warning "DRY RUN mode enabled - skipping rclone check"
fi
if ! command -v mail >/dev/null 2>&1; then
print_warning "mail command not available - logs will be saved locally only"
log_with_timestamp "[WARNING] Consider installing mailx: pkg_add mailx"
fi
print_success "All essential dependencies found"
}
# Create tar archive from directory with error handling
create_tar_archive() {
local source_dir="$1"
local archive_name="$2"
local tar_file="$BACKUP_BASE_DIR/$archive_name.tar"
print_status "Creating tar archive for: $source_dir"
if [ ! -d "$source_dir" ]; then
print_error "Directory does not exist: $source_dir"
return 1
fi
if [ ! -r "$source_dir" ]; then
print_error "Directory is not readable: $source_dir"
print_error "Try running as root or check permissions"
return 1
fi
local dir_size=$(du -sh "$source_dir" 2>/dev/null | cut -f1)
print_status "Directory size: $dir_size"
local backup_dir_parent=$(dirname "$BACKUP_BASE_DIR")
local available_space=$(df "$backup_dir_parent" | awk 'NR==2 {print $4}')
local available_space_mb=$(( available_space / 1024 ))
print_status "Available space in backup location: ${available_space_mb} MB"
if [ "$available_space_mb" -lt "$MIN_FREE_SPACE_MB" ]; then
print_error "Insufficient disk space. Available: ${available_space_mb}MB, Required: ${MIN_FREE_SPACE_MB}MB"
return 1
fi
local start_time=$(date +%s)
local parent_dir=$(dirname "$source_dir")
local dir_name=$(basename "$source_dir")
print_status "Creating archive: $tar_file"
if (cd "$parent_dir" && tar -cf "$tar_file" "$dir_name") 2>&1 | tee -a "$LOG_FILE" >&2; then
local end_time=$(date +%s)
local duration=$(( end_time - start_time ))
if [ ! -f "$tar_file" ]; then
print_error "Tar file was not created: $tar_file"
return 1
fi
local tar_size=$(stat -f%z "$tar_file" 2>/dev/null || stat -c%s "$tar_file" 2>/dev/null)
if [ "$tar_size" -eq 0 ]; then
print_warning "Tar file is empty, this might indicate an issue"
fi
print_success "Tar archive created in ${duration} seconds"
print_status "Archive size: $(( tar_size / 1024 / 1024 )) MB"
echo "$tar_file"
return 0
else
local tar_exit_code=$?
print_error "Failed to create tar archive for: $source_dir (exit code: $tar_exit_code)"
print_error "Check the log file for detailed error messages: $LOG_FILE"
if [ ! -w "$backup_dir_parent" ]; then
print_error "Backup directory is not writable: $backup_dir_parent"
fi
return 1
fi
}
# Remove leading slash and replace other slashes with hyphens for filenames
sanitize_filename() {
local path="$1"
echo "$path" | sed 's/^\/*//' | sed 's/\//-/g'
}
# Compress tar file using gzip with maximum compression
compress_tarball() {
local input_file="$1"
local output_file="${input_file}.gz"
print_status "Compressing '$(basename "$input_file")' with maximum compression (level $COMPRESSION_LEVEL)..."
local original_size=$(stat -f%z "$input_file" 2>/dev/null || stat -c%s "$input_file" 2>/dev/null)
log_with_timestamp "Starting compression - Original size: $original_size bytes"
local start_time=$(date +%s)
if gzip -${COMPRESSION_LEVEL} -c "$input_file" > "$output_file"; then
local end_time=$(date +%s)
local duration=$(( end_time - start_time ))
local compressed_size=$(stat -f%z "$output_file" 2>/dev/null || stat -c%s "$output_file" 2>/dev/null)
local compression_ratio=$(( (original_size - compressed_size) * 100 / original_size ))
print_success "Compression completed in ${duration} seconds"
print_status "Original size: $(( original_size / 1024 / 1024 )) MB"
print_status "Compressed size: $(( compressed_size / 1024 / 1024 )) MB"
print_status "Compression ratio: ${compression_ratio}%"
print_status "Space saved: $(( (original_size - compressed_size) / 1024 / 1024 )) MB"
echo "$output_file"
return 0
else
print_error "Failed to compress '$(basename "$input_file")'"
return 1
fi
}
# Upload file to cloud storage using rclone
upload_to_cloud() {
local file_to_upload="$1"
print_status "Uploading '$(basename "$file_to_upload")' to $RCLONE_REMOTE..."
if [ "$DRY_RUN" = "true" ]; then
print_warning "DRY RUN: Skipping actual upload"
local file_size=$(stat -f%z "$file_to_upload" 2>/dev/null || stat -c%s "$file_to_upload" 2>/dev/null)
print_status "Would upload file size: $(( file_size / 1024 / 1024 )) MB"
sleep 1
print_success "DRY RUN: Upload simulation completed"
return 0
fi
if ! command -v rclone >/dev/null 2>&1; then
print_error "rclone is not installed or not in PATH"
print_error "Please install rclone: pkg_add rclone"
return 1
fi
if ! rclone listremotes | grep -q "cf:"; then
print_error "rclone remote 'cf' is not configured"
print_error "Please configure rclone first: rclone config"
return 1
fi
local file_size=$(stat -f%z "$file_to_upload" 2>/dev/null || stat -c%s "$file_to_upload" 2>/dev/null)
print_status "Upload file size: $(( file_size / 1024 / 1024 )) MB"
local start_time=$(date +%s)
print_status "Starting upload with progress monitoring..."
if rclone copy "$file_to_upload" "$RCLONE_REMOTE" --progress --stats=1s >> "$LOG_FILE" 2>&1; then
local end_time=$(date +%s)
local duration=$(( end_time - start_time ))
print_success "Upload completed successfully in ${duration} seconds"
return 0
else
print_error "Upload failed"
return 1
fi
}
# Remove local tar and compressed files after successful upload
cleanup_files() {
local original_tar="$1"
local compressed_file="$2"
print_status "Cleaning up local files..."
if [ -f "$original_tar" ]; then
local tar_size=$(stat -f%z "$original_tar" 2>/dev/null || stat -c%s "$original_tar" 2>/dev/null)
if rm "$original_tar"; then
print_success "Removed original tar file: $(basename "$original_tar") ($(( tar_size / 1024 / 1024 )) MB freed)"
else
print_warning "Failed to remove original tar file: $(basename "$original_tar")"
fi
fi
if [ -f "$compressed_file" ]; then
local gz_size=$(stat -f%z "$compressed_file" 2>/dev/null || stat -c%s "$compressed_file" 2>/dev/null)
if rm "$compressed_file"; then
print_success "Removed compressed file: $(basename "$compressed_file") ($(( gz_size / 1024 / 1024 )) MB freed)"
else
print_warning "Failed to remove compressed file: $(basename "$compressed_file")"
fi
fi
}
# Process single tar file: compress, upload, and optionally cleanup
process_tar_file() {
local tarball="$1"
local auto_cleanup="${2:-false}"
print_status "Processing tar file: $(basename "$tarball")"
local compressed_file
if compressed_file=$(compress_tarball "$tarball"); then
if upload_to_cloud "$compressed_file"; then
print_success "Successfully processed: $(basename "$tarball")"
if [ "$auto_cleanup" = "true" ]; then
cleanup_files "$tarball" "$compressed_file"
fi
return 0
else
print_error "Failed to upload: $(basename "$tarball")"
return 1
fi
else
print_error "Failed to compress: $(basename "$tarball")"
return 1
fi
}
# Validate all directories exist and are readable before starting backup
validate_backup_directories() {
local dir_list="$1"
local temp_validation_file="/tmp/dir-validation-$$"
local valid_count=0
local invalid_count=0
print_status "Validating directories before backup..."
echo "$dir_list" | tr ',' '\n' > "$temp_validation_file"
while IFS= read -r dir; do
dir=$(echo "$dir" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$dir" ]; then
continue
fi
if [ ! -d "$dir" ]; then
print_error "Directory does not exist: $dir"
invalid_count=$((invalid_count + 1))
elif [ ! -r "$dir" ]; then
print_error "Directory is not readable: $dir"
invalid_count=$((invalid_count + 1))
else
print_status "✓ Valid directory: $dir"
valid_count=$((valid_count + 1))
fi
done < "$temp_validation_file"
rm -f "$temp_validation_file"
if [ $invalid_count -gt 0 ]; then
print_error "Found $invalid_count invalid directories. Aborting backup."
return 1
fi
print_success "All $valid_count directories validated successfully"
return 0
}
# Create tar archives for multiple directories and upload them
backup_directories() {
local dir_list="$1"
local failed_count=0
local success_count=0
local temp_list_file="/tmp/dir-list-$$"
print_status "Starting directory backup mode"
print_status "Target directories: $dir_list"
if ! validate_backup_directories "$dir_list"; then
exit 1
fi
if ! mkdir -p "$BACKUP_BASE_DIR"; then
print_error "Failed to create backup directory: $BACKUP_BASE_DIR"
exit 1
fi
print_status "Created temporary backup directory: $BACKUP_BASE_DIR"
echo "$dir_list" | tr ',' '\n' > "$temp_list_file"
while IFS= read -r dir; do
dir=$(echo "$dir" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$dir" ]; then
continue
fi
echo >&2
print_status "=== Processing directory: $dir ==="
local sanitized_name=$(sanitize_filename "$dir")
local timestamp=$(date +%Y%m%d)
local archive_name="${sanitized_name}-${timestamp}"
if tar_file=$(create_tar_archive "$dir" "$archive_name"); then
if process_tar_file "$tar_file" "true"; then
success_count=$((success_count + 1))
print_success "Completed backup of: $dir"
else
failed_count=$((failed_count + 1))
print_error "Failed backup of: $dir"
fi
else
failed_count=$((failed_count + 1))
print_error "Failed to create archive for: $dir"
fi
done < "$temp_list_file"
rm -f "$temp_list_file"
echo >&2
print_status "=== Backup Summary ==="
print_status "Successful backups: $success_count"
if [ "$failed_count" -gt 0 ]; then
print_error "Failed backups: $failed_count"
return 1
else
print_success "All directory backups completed successfully!"
return 0
fi
}
# Validate input tar file exists and is readable
validate_input() {
local tarball="$1"
if [ -z "$tarball" ]; then
print_error "No tarball file specified"
exit 1
fi
if [ ! -f "$tarball" ]; then
print_error "File '$tarball' does not exist"
exit 1
fi
local file_size=$(stat -f%z "$tarball" 2>/dev/null || stat -c%s "$tarball" 2>/dev/null)
print_status "Input file: $tarball"
print_status "File size: $(( file_size / 1024 / 1024 )) MB"
if ! file "$tarball" | grep -q "tar archive\|POSIX tar archive"; then
print_warning "File '$tarball' might not be a valid tar archive"
echo -n "Continue anyway? (y/N): " >&2
read -r response
case "$response" in
[yY]|[yY][eE][sS])
log_with_timestamp "User chose to continue with non-tar file"
;;
*)
print_error "Operation cancelled by user"
exit 1
;;
esac
else
print_success "File validated as tar archive"
fi
}
# Parse command line arguments and set global variables
parse_arguments() {
while [ $# -gt 0 ]; do
case "$1" in
--dir)
if [ -z "$2" ]; then
print_error "--dir flag requires directory list"
exit 1
fi
DIRECTORY_MODE=true
DIRECTORY_LIST="$2"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
--help|-h)
show_usage
exit 0
;;
-*)
print_error "Unknown option: $1"
show_usage
exit 1
;;
*)
if [ -z "$TARBALL_FILE" ]; then
TARBALL_FILE="$1"
else
print_error "Multiple file arguments not supported"
exit 1
fi
shift
;;
esac
done
}
# Display usage information and examples
show_usage() {
cat >&2 << EOF
OpenBSD Server Backup Script
============================
Usage:
$0 <tarball_file> # Process existing tarball
$0 --dir /etc,/var/www,/home # Backup server directories
$0 --dir /etc,/var/log,/usr/local # System directories backup
Options:
--dir DIRS Comma-separated list of directories to backup
--dry-run Test mode - skip actual upload to cloud storage
--help, -h Show this help message
Common Server Backup Examples:
sudo $0 --dir /etc,/var/www,/var/log # Web server
sudo $0 --dir /etc,/var/mail,/home # Mail server
sudo $0 --dir /etc,/var/lib/mysql,/var/www # Database + Web
sudo $0 --dir /etc,/usr/local/etc,/var/log # System config
This script will:
1. Validate directories and permissions
2. Create individual tar archives for each directory
3. Compress with maximum gzip compression (level 9)
4. Upload to cloud storage using rclone
5. Clean up local files after successful upload
6. Send detailed logs via email to $EMAIL_RECIPIENT
7. Maintain log rotation (keeps last $MAX_LOG_FILES logs)
Server Requirements:
- Run as root for complete system backups
- tar (pre-installed on OpenBSD)
- gzip (pre-installed on OpenBSD)
- rclone (install with: pkg_add rclone)
- mailx (install with: pkg_add mailx) for email notifications
- Configured rclone remote named 'cf'
- Minimum $MIN_FREE_SPACE_MB MB free space in /tmp
Log Location: $LOG_FILE
EOF
}
# Main script execution function
main() {
print_status "Starting server backup process on $HOSTNAME..."
if [ "$DRY_RUN" = "true" ]; then
print_warning "DRY RUN MODE ENABLED - No files will be uploaded"
fi
print_status "Email notifications will be sent to: $EMAIL_RECIPIENT"
print_status "Logs will be saved to: $LOG_FILE"
echo >&2
check_user_permissions
echo >&2
check_dependencies
echo >&2
if [ "$DIRECTORY_MODE" = "true" ]; then
if backup_directories "$DIRECTORY_LIST"; then
print_success "All directory backups completed successfully!"
else
print_error "Some directory backups failed"
exit 1
fi
else
validate_input "$TARBALL_FILE"
echo >&2
if process_tar_file "$TARBALL_FILE"; then
print_success "Tarball backup completed successfully!"
else
print_error "Tarball backup failed"
exit 1
fi
fi
echo >&2
print_success "All operations completed!"
}
# Global variables for argument parsing
ORIGINAL_ARGS="$*"
DIRECTORY_MODE=false
DIRECTORY_LIST=""
TARBALL_FILE=""
# Parse command line arguments
parse_arguments "$@"
# Initialize logging
initialize_log
# Validate arguments
if [ "$DIRECTORY_MODE" = "false" ] && [ -z "$TARBALL_FILE" ]; then
show_usage
exit 1
fi
# Run main function
main
|