CI / skip-ci-check (pull_request) Successful in 30s
CI / docker-ci (pull_request) Successful in 32s
CI / python-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 39s
CI / viewer-unit (pull_request) Successful in 2m21s
CI / admin-unit (pull_request) Successful in 2m43s
CI / e2e (pull_request) Successful in 3m22s
Stop shipping punimtag_password / admin defaults in install helpers and docs; generate secrets at install time; require ADMIN_PASSWORD from env (with test-only bootstrap in conftest).
518 lines
17 KiB
Bash
Executable File
518 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
# Comprehensive installation script for PunimTag Web
|
|
# Installs all system dependencies, Python packages, and frontend dependencies
|
|
|
|
set -e
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Project root directory
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$PROJECT_ROOT"
|
|
|
|
echo -e "${BLUE}╔══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BLUE}║ PunimTag Web - Comprehensive Installation Script ║${NC}"
|
|
echo -e "${BLUE}╚══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
# Function to check if command exists
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
# Generate a local-only secret (never commit the resulting .env)
|
|
gen_secret() {
|
|
if command_exists openssl; then
|
|
openssl rand -hex 16
|
|
else
|
|
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 32
|
|
fi
|
|
}
|
|
|
|
# Function to check Python version
|
|
check_python_version() {
|
|
if command_exists python3; then
|
|
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
|
PYTHON_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1)
|
|
PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
|
|
|
|
if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 12 ]); then
|
|
echo -e "${RED}❌ Python 3.12 or higher is required. Found: Python $PYTHON_VERSION${NC}"
|
|
return 1
|
|
else
|
|
echo -e "${GREEN}✅ Python $PYTHON_VERSION found${NC}"
|
|
return 0
|
|
fi
|
|
else
|
|
echo -e "${RED}❌ Python 3 is not installed${NC}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to check Node.js version
|
|
check_node_version() {
|
|
if command_exists node; then
|
|
NODE_VERSION=$(node -v | sed 's/v//')
|
|
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
|
|
|
|
if [ "$NODE_MAJOR" -lt 18 ]; then
|
|
echo -e "${RED}❌ Node.js 18 or higher is required. Found: Node.js $NODE_VERSION${NC}"
|
|
return 1
|
|
else
|
|
echo -e "${GREEN}✅ Node.js $NODE_VERSION found${NC}"
|
|
return 0
|
|
fi
|
|
else
|
|
echo -e "${RED}❌ Node.js is not installed${NC}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Detect OS
|
|
detect_os() {
|
|
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
|
if [ -f /etc/os-release ]; then
|
|
. /etc/os-release
|
|
OS=$ID
|
|
else
|
|
OS="linux"
|
|
fi
|
|
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
|
OS="macos"
|
|
else
|
|
OS="unknown"
|
|
fi
|
|
echo "$OS"
|
|
}
|
|
|
|
# Install system dependencies (Linux only)
|
|
install_system_dependencies() {
|
|
local os=$(detect_os)
|
|
|
|
if [[ "$os" != "ubuntu" && "$os" != "debian" ]]; then
|
|
echo -e "${YELLOW}⚠️ Automatic system dependency installation is only supported on Ubuntu/Debian${NC}"
|
|
echo -e "${YELLOW} Please install PostgreSQL and Redis manually for other systems${NC}"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${BLUE}📦 Installing system dependencies...${NC}"
|
|
|
|
# Update package list
|
|
sudo apt update
|
|
|
|
# Install PostgreSQL
|
|
if ! command_exists psql; then
|
|
echo -e "${BLUE} Installing PostgreSQL...${NC}"
|
|
sudo apt install -y postgresql postgresql-contrib
|
|
sudo systemctl start postgresql
|
|
sudo systemctl enable postgresql
|
|
echo -e "${GREEN} ✅ PostgreSQL installed${NC}"
|
|
else
|
|
echo -e "${GREEN} ✅ PostgreSQL already installed${NC}"
|
|
fi
|
|
|
|
# Install Redis
|
|
if ! command_exists redis-cli; then
|
|
echo -e "${BLUE} Installing Redis...${NC}"
|
|
sudo apt install -y redis-server
|
|
sudo systemctl start redis-server
|
|
sudo systemctl enable redis-server
|
|
echo -e "${GREEN} ✅ Redis installed${NC}"
|
|
else
|
|
echo -e "${GREEN} ✅ Redis already installed${NC}"
|
|
fi
|
|
|
|
# Install Python tkinter (required for native folder picker)
|
|
if ! python3 -c "import tkinter" 2>/dev/null; then
|
|
echo -e "${BLUE} Installing Python tkinter...${NC}"
|
|
sudo apt install -y python3-tk
|
|
echo -e "${GREEN} ✅ Python tkinter installed${NC}"
|
|
else
|
|
echo -e "${GREEN} ✅ Python tkinter already installed${NC}"
|
|
fi
|
|
|
|
# Verify Redis is running
|
|
if redis-cli ping >/dev/null 2>&1; then
|
|
echo -e "${GREEN} ✅ Redis is running${NC}"
|
|
else
|
|
echo -e "${YELLOW} ⚠️ Redis is not running. Starting Redis...${NC}"
|
|
sudo systemctl start redis-server || redis-server --daemonize yes
|
|
fi
|
|
}
|
|
|
|
# Setup PostgreSQL databases
|
|
setup_databases() {
|
|
echo -e "${BLUE}🗄️ Setting up PostgreSQL databases...${NC}"
|
|
|
|
if ! command_exists psql; then
|
|
echo -e "${YELLOW}⚠️ PostgreSQL is not installed. Skipping database setup.${NC}"
|
|
echo -e "${YELLOW} Please install PostgreSQL and run: ./scripts/setup_postgresql.sh${NC}"
|
|
return 0
|
|
fi
|
|
|
|
# Start PostgreSQL if not running
|
|
if ! sudo systemctl is-active --quiet postgresql; then
|
|
echo -e "${BLUE} Starting PostgreSQL service...${NC}"
|
|
sudo systemctl start postgresql
|
|
fi
|
|
|
|
# Create user and databases (password from env or freshly generated)
|
|
echo -e "${BLUE} Creating database user and databases...${NC}"
|
|
DB_PASSWORD="${PUNIMTAG_DB_PASSWORD:-$(gen_secret)}"
|
|
export PUNIMTAG_DB_PASSWORD="$DB_PASSWORD"
|
|
sudo -u postgres psql << EOF || true
|
|
-- Create user if it doesn't exist
|
|
DO \$\$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = 'punimtag') THEN
|
|
CREATE USER punimtag WITH PASSWORD '${DB_PASSWORD}';
|
|
END IF;
|
|
END
|
|
\$\$;
|
|
|
|
-- Create main database if it doesn't exist
|
|
SELECT 'CREATE DATABASE punimtag OWNER punimtag'
|
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'punimtag')\gexec
|
|
|
|
-- Create auth database if it doesn't exist
|
|
SELECT 'CREATE DATABASE punimtag_auth OWNER punimtag'
|
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'punimtag_auth')\gexec
|
|
|
|
-- Grant privileges
|
|
GRANT ALL PRIVILEGES ON DATABASE punimtag TO punimtag;
|
|
GRANT ALL PRIVILEGES ON DATABASE punimtag_auth TO punimtag;
|
|
\q
|
|
EOF
|
|
|
|
echo -e "${GREEN} ✅ Databases created${NC}"
|
|
echo -e "${YELLOW} ⚠️ DB password stored only in .env (generated for this install)${NC}"
|
|
}
|
|
|
|
# Create Python virtual environment
|
|
setup_python_venv() {
|
|
echo -e "${BLUE}🐍 Setting up Python virtual environment...${NC}"
|
|
|
|
if [ ! -d "venv" ]; then
|
|
echo -e "${BLUE} Creating virtual environment...${NC}"
|
|
python3 -m venv venv
|
|
echo -e "${GREEN} ✅ Virtual environment created${NC}"
|
|
else
|
|
echo -e "${GREEN} ✅ Virtual environment already exists${NC}"
|
|
fi
|
|
|
|
# Activate virtual environment
|
|
source venv/bin/activate
|
|
|
|
# Upgrade pip
|
|
echo -e "${BLUE} Upgrading pip...${NC}"
|
|
pip install --upgrade pip --quiet
|
|
|
|
echo -e "${GREEN} ✅ Python environment ready${NC}"
|
|
}
|
|
|
|
# Install Python dependencies
|
|
install_python_dependencies() {
|
|
echo -e "${BLUE}📦 Installing Python dependencies...${NC}"
|
|
|
|
if [ ! -f "requirements.txt" ]; then
|
|
echo -e "${RED}❌ requirements.txt not found${NC}"
|
|
return 1
|
|
fi
|
|
|
|
source venv/bin/activate
|
|
pip install -r requirements.txt
|
|
|
|
echo -e "${GREEN} ✅ Python dependencies installed${NC}"
|
|
}
|
|
|
|
# Download DeepFace model weights (optional, but recommended)
|
|
download_deepface_models() {
|
|
echo -e "${BLUE}🤖 Downloading DeepFace model weights (optional)...${NC}"
|
|
|
|
source venv/bin/activate
|
|
|
|
# Check if deepface is installed
|
|
if ! python3 -c "import deepface" 2>/dev/null; then
|
|
echo -e "${YELLOW} ⚠️ DeepFace not installed, skipping model download${NC}"
|
|
return 0
|
|
fi
|
|
|
|
# Create weights directory
|
|
mkdir -p ~/.deepface/weights
|
|
|
|
# Check if ArcFace weights already exist
|
|
if [ -f ~/.deepface/weights/arcface_weights.h5 ]; then
|
|
echo -e "${GREEN} ✅ ArcFace model weights already exist${NC}"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${BLUE} Downloading ArcFace model weights (~131MB, this may take a few minutes)...${NC}"
|
|
|
|
# Try to download using wget (more reliable than Python download in worker)
|
|
if command_exists wget; then
|
|
if wget -q --show-progress \
|
|
https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5 \
|
|
-O ~/.deepface/weights/arcface_weights.h5; then
|
|
echo -e "${GREEN} ✅ ArcFace model weights downloaded successfully${NC}"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Fallback: try with Python (may fail in some environments)
|
|
echo -e "${YELLOW} ⚠️ wget failed, trying Python download...${NC}"
|
|
if python3 << 'PYTHON'
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from deepface import DeepFace
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
# Create a dummy image to trigger model download
|
|
dummy_img = Image.new('RGB', (100, 100), color='black')
|
|
dummy_array = np.array(dummy_img)
|
|
|
|
# This will trigger model download
|
|
try:
|
|
DeepFace.represent(
|
|
img_path=dummy_array,
|
|
model_name="ArcFace",
|
|
detector_backend="retinaface",
|
|
enforce_detection=False,
|
|
align=True,
|
|
)
|
|
print("✅ Model weights downloaded successfully")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
if "arcface_weights" in str(e).lower():
|
|
print(f"⚠️ Download failed: {e}")
|
|
print(" You can download manually later or the worker will try again")
|
|
sys.exit(1)
|
|
# Other errors are OK (like no faces found)
|
|
print("✅ Model weights are available")
|
|
sys.exit(0)
|
|
except ImportError:
|
|
print("⚠️ DeepFace not available")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(f"⚠️ Error: {e}")
|
|
sys.exit(1)
|
|
PYTHON
|
|
then
|
|
echo -e "${GREEN} ✅ DeepFace models ready${NC}"
|
|
else
|
|
echo -e "${YELLOW} ⚠️ Automatic download failed. Models will be downloaded on first use.${NC}"
|
|
echo -e "${YELLOW} ⚠️ To download manually, run:${NC}"
|
|
echo -e "${BLUE} mkdir -p ~/.deepface/weights${NC}"
|
|
echo -e "${BLUE} wget https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5 -O ~/.deepface/weights/arcface_weights.h5${NC}"
|
|
fi
|
|
}
|
|
|
|
# Install frontend dependencies
|
|
install_frontend_dependencies() {
|
|
echo -e "${BLUE}📦 Installing frontend dependencies...${NC}"
|
|
|
|
# Install admin frontend
|
|
if [ -d "admin-frontend" ]; then
|
|
echo -e "${BLUE} Installing admin frontend dependencies...${NC}"
|
|
cd admin-frontend
|
|
if [ -f "package.json" ]; then
|
|
npm install
|
|
echo -e "${GREEN} ✅ Admin frontend dependencies installed${NC}"
|
|
fi
|
|
cd ..
|
|
fi
|
|
|
|
# Install viewer frontend
|
|
if [ -d "viewer-frontend" ]; then
|
|
echo -e "${BLUE} Installing viewer frontend dependencies...${NC}"
|
|
cd viewer-frontend
|
|
if [ -f "package.json" ]; then
|
|
npm install
|
|
echo -e "${GREEN} ✅ Viewer frontend dependencies installed${NC}"
|
|
fi
|
|
cd ..
|
|
fi
|
|
}
|
|
|
|
# Create .env file if it doesn't exist
|
|
create_env_file() {
|
|
echo -e "${BLUE}⚙️ Setting up environment configuration...${NC}"
|
|
|
|
if [ ! -f ".env" ]; then
|
|
echo -e "${BLUE} Creating .env file...${NC}"
|
|
DB_PASSWORD="${PUNIMTAG_DB_PASSWORD:-$(gen_secret)}"
|
|
ADMIN_PASSWORD="${PUNIMTAG_ADMIN_PASSWORD:-$(gen_secret)}"
|
|
SECRET_KEY="$(gen_secret)$(gen_secret)"
|
|
cat > .env << EOF
|
|
# Database (PostgreSQL by default) — password generated at install; do not commit .env
|
|
DATABASE_URL=postgresql+psycopg2://punimtag:${DB_PASSWORD}@localhost:5432/punimtag
|
|
|
|
# Auth Database (for frontend website user accounts - separate from main database)
|
|
DATABASE_URL_AUTH=postgresql+psycopg2://punimtag:${DB_PASSWORD}@localhost:5432/punimtag_auth
|
|
|
|
# JWT Secrets (change in production!)
|
|
SECRET_KEY=${SECRET_KEY}
|
|
|
|
# Admin credentials (generated; change if this host is exposed)
|
|
ADMIN_USERNAME=admin
|
|
ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
|
|
# Photo storage directory (default: data/uploads)
|
|
PHOTO_STORAGE_DIR=data/uploads
|
|
EOF
|
|
echo -e "${GREEN} ✅ .env file created${NC}"
|
|
echo -e "${YELLOW} ⚠️ Credentials are in .env only — never commit that file${NC}"
|
|
else
|
|
echo -e "${GREEN} ✅ .env file already exists${NC}"
|
|
fi
|
|
|
|
# Create admin frontend .env file if it doesn't exist
|
|
if [ -d "admin-frontend" ] && [ ! -f "admin-frontend/.env" ]; then
|
|
echo -e "${BLUE} Creating admin-frontend/.env file...${NC}"
|
|
cat > admin-frontend/.env << 'EOF'
|
|
# Backend API URL (must be accessible from browsers)
|
|
VITE_API_URL=http://127.0.0.1:8000
|
|
EOF
|
|
echo -e "${GREEN} ✅ admin-frontend/.env file created${NC}"
|
|
elif [ -d "admin-frontend" ]; then
|
|
echo -e "${GREEN} ✅ admin-frontend/.env file already exists${NC}"
|
|
fi
|
|
|
|
# Create viewer frontend .env file if it doesn't exist
|
|
if [ -d "viewer-frontend" ] && [ ! -f "viewer-frontend/.env.local" ]; then
|
|
echo -e "${BLUE} Creating viewer-frontend/.env.local file...${NC}"
|
|
DB_PASSWORD="${PUNIMTAG_DB_PASSWORD:-$(gen_secret)}"
|
|
NEXTAUTH_SECRET="$(gen_secret)$(gen_secret)"
|
|
cat > viewer-frontend/.env.local << EOF
|
|
# Database connection (read-only recommended) — do not commit
|
|
DATABASE_URL=postgresql://punimtag:${DB_PASSWORD}@localhost:5432/punimtag
|
|
|
|
# Auth database connection
|
|
DATABASE_URL_AUTH=postgresql://punimtag:${DB_PASSWORD}@localhost:5432/punimtag_auth
|
|
|
|
# NextAuth configuration
|
|
NEXTAUTH_URL=http://localhost:3001
|
|
NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
|
EOF
|
|
echo -e "${GREEN} ✅ viewer-frontend/.env.local file created${NC}"
|
|
elif [ -d "viewer-frontend" ]; then
|
|
echo -e "${GREEN} ✅ viewer-frontend/.env.local file already exists${NC}"
|
|
fi
|
|
}
|
|
|
|
# Create data directories
|
|
create_data_directories() {
|
|
echo -e "${BLUE}📁 Creating data directories...${NC}"
|
|
|
|
mkdir -p data/uploads
|
|
mkdir -p data/thumbnails
|
|
|
|
echo -e "${GREEN} ✅ Data directories created${NC}"
|
|
}
|
|
|
|
# Main installation function
|
|
main() {
|
|
echo -e "${BLUE}🔍 Checking prerequisites...${NC}"
|
|
echo ""
|
|
|
|
# Check Python
|
|
if ! check_python_version; then
|
|
echo -e "${RED}❌ Please install Python 3.12 or higher${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Check Node.js
|
|
if ! check_node_version; then
|
|
echo -e "${RED}❌ Please install Node.js 18 or higher${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Check npm
|
|
if ! command_exists npm; then
|
|
echo -e "${RED}❌ npm is not installed${NC}"
|
|
exit 1
|
|
else
|
|
echo -e "${GREEN}✅ npm found${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BLUE}🚀 Starting installation...${NC}"
|
|
echo ""
|
|
|
|
# Install system dependencies (Linux only)
|
|
install_system_dependencies
|
|
echo ""
|
|
|
|
# Setup databases
|
|
setup_databases
|
|
echo ""
|
|
|
|
# Setup Python environment
|
|
setup_python_venv
|
|
echo ""
|
|
|
|
# Install Python dependencies
|
|
install_python_dependencies
|
|
echo ""
|
|
|
|
# Download DeepFace model weights (optional)
|
|
download_deepface_models
|
|
echo ""
|
|
|
|
# Install frontend dependencies
|
|
install_frontend_dependencies
|
|
echo ""
|
|
|
|
# Create .env files
|
|
create_env_file
|
|
echo ""
|
|
|
|
# Create data directories
|
|
create_data_directories
|
|
echo ""
|
|
|
|
# Success message
|
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ ✅ Installation Complete! ║${NC}"
|
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e "${BLUE}📋 Next Steps:${NC}"
|
|
echo ""
|
|
echo -e "1. ${YELLOW}Review and update .env file${NC} (if needed):"
|
|
echo -e " ${BLUE} nano .env${NC}"
|
|
echo ""
|
|
echo -e "2. ${YELLOW}Activate the virtual environment:${NC}"
|
|
echo -e " ${BLUE} source venv/bin/activate${NC}"
|
|
echo ""
|
|
echo -e "3. ${YELLOW}Start the backend API:${NC}"
|
|
echo -e " ${BLUE} export PYTHONPATH=$PROJECT_ROOT${NC}"
|
|
echo -e " ${BLUE} uvicorn backend.app:app --host 127.0.0.1 --port 8000${NC}"
|
|
echo ""
|
|
echo -e "4. ${YELLOW}In another terminal, start the admin frontend:${NC}"
|
|
echo -e " ${BLUE} cd admin-frontend${NC}"
|
|
echo -e " ${BLUE} npm run dev${NC}"
|
|
echo ""
|
|
echo -e "5. ${YELLOW}In another terminal, start the viewer frontend (optional):${NC}"
|
|
echo -e " ${BLUE} cd viewer-frontend${NC}"
|
|
echo -e " ${BLUE} npm run dev${NC}"
|
|
echo ""
|
|
echo -e "6. ${YELLOW}Access the applications:${NC}"
|
|
echo -e " ${BLUE} Admin: http://localhost:3000${NC}"
|
|
echo -e " ${BLUE} Viewer: http://localhost:3001${NC}"
|
|
echo ""
|
|
echo -e "${GREEN}For more information, see README.md${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# Run main function
|
|
main
|
|
|