mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Added one-shot install scripts
This commit is contained in:
@@ -116,6 +116,15 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- Job Failed: When a job fails to start (enabled by default)
|
||||
- Queue Complete: When all queued jobs finish
|
||||
- New "Print Queue" section in notification provider settings
|
||||
- **Installation Scripts** - Interactive install scripts for Linux and macOS:
|
||||
- Native install (`install.sh`): Python venv, systemd/launchd service, Node.js 22
|
||||
- Docker install (`docker-install.sh`): Docker Compose setup with health checks
|
||||
- Interactive prompts for: install path, port, bind address, timezone, data/log directories
|
||||
- Unattended mode with `--yes` flag for automation
|
||||
- Auto-detects OS and package manager (apt, dnf, pacman, brew)
|
||||
- Option to set system timezone during installation
|
||||
- Shows IP address for network access when binding to 0.0.0.0
|
||||
- Supports updating existing installations
|
||||
|
||||
### Fixes
|
||||
- **Multi-Plate Thumbnail in Queue** - Fixed queue items showing wrong thumbnail for multi-plate files (Issue #166):
|
||||
|
||||
+48
-18
@@ -1,31 +1,61 @@
|
||||
# BamBuddy Systemd Service Template
|
||||
#
|
||||
# INSTALLATION:
|
||||
# 1. Copy this file to /etc/systemd/system/bambuddy.service
|
||||
# 2. Replace placeholders:
|
||||
# - INSTALL_PATH: Where BamBuddy is installed (e.g., /opt/bambuddy)
|
||||
# - SERVICE_USER: User to run as (e.g., bambuddy)
|
||||
# - DATA_DIR: Data directory (e.g., /opt/bambuddy/data)
|
||||
# - LOG_DIR: Log directory (e.g., /opt/bambuddy/logs)
|
||||
# 3. Run: sudo systemctl daemon-reload
|
||||
# 4. Run: sudo systemctl enable bambuddy
|
||||
# 5. Run: sudo systemctl start bambuddy
|
||||
#
|
||||
# Or use the install script: ./install/install.sh
|
||||
#
|
||||
|
||||
[Unit]
|
||||
Description=BamBuddy Print Archive
|
||||
Description=BamBuddy - Bambu Lab Print Management
|
||||
Documentation=https://github.com/maziggy/bambuddy
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=claude
|
||||
Group=claude
|
||||
WorkingDirectory=<dir>/bambuddy
|
||||
Environment="PATH=<dir/bambuddy/venv/bin"
|
||||
User=SERVICE_USER
|
||||
Group=SERVICE_USER
|
||||
WorkingDirectory=INSTALL_PATH
|
||||
|
||||
# Force kill after 10 seconds if graceful shutdown fails
|
||||
# Environment file (optional - created by install script)
|
||||
EnvironmentFile=-INSTALL_PATH/.env
|
||||
|
||||
# Use virtual environment
|
||||
Environment="PATH=INSTALL_PATH/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
|
||||
# Server configuration
|
||||
ExecStart=INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port ${PORT:-8000}
|
||||
|
||||
# Restart policy
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Graceful shutdown
|
||||
TimeoutStopSec=10
|
||||
|
||||
# Kill any zombie ffmpeg processes before starting/after stopping
|
||||
ExecStartPre=-/usr/bin/pkill -9 ffmpeg
|
||||
ExecStopPost=-/usr/bin/pkill -9 ffmpeg
|
||||
# Kill zombie ffmpeg processes (timelapse processing)
|
||||
ExecStartPre=-/usr/bin/pkill -9 -f "ffmpeg.*bambuddy"
|
||||
ExecStopPost=-/usr/bin/pkill -9 -f "ffmpeg.*bambuddy"
|
||||
|
||||
# Ensure directories exist and have correct permissions before starting
|
||||
# The + prefix runs the command as root even though User=claude
|
||||
ExecStartPre=+/bin/mkdir -p <dir>/bambuddy/logs
|
||||
ExecStartPre=+/bin/mkdir -p <dir>/bambuddy/archive
|
||||
ExecStartPre=+/bin/chown -R <user>:<user> <dir>/bambuddy/logs
|
||||
ExecStartPre=+/bin/chown -R <user>:<user> <dir>/bambuddy/archive
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=bambuddy
|
||||
|
||||
ExecStart=<dir>/bambuddy/venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port 8000
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=DATA_DIR LOG_DIR INSTALL_PATH
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
# BamBuddy Installation Scripts
|
||||
|
||||
Interactive installation scripts for BamBuddy with support for both native and Docker deployments.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Docker Installation (Recommended)
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/docker-install.sh | bash
|
||||
```
|
||||
|
||||
### Native Installation
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scripts Overview
|
||||
|
||||
| Script | Platform | Method |
|
||||
|--------|----------|--------|
|
||||
| `install.sh` | Linux, macOS | Native (Python venv) |
|
||||
| `docker-install.sh` | Linux, macOS | Docker |
|
||||
|
||||
---
|
||||
|
||||
## Native Installation Scripts
|
||||
|
||||
### `install.sh` (Linux/macOS)
|
||||
|
||||
Installs BamBuddy with Python virtual environment and optional systemd/launchd service.
|
||||
|
||||
**Supported Systems:**
|
||||
- Debian/Ubuntu (apt)
|
||||
- RHEL/Fedora/CentOS (dnf/yum)
|
||||
- Arch Linux (pacman)
|
||||
- openSUSE (zypper)
|
||||
- macOS (Homebrew)
|
||||
|
||||
**Options:**
|
||||
```
|
||||
--path PATH Installation directory (default: /opt/bambuddy)
|
||||
--port PORT Port to listen on (default: 8000)
|
||||
--tz TIMEZONE Timezone (default: system timezone)
|
||||
--data-dir PATH Data directory (default: INSTALL_PATH/data)
|
||||
--log-dir PATH Log directory (default: INSTALL_PATH/logs)
|
||||
--debug Enable debug mode
|
||||
--log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
|
||||
--no-service Skip systemd/launchd service setup
|
||||
--yes, -y Non-interactive mode, accept defaults
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Interactive installation
|
||||
./install.sh
|
||||
|
||||
# Unattended with custom settings
|
||||
./install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes
|
||||
|
||||
# Minimal unattended
|
||||
./install.sh -y
|
||||
|
||||
# Skip service setup
|
||||
./install.sh --no-service -y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Installation Scripts
|
||||
|
||||
### `docker-install.sh` (Linux/macOS)
|
||||
|
||||
Installs BamBuddy using Docker containers.
|
||||
|
||||
**Options:**
|
||||
```
|
||||
--path PATH Installation directory (default: ~/bambuddy)
|
||||
--port PORT Port to expose (default: 8000)
|
||||
--tz TIMEZONE Timezone (default: system timezone)
|
||||
--build Build from source instead of using pre-built image
|
||||
--yes, -y Non-interactive mode, accept defaults
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Interactive installation
|
||||
./docker-install.sh
|
||||
|
||||
# Unattended with custom settings
|
||||
./docker-install.sh --path /srv/bambuddy --port 3000 --tz Europe/Berlin --yes
|
||||
|
||||
# Build from source
|
||||
./docker-install.sh --build --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
All scripts support these configuration options:
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| Install Path | Where BamBuddy is installed | `/opt/bambuddy` (Linux/Docker) |
|
||||
| Port | HTTP port for web interface | `8000` |
|
||||
| Timezone | Server timezone | System timezone or `UTC` |
|
||||
| Data Directory | Database and archives | `INSTALL_PATH/data` |
|
||||
| Log Directory | Application logs | `INSTALL_PATH/logs` |
|
||||
| Debug Mode | Enable verbose logging | `false` |
|
||||
| Log Level | INFO, WARNING, ERROR, DEBUG | `INFO` |
|
||||
|
||||
---
|
||||
|
||||
## Post-Installation
|
||||
|
||||
### Accessing BamBuddy
|
||||
|
||||
After installation, open your browser to:
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
Or use the port you specified during installation.
|
||||
|
||||
### Service Management
|
||||
|
||||
**Linux (systemd):**
|
||||
```bash
|
||||
sudo systemctl status bambuddy # Check status
|
||||
sudo systemctl start bambuddy # Start
|
||||
sudo systemctl stop bambuddy # Stop
|
||||
sudo systemctl restart bambuddy # Restart
|
||||
sudo journalctl -u bambuddy -f # View logs
|
||||
```
|
||||
|
||||
**macOS (launchd):**
|
||||
```bash
|
||||
launchctl list | grep bambuddy # Check status
|
||||
launchctl load ~/Library/LaunchAgents/com.bambuddy.app.plist # Start
|
||||
launchctl unload ~/Library/LaunchAgents/com.bambuddy.app.plist # Stop
|
||||
```
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker compose ps # Check status
|
||||
docker compose up -d # Start
|
||||
docker compose down # Stop
|
||||
docker compose restart # Restart
|
||||
docker compose logs -f # View logs
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
**Native installation:**
|
||||
```bash
|
||||
cd /opt/bambuddy
|
||||
git pull
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cd frontend && npm ci && npm run build
|
||||
sudo systemctl restart bambuddy # Linux
|
||||
```
|
||||
|
||||
**Docker (pre-built image):**
|
||||
```bash
|
||||
cd ~/bambuddy
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Docker (from source):**
|
||||
```bash
|
||||
cd ~/bambuddy
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Denied (Linux)
|
||||
Run with `sudo` or ensure your user has appropriate permissions:
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
### Docker: Printer Discovery Not Working
|
||||
Docker Desktop for macOS doesn't support host networking. Add printers manually by IP address in the BamBuddy web interface.
|
||||
|
||||
### Service Won't Start
|
||||
Check logs for errors:
|
||||
```bash
|
||||
# Linux
|
||||
sudo journalctl -u bambuddy -n 50
|
||||
|
||||
# Docker
|
||||
docker compose logs bambuddy
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
Choose a different port during installation or stop the conflicting service:
|
||||
```bash
|
||||
# Find what's using port 8000
|
||||
sudo lsof -i :8000 # Linux/macOS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Native Installation
|
||||
- Python 3.10+ (automatically installed if missing)
|
||||
- Node.js 18+ (automatically installed if missing)
|
||||
- Git (automatically installed if missing)
|
||||
- ~500MB disk space
|
||||
|
||||
### Docker Installation
|
||||
- Docker Engine 20+ or Docker Desktop
|
||||
- ~1GB disk space (includes image)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation:** https://wiki.bambuddy.cool
|
||||
- **Discord:** https://discord.gg/aFS3ZfScHM
|
||||
- **Issues:** https://github.com/maziggy/bambuddy/issues
|
||||
Executable
+541
@@ -0,0 +1,541 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# BamBuddy Docker Installation Script
|
||||
# Supports: Linux (all distros), macOS
|
||||
#
|
||||
# Usage:
|
||||
# Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/docker-install.sh | bash
|
||||
# Unattended: ./docker-install.sh --path /opt/bambuddy --port 8000 --yes
|
||||
#
|
||||
# Options:
|
||||
# --path PATH Installation directory (default: /opt/bambuddy)
|
||||
# --port PORT Port to expose (default: 8000)
|
||||
# --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)
|
||||
# --tz TIMEZONE Timezone (default: system timezone or UTC)
|
||||
# --build Build from source instead of using pre-built image
|
||||
# --yes, -y Non-interactive mode, accept defaults
|
||||
# --help, -h Show this help message
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
|
||||
# Default values
|
||||
DEFAULT_INSTALL_PATH="/opt/bambuddy"
|
||||
DEFAULT_PORT="8000"
|
||||
DEFAULT_BIND_ADDRESS="0.0.0.0"
|
||||
|
||||
# Script variables
|
||||
INSTALL_PATH=""
|
||||
PORT=""
|
||||
BIND_ADDRESS=""
|
||||
TIMEZONE=""
|
||||
BUILD_FROM_SOURCE="false"
|
||||
NON_INTERACTIVE="false"
|
||||
OS_TYPE=""
|
||||
DOCKER_CMD=""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
print_banner() {
|
||||
echo -e "${CYAN}"
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ ____ _ _ _ ║"
|
||||
echo "║ | __ ) __ _ _ __ ___ | |__ _ _ __| | __| |_ _ ║"
|
||||
echo "║ | _ \\ / _\` | '_ \` _ \\| '_ \\| | | |/ _\` |/ _\` | | | | ║"
|
||||
echo "║ | |_) | (_| | | | | | | |_) | |_| | (_| | (_| | |_| | ║"
|
||||
echo "║ |____/ \\__,_|_| |_| |_|_.__/ \\__,_|\\__,_|\\__,_|\\__, | ║"
|
||||
echo "║ |___/ ║"
|
||||
echo "║ ║"
|
||||
echo "║ Docker Installation Script ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
prompt() {
|
||||
local prompt_text="$1"
|
||||
local default_value="$2"
|
||||
local var_name="$3"
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
eval "$var_name=\"$default_value\""
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "$default_value" ]]; then
|
||||
echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
|
||||
else
|
||||
echo -en "${BOLD}$prompt_text${NC}: "
|
||||
fi
|
||||
|
||||
read -r input
|
||||
if [[ -z "$input" ]]; then
|
||||
eval "$var_name=\"$default_value\""
|
||||
else
|
||||
eval "$var_name=\"$input\""
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_yes_no() {
|
||||
local prompt_text="$1"
|
||||
local default="$2" # y or n
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
[[ "$default" == "y" ]] && return 0 || return 1
|
||||
fi
|
||||
|
||||
local yn_hint="[y/n]"
|
||||
[[ "$default" == "y" ]] && yn_hint="[Y/n]"
|
||||
[[ "$default" == "n" ]] && yn_hint="[y/N]"
|
||||
|
||||
while true; do
|
||||
echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
|
||||
read -r yn
|
||||
[[ -z "$yn" ]] && yn="$default"
|
||||
case "$yn" in
|
||||
[Yy]* ) return 0;;
|
||||
[Nn]* ) return 1;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_help() {
|
||||
echo "BamBuddy Docker Installation Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --path PATH Installation directory (default: /opt/bambuddy)"
|
||||
echo " --port PORT Port to expose (default: 8000)"
|
||||
echo " --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)"
|
||||
echo " --tz TIMEZONE Timezone (default: system timezone or UTC)"
|
||||
echo " --build Build from source instead of using pre-built image"
|
||||
echo " --yes, -y Non-interactive mode, accept defaults"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " Interactive installation:"
|
||||
echo " ./docker-install.sh"
|
||||
echo ""
|
||||
echo " Unattended installation with custom settings:"
|
||||
echo " ./docker-install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes"
|
||||
echo ""
|
||||
echo " Build from source:"
|
||||
echo " ./docker-install.sh --build --yes"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# System Detection
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
detect_os() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
OS_TYPE="macos"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
OS_TYPE="linux"
|
||||
else
|
||||
log_error "Cannot detect operating system"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_docker() {
|
||||
# Check for docker compose (v2) or docker-compose (v1)
|
||||
if docker compose version &>/dev/null 2>&1; then
|
||||
DOCKER_CMD="docker compose"
|
||||
log_success "Found Docker Compose v2"
|
||||
return 0
|
||||
elif docker-compose --version &>/dev/null 2>&1; then
|
||||
DOCKER_CMD="docker-compose"
|
||||
log_success "Found Docker Compose v1"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_timezone() {
|
||||
if [[ -n "$TIMEZONE" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to get system timezone (with error handling for set -e)
|
||||
TIMEZONE=""
|
||||
if [[ -f /etc/timezone ]]; then
|
||||
TIMEZONE=$(cat /etc/timezone 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
if [[ -z "$TIMEZONE" ]] && [[ -L /etc/localtime ]]; then
|
||||
TIMEZONE=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||') || true
|
||||
fi
|
||||
|
||||
if [[ -z "$TIMEZONE" ]] && command -v timedatectl &>/dev/null; then
|
||||
TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
# Default to UTC if not found (use if/then to avoid set -e issue with &&)
|
||||
if [[ -z "$TIMEZONE" ]]; then
|
||||
TIMEZONE="UTC"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Installation Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
install_docker() {
|
||||
log_info "Docker not found, installing..."
|
||||
|
||||
case "$OS_TYPE" in
|
||||
linux)
|
||||
# Use Docker's convenience script
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# Add current user to docker group
|
||||
if [[ -n "$SUDO_USER" ]]; then
|
||||
sudo usermod -aG docker "$SUDO_USER"
|
||||
log_warn "Added $SUDO_USER to docker group. You may need to log out and back in."
|
||||
else
|
||||
sudo usermod -aG docker "$USER"
|
||||
log_warn "Added $USER to docker group. You may need to log out and back in."
|
||||
fi
|
||||
|
||||
# Start Docker service
|
||||
sudo systemctl enable docker
|
||||
sudo systemctl start docker
|
||||
;;
|
||||
macos)
|
||||
log_error "Docker Desktop not found."
|
||||
log_error "Please install Docker Desktop for Mac from: https://www.docker.com/products/docker-desktop"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
log_success "Docker installed"
|
||||
}
|
||||
|
||||
create_install_dir() {
|
||||
log_info "Creating installation directory..."
|
||||
|
||||
mkdir -p "$INSTALL_PATH"
|
||||
cd "$INSTALL_PATH"
|
||||
|
||||
log_success "Directory created: $INSTALL_PATH"
|
||||
}
|
||||
|
||||
download_compose_file() {
|
||||
log_info "Downloading docker-compose.yml..."
|
||||
|
||||
if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
|
||||
# Clone the full repo for building
|
||||
if [[ -d ".git" ]]; then
|
||||
log_info "Existing repository found, updating..."
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
else
|
||||
git clone https://github.com/maziggy/bambuddy.git .
|
||||
fi
|
||||
else
|
||||
# Just download the compose file
|
||||
curl -fsSL -o docker-compose.yml \
|
||||
https://raw.githubusercontent.com/maziggy/bambuddy/main/docker-compose.yml
|
||||
fi
|
||||
|
||||
log_success "docker-compose.yml ready"
|
||||
}
|
||||
|
||||
create_env_file() {
|
||||
log_info "Creating environment configuration..."
|
||||
|
||||
cat > .env << EOF
|
||||
# BamBuddy Docker Configuration
|
||||
# Generated by docker-install.sh on $(date)
|
||||
|
||||
# Port BamBuddy runs on
|
||||
PORT=$PORT
|
||||
|
||||
# Timezone
|
||||
TZ=$TIMEZONE
|
||||
EOF
|
||||
|
||||
log_success "Environment file created"
|
||||
}
|
||||
|
||||
customize_compose() {
|
||||
# Detect if we need to disable host networking (macOS/Windows in Docker Desktop)
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
log_warn "Docker Desktop detected. Host networking is not supported."
|
||||
log_info "Modifying docker-compose.yml for port mapping..."
|
||||
|
||||
# Create a modified compose file for macOS
|
||||
if [[ -f docker-compose.yml ]]; then
|
||||
# Comment out network_mode: host and uncomment ports section
|
||||
sed -i.bak \
|
||||
-e 's/^[[:space:]]*network_mode: host/# network_mode: host/' \
|
||||
-e 's/^[[:space:]]*#ports:/ ports:/' \
|
||||
-e 's/^[[:space:]]*#[[:space:]]*- "\${PORT:-8000}:8000"/ - "\${PORT:-8000}:8000"/' \
|
||||
docker-compose.yml
|
||||
|
||||
log_warn "Printer discovery may not work. Add printers manually by IP address."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_container() {
|
||||
log_info "Starting BamBuddy..."
|
||||
|
||||
if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
|
||||
$DOCKER_CMD up -d --build
|
||||
else
|
||||
$DOCKER_CMD up -d
|
||||
fi
|
||||
|
||||
# Wait for container to start
|
||||
log_info "Waiting for container to start..."
|
||||
local max_attempts=15
|
||||
local attempt=0
|
||||
|
||||
while [[ $attempt -lt $max_attempts ]]; do
|
||||
# Check if container is running (Up)
|
||||
if $DOCKER_CMD ps | grep -q "Up"; then
|
||||
log_success "BamBuddy container is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if container failed
|
||||
if $DOCKER_CMD ps -a | grep -q "Exited"; then
|
||||
log_error "Container failed to start"
|
||||
log_info "Check logs with: $DOCKER_CMD logs bambuddy"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
log_warn "Container may still be starting. Check with: $DOCKER_CMD ps"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main Installation Flow
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--path)
|
||||
INSTALL_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bind)
|
||||
BIND_ADDRESS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--tz)
|
||||
TIMEZONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--build)
|
||||
BUILD_FROM_SOURCE="true"
|
||||
shift
|
||||
;;
|
||||
--yes|-y)
|
||||
NON_INTERACTIVE="true"
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
gather_config() {
|
||||
echo ""
|
||||
echo -e "${BOLD}Installation Configuration${NC}"
|
||||
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
|
||||
# Installation path
|
||||
[[ -z "$INSTALL_PATH" ]] && prompt "Installation directory" "$DEFAULT_INSTALL_PATH" INSTALL_PATH
|
||||
|
||||
# Port
|
||||
[[ -z "$PORT" ]] && prompt "Port to expose" "$DEFAULT_PORT" PORT
|
||||
|
||||
# Bind address
|
||||
if [[ -z "$BIND_ADDRESS" ]]; then
|
||||
echo ""
|
||||
echo "Network access:"
|
||||
echo " 0.0.0.0 - Accessible from other devices on your network (recommended)"
|
||||
echo " 127.0.0.1 - Only accessible from this machine"
|
||||
prompt "Bind address" "$DEFAULT_BIND_ADDRESS" BIND_ADDRESS
|
||||
fi
|
||||
|
||||
# Timezone
|
||||
detect_timezone
|
||||
prompt "Timezone" "$TIMEZONE" TIMEZONE
|
||||
|
||||
# Build from source?
|
||||
if [[ "$BUILD_FROM_SOURCE" != "true" ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||
if prompt_yes_no "Build from source? (No = use pre-built image)" "n"; then
|
||||
BUILD_FROM_SOURCE="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Confirm
|
||||
echo ""
|
||||
echo -e "${BOLD}Installation Summary${NC}"
|
||||
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
||||
echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
|
||||
echo -e " Port: ${GREEN}$PORT${NC}"
|
||||
echo -e " Bind address: ${GREEN}$BIND_ADDRESS${NC}"
|
||||
echo -e " Timezone: ${GREEN}$TIMEZONE${NC}"
|
||||
echo -e " Build source: ${GREEN}$BUILD_FROM_SOURCE${NC}"
|
||||
echo ""
|
||||
|
||||
if ! prompt_yes_no "Proceed with installation?" "y"; then
|
||||
echo "Installation cancelled."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
print_banner
|
||||
|
||||
# Check if running via pipe (curl | bash) - interactive mode won't work
|
||||
if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||
log_error "Interactive mode requires a terminal."
|
||||
log_info "When using 'curl | bash', you must use non-interactive mode:"
|
||||
echo ""
|
||||
echo " curl -fsSL URL | bash -s -- --yes"
|
||||
echo ""
|
||||
log_info "Or download and run directly:"
|
||||
echo ""
|
||||
echo " curl -fsSL URL -o docker-install.sh && chmod +x docker-install.sh && ./docker-install.sh"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect system
|
||||
log_info "Detecting system..."
|
||||
detect_os
|
||||
log_success "Detected: $OS_TYPE"
|
||||
|
||||
# Check for Docker
|
||||
if ! command -v docker &>/dev/null; then
|
||||
install_docker
|
||||
fi
|
||||
|
||||
if ! detect_docker; then
|
||||
log_error "Docker Compose not found. Please install Docker Compose."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker daemon is running
|
||||
if ! docker info &>/dev/null; then
|
||||
log_error "Docker daemon is not running. Please start Docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gather configuration
|
||||
gather_config
|
||||
|
||||
# Install steps
|
||||
echo ""
|
||||
echo -e "${BOLD}Starting Installation${NC}"
|
||||
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
|
||||
create_install_dir
|
||||
download_compose_file
|
||||
create_env_file
|
||||
customize_compose
|
||||
start_container
|
||||
|
||||
# Done!
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ ║${NC}"
|
||||
echo -e "${GREEN}║ Installation Complete! ║${NC}"
|
||||
echo -e "${GREEN}║ ║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
# Show appropriate URL based on bind address
|
||||
if [[ "$BIND_ADDRESS" == "0.0.0.0" ]]; then
|
||||
local ip_addr
|
||||
ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
|
||||
echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
|
||||
echo -e " ${CYAN}http://$ip_addr:$PORT${NC} (from other devices)"
|
||||
else
|
||||
echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BOLD}Manage container:${NC}"
|
||||
echo -e " Status: cd $INSTALL_PATH && $DOCKER_CMD ps"
|
||||
echo -e " Logs: cd $INSTALL_PATH && $DOCKER_CMD logs -f bambuddy"
|
||||
echo -e " Stop: cd $INSTALL_PATH && $DOCKER_CMD down"
|
||||
echo -e " Start: cd $INSTALL_PATH && $DOCKER_CMD up -d"
|
||||
echo -e " Restart: cd $INSTALL_PATH && $DOCKER_CMD restart"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Update BamBuddy:${NC}"
|
||||
if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
|
||||
echo -e " cd $INSTALL_PATH && git pull && $DOCKER_CMD up -d --build"
|
||||
else
|
||||
echo -e " cd $INSTALL_PATH && $DOCKER_CMD pull && $DOCKER_CMD up -d"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BOLD}Data location:${NC} Docker volumes (bambuddy_data, bambuddy_logs)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
echo -e " ${YELLOW}Note:${NC} Printer discovery may not work with Docker Desktop."
|
||||
echo -e " Add printers manually using their IP address."
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+883
@@ -0,0 +1,883 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# BamBuddy Native Installation Script
|
||||
# Supports: Debian/Ubuntu, RHEL/Fedora/CentOS, Arch Linux, macOS
|
||||
#
|
||||
# Usage:
|
||||
# Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/install.sh | bash
|
||||
# Unattended: ./install.sh --path /opt/bambuddy --port 8000 --yes
|
||||
#
|
||||
# Options:
|
||||
# --path PATH Installation directory (default: /opt/bambuddy)
|
||||
# --port PORT Port to listen on (default: 8000)
|
||||
# --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)
|
||||
# --tz TIMEZONE Timezone (default: system timezone or UTC)
|
||||
# --data-dir PATH Data directory (default: INSTALL_PATH/data)
|
||||
# --log-dir PATH Log directory (default: INSTALL_PATH/logs)
|
||||
# --debug Enable debug mode
|
||||
# --log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
|
||||
# --no-service Skip systemd service setup (Linux only)
|
||||
# --set-system-tz Set system timezone to match (for unattended installs)
|
||||
# --yes, -y Non-interactive mode, accept defaults
|
||||
# --help, -h Show this help message
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
|
||||
# Default values
|
||||
DEFAULT_INSTALL_PATH="/opt/bambuddy"
|
||||
DEFAULT_PORT="8000"
|
||||
DEFAULT_BIND_ADDRESS="0.0.0.0"
|
||||
DEFAULT_LOG_LEVEL="INFO"
|
||||
DEFAULT_DEBUG="false"
|
||||
|
||||
# Script variables
|
||||
INSTALL_PATH=""
|
||||
PORT=""
|
||||
BIND_ADDRESS=""
|
||||
TIMEZONE=""
|
||||
DATA_DIR=""
|
||||
LOG_DIR=""
|
||||
DEBUG_MODE=""
|
||||
LOG_LEVEL=""
|
||||
SKIP_SERVICE="false"
|
||||
SET_SYSTEM_TZ=""
|
||||
NON_INTERACTIVE="false"
|
||||
OS_TYPE=""
|
||||
PKG_MANAGER=""
|
||||
PYTHON_CMD=""
|
||||
SERVICE_USER="bambuddy"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
print_banner() {
|
||||
echo -e "${CYAN}"
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ ____ _ _ _ ║"
|
||||
echo "║ | __ ) __ _ _ __ ___ | |__ _ _ __| | __| |_ _ ║"
|
||||
echo "║ | _ \\ / _\` | '_ \` _ \\| '_ \\| | | |/ _\` |/ _\` | | | | ║"
|
||||
echo "║ | |_) | (_| | | | | | | |_) | |_| | (_| | (_| | |_| | ║"
|
||||
echo "║ |____/ \\__,_|_| |_| |_|_.__/ \\__,_|\\__,_|\\__,_|\\__, | ║"
|
||||
echo "║ |___/ ║"
|
||||
echo "║ ║"
|
||||
echo "║ Native Installation Script ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
prompt() {
|
||||
local prompt_text="$1"
|
||||
local default_value="$2"
|
||||
local var_name="$3"
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
eval "$var_name=\"$default_value\""
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "$default_value" ]]; then
|
||||
echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
|
||||
else
|
||||
echo -en "${BOLD}$prompt_text${NC}: "
|
||||
fi
|
||||
|
||||
read -r input
|
||||
if [[ -z "$input" ]]; then
|
||||
eval "$var_name=\"$default_value\""
|
||||
else
|
||||
eval "$var_name=\"$input\""
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_yes_no() {
|
||||
local prompt_text="$1"
|
||||
local default="$2" # y or n
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
[[ "$default" == "y" ]] && return 0 || return 1
|
||||
fi
|
||||
|
||||
local yn_hint="[y/n]"
|
||||
[[ "$default" == "y" ]] && yn_hint="[Y/n]"
|
||||
[[ "$default" == "n" ]] && yn_hint="[y/N]"
|
||||
|
||||
while true; do
|
||||
echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
|
||||
read -r yn
|
||||
[[ -z "$yn" ]] && yn="$default"
|
||||
case "$yn" in
|
||||
[Yy]* ) return 0;;
|
||||
[Nn]* ) return 1;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_help() {
|
||||
echo "BamBuddy Native Installation Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --path PATH Installation directory (default: /opt/bambuddy)"
|
||||
echo " --port PORT Port to listen on (default: 8000)"
|
||||
echo " --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)"
|
||||
echo " --tz TIMEZONE Timezone (default: system timezone or UTC)"
|
||||
echo " --data-dir PATH Data directory (default: INSTALL_PATH/data)"
|
||||
echo " --log-dir PATH Log directory (default: INSTALL_PATH/logs)"
|
||||
echo " --debug Enable debug mode"
|
||||
echo " --log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)"
|
||||
echo " --no-service Skip systemd service setup (Linux only)"
|
||||
echo " --set-system-tz Set system timezone to match (for unattended installs)"
|
||||
echo " --yes, -y Non-interactive mode, accept defaults"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " Interactive installation:"
|
||||
echo " ./install.sh"
|
||||
echo ""
|
||||
echo " Unattended installation with custom settings:"
|
||||
echo " ./install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes"
|
||||
echo ""
|
||||
echo " Minimal unattended installation:"
|
||||
echo " ./install.sh -y"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# System Detection
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
detect_os() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
OS_TYPE="macos"
|
||||
PKG_MANAGER="brew"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|debian|raspbian|linuxmint|pop)
|
||||
OS_TYPE="debian"
|
||||
PKG_MANAGER="apt"
|
||||
;;
|
||||
fedora|rhel|centos|rocky|almalinux|ol)
|
||||
OS_TYPE="rhel"
|
||||
if command -v dnf &>/dev/null; then
|
||||
PKG_MANAGER="dnf"
|
||||
else
|
||||
PKG_MANAGER="yum"
|
||||
fi
|
||||
;;
|
||||
arch|manjaro|endeavouros)
|
||||
OS_TYPE="arch"
|
||||
PKG_MANAGER="pacman"
|
||||
;;
|
||||
opensuse*|sles)
|
||||
OS_TYPE="suse"
|
||||
PKG_MANAGER="zypper"
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported Linux distribution: $ID"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
log_error "Cannot detect operating system"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_python() {
|
||||
# Try python3 first, then python
|
||||
if command -v python3 &>/dev/null; then
|
||||
PYTHON_CMD="python3"
|
||||
elif command -v python &>/dev/null; then
|
||||
local version
|
||||
version=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1)
|
||||
if [[ "$version" -ge 3 ]]; then
|
||||
PYTHON_CMD="python"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$PYTHON_CMD" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check version >= 3.10
|
||||
local version
|
||||
version=$($PYTHON_CMD -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
local major minor
|
||||
major=$(echo "$version" | cut -d'.' -f1)
|
||||
minor=$(echo "$version" | cut -d'.' -f2)
|
||||
|
||||
if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then
|
||||
log_warn "Python $version found, but 3.10+ is required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "Found Python $version"
|
||||
return 0
|
||||
}
|
||||
|
||||
detect_timezone() {
|
||||
if [[ -n "$TIMEZONE" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to get system timezone (with error handling for set -e)
|
||||
TIMEZONE=""
|
||||
if [[ -f /etc/timezone ]]; then
|
||||
TIMEZONE=$(cat /etc/timezone 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
if [[ -z "$TIMEZONE" ]] && [[ -L /etc/localtime ]]; then
|
||||
TIMEZONE=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||') || true
|
||||
fi
|
||||
|
||||
if [[ -z "$TIMEZONE" ]] && command -v timedatectl &>/dev/null; then
|
||||
TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
# Default to UTC if not found (use if/then to avoid set -e issue with &&)
|
||||
if [[ -z "$TIMEZONE" ]]; then
|
||||
TIMEZONE="UTC"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Package Installation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
install_dependencies() {
|
||||
log_info "Installing system dependencies..."
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
apt)
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-pip python3-venv git curl ffmpeg
|
||||
;;
|
||||
dnf|yum)
|
||||
sudo $PKG_MANAGER install -y python3 python3-pip git curl ffmpeg
|
||||
;;
|
||||
pacman)
|
||||
sudo pacman -Sy --noconfirm python python-pip git curl ffmpeg
|
||||
;;
|
||||
zypper)
|
||||
sudo zypper install -y python3 python3-pip git curl ffmpeg
|
||||
;;
|
||||
brew)
|
||||
# Check if Homebrew is installed
|
||||
if ! command -v brew &>/dev/null; then
|
||||
log_error "Homebrew not found. Please install it first: https://brew.sh"
|
||||
exit 1
|
||||
fi
|
||||
brew install python git curl ffmpeg
|
||||
;;
|
||||
esac
|
||||
|
||||
log_success "System dependencies installed"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Installation Steps
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
create_user() {
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
return # Skip user creation on macOS
|
||||
fi
|
||||
|
||||
if id "$SERVICE_USER" &>/dev/null; then
|
||||
log_info "User '$SERVICE_USER' already exists"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Creating service user '$SERVICE_USER'..."
|
||||
sudo useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SERVICE_USER"
|
||||
log_success "Service user created"
|
||||
}
|
||||
|
||||
download_bambuddy() {
|
||||
log_info "Downloading BamBuddy..."
|
||||
|
||||
if [[ -d "$INSTALL_PATH/.git" ]]; then
|
||||
log_info "Existing installation found, updating..."
|
||||
# Add safe.directory to avoid "dubious ownership" error when running as root
|
||||
git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
|
||||
cd "$INSTALL_PATH"
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
# Ensure correct ownership after update
|
||||
sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true
|
||||
else
|
||||
sudo mkdir -p "$INSTALL_PATH"
|
||||
sudo chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true
|
||||
git clone https://github.com/maziggy/bambuddy.git "$INSTALL_PATH"
|
||||
sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_success "BamBuddy downloaded to $INSTALL_PATH"
|
||||
}
|
||||
|
||||
setup_virtualenv() {
|
||||
log_info "Setting up Python virtual environment..."
|
||||
|
||||
cd "$INSTALL_PATH"
|
||||
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
$PYTHON_CMD -m venv venv
|
||||
source venv/bin/activate
|
||||
else
|
||||
sudo -u "$SERVICE_USER" $PYTHON_CMD -m venv venv 2>/dev/null || $PYTHON_CMD -m venv venv
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
log_success "Virtual environment configured"
|
||||
}
|
||||
|
||||
check_node_version() {
|
||||
# Returns 0 if Node.js 20+ is available, 1 otherwise
|
||||
if ! command -v node &>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local version
|
||||
version=$(node --version 2>/dev/null | sed 's/^v//')
|
||||
local major
|
||||
major=$(echo "$version" | cut -d'.' -f1)
|
||||
|
||||
if [[ "$major" -ge 20 ]]; then
|
||||
log_success "Found Node.js v$version"
|
||||
return 0
|
||||
else
|
||||
log_warn "Found Node.js v$version (need 20+)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_nodejs() {
|
||||
log_info "Installing Node.js 22..."
|
||||
case "$PKG_MANAGER" in
|
||||
apt)
|
||||
# Remove old nodejs if present
|
||||
sudo apt-get remove -y nodejs npm 2>/dev/null || true
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
;;
|
||||
dnf|yum)
|
||||
sudo $PKG_MANAGER remove -y nodejs npm 2>/dev/null || true
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo $PKG_MANAGER install -y nodejs
|
||||
;;
|
||||
pacman)
|
||||
sudo pacman -S --noconfirm nodejs npm
|
||||
;;
|
||||
zypper)
|
||||
sudo zypper install -y nodejs22
|
||||
;;
|
||||
brew)
|
||||
brew install node@22
|
||||
brew link --overwrite node@22
|
||||
;;
|
||||
*)
|
||||
log_error "Please install Node.js 20+ manually: https://nodejs.org/"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
# Refresh PATH
|
||||
hash -r 2>/dev/null || true
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
log_info "Building frontend..."
|
||||
|
||||
cd "$INSTALL_PATH/frontend"
|
||||
|
||||
# Check for Node.js 20+
|
||||
if ! check_node_version; then
|
||||
install_nodejs
|
||||
# Verify installation
|
||||
if ! check_node_version; then
|
||||
log_error "Failed to install Node.js 20+. Please install manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
log_success "Frontend built"
|
||||
}
|
||||
|
||||
create_directories() {
|
||||
log_info "Creating data directories..."
|
||||
|
||||
sudo mkdir -p "$DATA_DIR" "$LOG_DIR"
|
||||
|
||||
if [[ "$OS_TYPE" != "macos" ]]; then
|
||||
sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" "$LOG_DIR"
|
||||
fi
|
||||
|
||||
log_success "Directories created"
|
||||
}
|
||||
|
||||
create_env_file() {
|
||||
log_info "Creating environment configuration..."
|
||||
|
||||
local env_file="$INSTALL_PATH/.env"
|
||||
|
||||
# Note: Only include settings recognized by the app's pydantic Settings class
|
||||
# Other settings (PORT, BIND_ADDRESS, DATA_DIR, LOG_DIR, TZ) are set in systemd service
|
||||
cat > /tmp/bambuddy.env << EOF
|
||||
# BamBuddy Configuration
|
||||
# Generated by install.sh on $(date)
|
||||
|
||||
# Debug mode (true = verbose logging)
|
||||
DEBUG=$DEBUG_MODE
|
||||
|
||||
# Log level (only used when DEBUG=false)
|
||||
# Options: DEBUG, INFO, WARNING, ERROR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
|
||||
# Enable file logging
|
||||
LOG_TO_FILE=true
|
||||
EOF
|
||||
|
||||
sudo mv /tmp/bambuddy.env "$env_file"
|
||||
if [[ "$OS_TYPE" != "macos" ]]; then
|
||||
sudo chown "$SERVICE_USER:$SERVICE_USER" "$env_file"
|
||||
fi
|
||||
sudo chmod 600 "$env_file"
|
||||
|
||||
log_success "Environment file created at $env_file"
|
||||
}
|
||||
|
||||
create_systemd_service() {
|
||||
if [[ "$OS_TYPE" == "macos" ]] || [[ "$SKIP_SERVICE" == "true" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Creating systemd service..."
|
||||
|
||||
cat > /tmp/bambuddy.service << EOF
|
||||
[Unit]
|
||||
Description=BamBuddy - Bambu Lab Print Management
|
||||
Documentation=https://github.com/maziggy/bambuddy
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$SERVICE_USER
|
||||
Group=$SERVICE_USER
|
||||
WorkingDirectory=$INSTALL_PATH
|
||||
|
||||
# App settings from .env file
|
||||
EnvironmentFile=$INSTALL_PATH/.env
|
||||
|
||||
# Service settings (not in .env to avoid pydantic validation errors)
|
||||
Environment="DATA_DIR=$DATA_DIR"
|
||||
Environment="LOG_DIR=$LOG_DIR"
|
||||
Environment="TZ=$TIMEZONE"
|
||||
|
||||
ExecStart=$INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host $BIND_ADDRESS --port $PORT
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=$DATA_DIR $LOG_DIR $INSTALL_PATH
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo mv /tmp/bambuddy.service /etc/systemd/system/bambuddy.service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
log_success "Systemd service created"
|
||||
|
||||
if prompt_yes_no "Enable BamBuddy to start on boot?" "y"; then
|
||||
sudo systemctl enable bambuddy
|
||||
log_success "Service enabled"
|
||||
fi
|
||||
|
||||
if prompt_yes_no "Start BamBuddy now?" "y"; then
|
||||
sudo systemctl start bambuddy
|
||||
sleep 2
|
||||
if sudo systemctl is-active --quiet bambuddy; then
|
||||
log_success "BamBuddy is running"
|
||||
else
|
||||
log_warn "Service may have failed to start. Check: sudo journalctl -u bambuddy -f"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
create_launchd_service() {
|
||||
if [[ "$OS_TYPE" != "macos" ]] || [[ "$SKIP_SERVICE" == "true" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Creating launchd service..."
|
||||
|
||||
local plist_path="$HOME/Library/LaunchAgents/com.bambuddy.app.plist"
|
||||
|
||||
cat > "$plist_path" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.bambuddy.app</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$INSTALL_PATH/venv/bin/uvicorn</string>
|
||||
<string>backend.app.main:app</string>
|
||||
<string>--host</string>
|
||||
<string>$BIND_ADDRESS</string>
|
||||
<string>--port</string>
|
||||
<string>$PORT</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$INSTALL_PATH</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>DEBUG</key>
|
||||
<string>$DEBUG_MODE</string>
|
||||
<key>LOG_LEVEL</key>
|
||||
<string>$LOG_LEVEL</string>
|
||||
<key>DATA_DIR</key>
|
||||
<string>$DATA_DIR</string>
|
||||
<key>LOG_DIR</key>
|
||||
<string>$LOG_DIR</string>
|
||||
<key>TZ</key>
|
||||
<string>$TIMEZONE</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>$LOG_DIR/bambuddy.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>$LOG_DIR/bambuddy.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
log_success "Launchd plist created at $plist_path"
|
||||
|
||||
if prompt_yes_no "Load BamBuddy service now?" "y"; then
|
||||
launchctl load "$plist_path"
|
||||
sleep 2
|
||||
if launchctl list | grep -q "com.bambuddy.app"; then
|
||||
log_success "BamBuddy is running"
|
||||
else
|
||||
log_warn "Service may have failed to start. Check: cat $LOG_DIR/bambuddy.error.log"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main Installation Flow
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--path)
|
||||
INSTALL_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bind)
|
||||
BIND_ADDRESS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--tz)
|
||||
TIMEZONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--data-dir)
|
||||
DATA_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--log-dir)
|
||||
LOG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--debug)
|
||||
DEBUG_MODE="true"
|
||||
shift
|
||||
;;
|
||||
--log-level)
|
||||
LOG_LEVEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-service)
|
||||
SKIP_SERVICE="true"
|
||||
shift
|
||||
;;
|
||||
--set-system-tz)
|
||||
SET_SYSTEM_TZ="true"
|
||||
shift
|
||||
;;
|
||||
--yes|-y)
|
||||
NON_INTERACTIVE="true"
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
gather_config() {
|
||||
echo ""
|
||||
echo -e "${BOLD}Installation Configuration${NC}"
|
||||
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
|
||||
# Installation path
|
||||
[[ -z "$INSTALL_PATH" ]] && prompt "Installation directory" "$DEFAULT_INSTALL_PATH" INSTALL_PATH
|
||||
|
||||
# Port
|
||||
[[ -z "$PORT" ]] && prompt "Port to listen on" "$DEFAULT_PORT" PORT
|
||||
|
||||
# Bind address
|
||||
if [[ -z "$BIND_ADDRESS" ]]; then
|
||||
echo ""
|
||||
echo "Network access:"
|
||||
echo " 0.0.0.0 - Accessible from other devices on your network (recommended)"
|
||||
echo " 127.0.0.1 - Only accessible from this machine"
|
||||
prompt "Bind address" "$DEFAULT_BIND_ADDRESS" BIND_ADDRESS
|
||||
fi
|
||||
|
||||
# Timezone
|
||||
detect_timezone
|
||||
prompt "Timezone" "$TIMEZONE" TIMEZONE
|
||||
|
||||
# Offer to set system timezone if different from current (skip if already set via --set-system-tz)
|
||||
if [[ -z "$SET_SYSTEM_TZ" ]]; then
|
||||
local current_tz
|
||||
current_tz=$(timedatectl show --property=Timezone --value 2>/dev/null) || true
|
||||
if [[ -n "$TIMEZONE" ]] && [[ "$TIMEZONE" != "$current_tz" ]]; then
|
||||
# Default to "n" so unattended installs don't change system TZ unless --set-system-tz is used
|
||||
if prompt_yes_no "Set system timezone to $TIMEZONE?" "n"; then
|
||||
SET_SYSTEM_TZ="true"
|
||||
else
|
||||
SET_SYSTEM_TZ="false"
|
||||
fi
|
||||
else
|
||||
SET_SYSTEM_TZ="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Data directory
|
||||
[[ -z "$DATA_DIR" ]] && DATA_DIR="$INSTALL_PATH/data"
|
||||
prompt "Data directory" "$DATA_DIR" DATA_DIR
|
||||
|
||||
# Log directory
|
||||
[[ -z "$LOG_DIR" ]] && LOG_DIR="$INSTALL_PATH/logs"
|
||||
prompt "Log directory" "$LOG_DIR" LOG_DIR
|
||||
|
||||
# Debug mode
|
||||
if [[ -z "$DEBUG_MODE" ]]; then
|
||||
if prompt_yes_no "Enable debug mode?" "n"; then
|
||||
DEBUG_MODE="true"
|
||||
else
|
||||
DEBUG_MODE="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Log level
|
||||
if [[ -z "$LOG_LEVEL" ]]; then
|
||||
echo ""
|
||||
echo "Log levels: DEBUG, INFO, WARNING, ERROR"
|
||||
prompt "Log level" "$DEFAULT_LOG_LEVEL" LOG_LEVEL
|
||||
fi
|
||||
|
||||
# Confirm
|
||||
echo ""
|
||||
echo -e "${BOLD}Installation Summary${NC}"
|
||||
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
||||
echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
|
||||
echo -e " Port: ${GREEN}$PORT${NC}"
|
||||
echo -e " Bind address: ${GREEN}$BIND_ADDRESS${NC}"
|
||||
echo -e " Timezone: ${GREEN}$TIMEZONE${NC}"
|
||||
echo -e " Data dir: ${GREEN}$DATA_DIR${NC}"
|
||||
echo -e " Log dir: ${GREEN}$LOG_DIR${NC}"
|
||||
echo -e " Debug mode: ${GREEN}$DEBUG_MODE${NC}"
|
||||
echo -e " Log level: ${GREEN}$LOG_LEVEL${NC}"
|
||||
echo ""
|
||||
|
||||
if ! prompt_yes_no "Proceed with installation?" "y"; then
|
||||
echo "Installation cancelled."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
print_banner
|
||||
|
||||
# Check if running via pipe (curl | bash) - interactive mode won't work
|
||||
if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||
log_error "Interactive mode requires a terminal."
|
||||
log_info "When using 'curl | bash', you must use non-interactive mode:"
|
||||
echo ""
|
||||
echo " curl -fsSL URL | bash -s -- --yes"
|
||||
echo ""
|
||||
log_info "Or download and run directly:"
|
||||
echo ""
|
||||
echo " curl -fsSL URL -o install.sh && chmod +x install.sh && ./install.sh"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for root (we need sudo for some operations)
|
||||
if [[ "$EUID" -eq 0 ]] && [[ "$OS_TYPE" != "macos" ]]; then
|
||||
log_warn "Running as root. Consider using a regular user with sudo privileges."
|
||||
fi
|
||||
|
||||
# Detect system
|
||||
log_info "Detecting system..."
|
||||
detect_os
|
||||
log_success "Detected: $OS_TYPE (package manager: $PKG_MANAGER)"
|
||||
|
||||
# Check/install Python
|
||||
if ! detect_python; then
|
||||
log_info "Python 3.10+ not found, will install..."
|
||||
fi
|
||||
|
||||
# Gather configuration
|
||||
gather_config
|
||||
|
||||
# Install steps
|
||||
echo ""
|
||||
echo -e "${BOLD}Starting Installation${NC}"
|
||||
echo -e "${CYAN}─────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
|
||||
install_dependencies
|
||||
detect_python || { log_error "Failed to install Python"; exit 1; }
|
||||
|
||||
# Set system timezone if requested
|
||||
if [[ "$SET_SYSTEM_TZ" == "true" ]]; then
|
||||
log_info "Setting system timezone to $TIMEZONE..."
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
sudo systemsetup -settimezone "$TIMEZONE" 2>/dev/null || true
|
||||
else
|
||||
sudo timedatectl set-timezone "$TIMEZONE" 2>/dev/null || true
|
||||
fi
|
||||
log_success "System timezone set to $TIMEZONE"
|
||||
fi
|
||||
|
||||
if [[ "$OS_TYPE" != "macos" ]]; then
|
||||
create_user
|
||||
else
|
||||
SERVICE_USER="$USER"
|
||||
fi
|
||||
|
||||
download_bambuddy
|
||||
setup_virtualenv
|
||||
build_frontend
|
||||
create_directories
|
||||
create_env_file
|
||||
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
create_launchd_service
|
||||
else
|
||||
create_systemd_service
|
||||
fi
|
||||
|
||||
# Done!
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ ║${NC}"
|
||||
echo -e "${GREEN}║ Installation Complete! ║${NC}"
|
||||
echo -e "${GREEN}║ ║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
# Show appropriate URL based on bind address
|
||||
if [[ "$BIND_ADDRESS" == "0.0.0.0" ]]; then
|
||||
local ip_addr
|
||||
ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
|
||||
echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
|
||||
echo -e " ${CYAN}http://$ip_addr:$PORT${NC} (from other devices)"
|
||||
else
|
||||
echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
|
||||
fi
|
||||
echo ""
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
echo -e " ${BOLD}Manage service:${NC}"
|
||||
echo -e " Start: launchctl load ~/Library/LaunchAgents/com.bambuddy.app.plist"
|
||||
echo -e " Stop: launchctl unload ~/Library/LaunchAgents/com.bambuddy.app.plist"
|
||||
echo -e " Logs: tail -f $LOG_DIR/bambuddy.log"
|
||||
else
|
||||
echo -e " ${BOLD}Manage service:${NC}"
|
||||
echo -e " Status: sudo systemctl status bambuddy"
|
||||
echo -e " Start: sudo systemctl start bambuddy"
|
||||
echo -e " Stop: sudo systemctl stop bambuddy"
|
||||
echo -e " Logs: sudo journalctl -u bambuddy -f"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BOLD}Update BamBuddy:${NC}"
|
||||
echo -e " cd $INSTALL_PATH && git pull && source venv/bin/activate"
|
||||
echo -e " pip install -r requirements.txt && cd frontend && npm ci && npm run build"
|
||||
if [[ "$OS_TYPE" != "macos" ]]; then
|
||||
echo -e " sudo systemctl restart bambuddy"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user