#!/bin/bash # =============================================== # Ente Cloudron App - Main Startup Script # =============================================== # Enable strict error handling set -e # Initialize logging LOG_FILE="/app/data/logs/ente.log" mkdir -p /app/data/logs # Log function for consistent output log() { local level="$1" local message="$2" local timestamp=$(date +"%Y-%m-%d %H:%M:%S") echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" } log "INFO" "Starting Ente Cloudron app" log "INFO" "Running in Cloudron environment with domain: ${CLOUDRON_APP_DOMAIN}" # Prevent infinite loops through startup flag if [ -f "/app/data/startup_in_progress" ]; then if [ "$(find /app/data/startup_in_progress -mmin +2)" ]; then log "WARN" "Found old startup flag, removing and continuing" rm -f "/app/data/startup_in_progress" else log "ERROR" "Startup script is already running (started less than 2 minutes ago)" log "ERROR" "Possible infinite loop detected. Exiting." exit 1 fi fi # Create the flag file to indicate we're starting up echo "$(date): Starting up" > /app/data/startup_in_progress trap 'rm -f /app/data/startup_in_progress' EXIT # =============================================== # Initialize directories # =============================================== log "INFO" "Creating necessary directories" # App directories mkdir -p /app/data/ente/server mkdir -p /app/data/ente/web mkdir -p /app/data/tmp mkdir -p /app/data/web/{photos,accounts,auth,cast} # =============================================== # Repository setup # =============================================== ENTE_REPO_DIR="/app/data/ente/repository" log "INFO" "Setting up Ente repository at ${ENTE_REPO_DIR}" if [ ! -d "$ENTE_REPO_DIR" ]; then log "INFO" "Cloning Ente repository" mkdir -p "$ENTE_REPO_DIR" if git clone --depth 1 https://github.com/ente-io/ente "$ENTE_REPO_DIR"; then log "INFO" "Repository cloned successfully" else log "ERROR" "Failed to clone repository" fi else log "INFO" "Repository already exists, pulling latest changes" cd "$ENTE_REPO_DIR" if git pull; then log "INFO" "Repository updated successfully" else log "WARN" "Failed to update repository, using existing version" fi fi # =============================================== # Configuration # =============================================== log "INFO" "Setting up configuration" # S3 configuration S3_CONFIG="/app/data/s3.env" if [ ! -f "$S3_CONFIG" ]; then log "INFO" "Creating default S3 configuration file" cat > "$S3_CONFIG" << EOF # Ente S3 Storage Configuration # Edit these values with your S3-compatible storage credentials # Required settings S3_ACCESS_KEY=your-access-key S3_SECRET_KEY=your-secret-key S3_ENDPOINT=your-s3-endpoint # e.g., s3.amazonaws.com S3_REGION=your-region # e.g., us-east-1 S3_BUCKET=your-bucket-name # Optional settings # S3_PREFIX=ente/ # Optional prefix for all objects # S3_PUBLIC_URL= # Optional public URL for the bucket (if different from endpoint) EOF chmod 600 "$S3_CONFIG" log "INFO" "Created S3 config template at ${S3_CONFIG}" log "WARN" "⚠️ YOU MUST EDIT /app/data/s3.env WITH YOUR ACTUAL S3 CREDENTIALS ⚠️" else log "INFO" "S3 configuration file already exists" fi # Load S3 configuration if [ -f "$S3_CONFIG" ]; then source "$S3_CONFIG" log "INFO" "Loaded S3 configuration" fi # Museum server configuration MUSEUM_CONFIG="/app/data/ente/server/museum.yaml" if [ ! -f "$MUSEUM_CONFIG" ]; then log "INFO" "Creating Museum server configuration" cat > "$MUSEUM_CONFIG" << EOF # Museum server configuration # Server settings port: 3080 host: 0.0.0.0 log_level: info # Database configuration db: driver: postgres source: "postgres://${CLOUDRON_POSTGRESQL_USERNAME}:${CLOUDRON_POSTGRESQL_PASSWORD}@${CLOUDRON_POSTGRESQL_HOST}:${CLOUDRON_POSTGRESQL_PORT}/${CLOUDRON_POSTGRESQL_DATABASE}?sslmode=disable" max_conns: 10 max_idle: 5 # CORS settings cors: allow_origins: - "*" # S3 storage configuration s3: endpoint: "${S3_ENDPOINT:-s3.amazonaws.com}" region: "${S3_REGION:-us-east-1}" access_key: "${S3_ACCESS_KEY}" secret_key: "${S3_SECRET_KEY}" bucket: "${S3_BUCKET}" public_url: "https://${CLOUDRON_APP_FQDN}/photos" # Email settings email: enabled: true host: "${CLOUDRON_SMTP_SERVER:-localhost}" port: ${CLOUDRON_SMTP_PORT:-25} username: "${CLOUDRON_SMTP_USERNAME:-""}" password: "${CLOUDRON_SMTP_PASSWORD:-""}" from: "Ente <${CLOUDRON_MAIL_FROM:-no-reply@${CLOUDRON_APP_DOMAIN}}>" EOF chmod 600 "$MUSEUM_CONFIG" log "INFO" "Created Museum configuration at ${MUSEUM_CONFIG}" else log "INFO" "Museum configuration already exists" fi # =============================================== # Database check # =============================================== log "INFO" "Testing PostgreSQL connectivity" if PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql -h "$CLOUDRON_POSTGRESQL_HOST" \ -p "$CLOUDRON_POSTGRESQL_PORT" -U "$CLOUDRON_POSTGRESQL_USERNAME" \ -d "$CLOUDRON_POSTGRESQL_DATABASE" -c "SELECT 1;" > /dev/null 2>&1; then log "INFO" "PostgreSQL connection successful" else log "ERROR" "Failed to connect to PostgreSQL" log "ERROR" "Connection details:" log "ERROR" " Host: $CLOUDRON_POSTGRESQL_HOST" log "ERROR" " Port: $CLOUDRON_POSTGRESQL_PORT" log "ERROR" " User: $CLOUDRON_POSTGRESQL_USERNAME" log "ERROR" " Database: $CLOUDRON_POSTGRESQL_DATABASE" exit 1 fi # =============================================== # Museum Server Binary Setup # =============================================== MUSEUM_BIN="/app/data/ente/server/museum" MUSEUM_LOG="/app/data/logs/museum.log" USE_PLACEHOLDER=false log "INFO" "Setting up Museum server binary" # Function to validate a binary validate_binary() { local bin_path="$1" # Basic file existence check if [ ! -f "$bin_path" ]; then return 1 fi # Check if file is executable if [ ! -x "$bin_path" ]; then chmod +x "$bin_path" || return 1 fi # Check if it's a text file (most likely an error message) if file "$bin_path" | grep -q "text"; then return 1 fi # Check if it's a valid binary type if ! file "$bin_path" | grep -q -E "ELF|Mach-O|PE32"; then return 1 fi return 0 } # Check and remove invalid binary if [ -f "$MUSEUM_BIN" ]; then if ! validate_binary "$MUSEUM_BIN"; then log "WARN" "Found invalid Museum binary, removing" rm -f "$MUSEUM_BIN" else log "INFO" "Found valid Museum binary" fi fi # Build or download if needed if [ ! -f "$MUSEUM_BIN" ]; then # Try building first if Go is available if command -v go >/dev/null 2>&1; then log "INFO" "Go is available, attempting to build Museum server" cd "$ENTE_REPO_DIR/server" export GOPATH="/app/data/go" export PATH="$GOPATH/bin:$PATH" mkdir -p "$GOPATH/src" "$GOPATH/bin" "$GOPATH/pkg" # Install dependencies if needed if command -v apt-get >/dev/null 2>&1; then log "INFO" "Installing build dependencies" apt-get update -y && apt-get install -y gcc libsodium-dev pkg-config fi log "INFO" "Building Museum server..." if go build -o "$MUSEUM_BIN" ./cmd/museum; then if validate_binary "$MUSEUM_BIN"; then log "INFO" "Successfully built Museum server" else log "ERROR" "Build completed but resulted in an invalid binary" rm -f "$MUSEUM_BIN" fi else log "ERROR" "Failed to build Museum server" fi else log "INFO" "Go is not available, skipping build attempt" fi # If build failed or wasn't attempted, try downloading if [ ! -f "$MUSEUM_BIN" ] || ! validate_binary "$MUSEUM_BIN"; then log "INFO" "Attempting to download pre-built Museum server binary" # Determine architecture ARCH=$(uname -m) OS=$(uname -s | tr '[:upper:]' '[:lower:]') # Map architecture to standard names if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; fi if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then ARCH="arm64"; fi log "INFO" "Detected system: $OS-$ARCH" # Define possible download URLs DOWNLOAD_URLS=( "https://github.com/ente-io/ente/releases/latest/download/museum-${OS}-${ARCH}" "https://github.com/ente-io/ente/releases/download/latest/museum-${OS}-${ARCH}" "https://github.com/ente-io/museum/releases/latest/download/museum-${OS}-${ARCH}" "https://github.com/ente-io/museum/releases/download/latest/museum-${OS}-${ARCH}" ) # Try each URL SUCCESS=false for URL in "${DOWNLOAD_URLS[@]}"; do log "INFO" "Attempting download from $URL" if curl -L -f -s -o "$MUSEUM_BIN.tmp" "$URL"; then chmod +x "$MUSEUM_BIN.tmp" if validate_binary "$MUSEUM_BIN.tmp"; then mv "$MUSEUM_BIN.tmp" "$MUSEUM_BIN" log "INFO" "Successfully downloaded Museum server binary" SUCCESS=true break else log "WARN" "Downloaded file is not a valid binary" rm -f "$MUSEUM_BIN.tmp" fi else log "WARN" "Failed to download from $URL" fi done if [ "$SUCCESS" = false ]; then log "ERROR" "All download attempts failed" USE_PLACEHOLDER=true fi fi fi # Final check for Museum binary if [ ! -f "$MUSEUM_BIN" ] || ! validate_binary "$MUSEUM_BIN"; then log "WARN" "No valid Museum binary available" USE_PLACEHOLDER=true fi # =============================================== # Web Application Setup # =============================================== log "INFO" "Setting up web applications" # Function to create a placeholder page create_placeholder_page() { local app_name="$1" local app_dir="/app/data/web/$app_name" mkdir -p "$app_dir" cat > "$app_dir/index.html" << EOF Ente $app_name

Ente $app_name

Status: The Ente server is being configured.

This is the Ente $app_name application running on your Cloudron. To complete the setup:

  1. Configure your S3 storage in /app/data/s3.env
  2. Ensure the Museum server is properly running
  3. You might need to restart the app after configuration changes

GitHub Repository · Documentation

EOF # Create runtime config cat > "$app_dir/runtime-config.js" << EOF window.RUNTIME_CONFIG = { API_URL: "/api", PUBLIC_ALBUMS_URL: "/public", DEBUG: true }; console.log("Loaded Ente runtime config:", window.RUNTIME_CONFIG); EOF log "INFO" "Created placeholder for $app_name app" } # Create placeholder pages for each app if they don't exist for APP in photos accounts auth cast; do if [ ! -f "/app/data/web/$APP/index.html" ]; then create_placeholder_page "$APP" fi done # =============================================== # Node.js Placeholder Server # =============================================== create_nodejs_placeholder() { log "INFO" "Creating Node.js placeholder server" # Create server script cat > "/app/data/ente/server/placeholder.js" << 'EOF' const http = require('http'); const fs = require('fs'); const { execSync } = require('child_process'); const path = require('path'); const PORT = 3080; const LOG_FILE = '/app/data/logs/museum.log'; const DB_SCHEMA_FILE = '/app/data/ente/server/schema.sql'; // Log function function log(message) { const timestamp = new Date().toISOString(); const logMessage = `${timestamp} - ${message}\n`; console.log(logMessage); try { fs.appendFileSync(LOG_FILE, logMessage); } catch (err) { console.error(`Error writing to log: ${err.message}`); } } log('Starting Ente placeholder server...'); // Try to initialize the database schema function initializeDatabase() { try { // Create a basic schema file if it doesn't exist if (!fs.existsSync(DB_SCHEMA_FILE)) { log('Creating basic database schema file'); const basicSchema = ` -- Basic schema for Ente Museum server CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS files ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), filename VARCHAR(255) NOT NULL, path VARCHAR(255) NOT NULL, mime_type VARCHAR(100), size BIGINT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); `; fs.writeFileSync(DB_SCHEMA_FILE, basicSchema); } // Initialize database const dbUser = process.env.CLOUDRON_POSTGRESQL_USERNAME; const dbPassword = process.env.CLOUDRON_POSTGRESQL_PASSWORD; const dbHost = process.env.CLOUDRON_POSTGRESQL_HOST; const dbPort = process.env.CLOUDRON_POSTGRESQL_PORT; const dbName = process.env.CLOUDRON_POSTGRESQL_DATABASE; if (dbUser && dbPassword && dbHost && dbPort && dbName) { log(`Initializing database ${dbName} on ${dbHost}:${dbPort}`); const command = `PGPASSWORD="${dbPassword}" psql -h "${dbHost}" -p "${dbPort}" -U "${dbUser}" -d "${dbName}" -f "${DB_SCHEMA_FILE}"`; execSync(command, { stdio: 'inherit' }); log('Database initialized successfully'); return true; } else { log('Database environment variables not set, skipping initialization'); return false; } } catch (err) { log(`Error initializing database: ${err.message}`); return false; } } // Try to initialize database initializeDatabase(); // API response handlers const apiHandlers = { // Health check endpoint '/health': (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'OK', server: 'Ente Placeholder', version: '1.0.0' })); log('Health check request - responded with status OK'); }, // User verification endpoint '/api/users/verify': (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); log('User verify request - responding with success'); res.end(JSON.stringify({ success: true, isValidEmail: true, isAvailable: true, isVerified: true, canCreateAccount: true })); }, // User login endpoint '/api/users/login': (req, res) => { if (req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { log('Login request received'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, token: 'placeholder-jwt-token-' + Date.now(), user: { id: 1, email: 'placeholder@example.com', name: 'Placeholder User' } })); }); } else { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); } }, // User signup endpoint '/api/users/signup': (req, res) => { if (req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { log('Signup request received'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, token: 'placeholder-jwt-token-' + Date.now(), user: { id: 1, email: 'placeholder@example.com', name: 'New User' } })); }); } else { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); } }, // Files endpoint '/api/files': (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); log('Files request - responding with empty list'); res.end(JSON.stringify({ success: true, files: [] })); }, // Collections endpoint '/api/collections': (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); log('Collections request - responding with empty list'); res.end(JSON.stringify({ success: true, collections: [] })); }, // Default API handler 'default': (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); log(`API request to ${req.url} - responding with generic success`); res.end(JSON.stringify({ success: true, message: 'Placeholder API response', path: req.url })); } }; // Create server const server = http.createServer((req, res) => { log(`Request received: ${req.method} ${req.url}`); // Set CORS headers for all responses res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type,Authorization'); // Handle OPTIONS request (for CORS preflight) if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } // Handle health check endpoint if (req.url === '/health' || req.url === '/api/health') { apiHandlers['/health'](req, res); return; } // Handle paths that exactly match defined endpoints if (apiHandlers[req.url]) { apiHandlers[req.url](req, res); return; } // Route based on URL pattern if (req.url.startsWith('/api/')) { const handler = apiHandlers['default']; handler(req, res); return; } // Default response for any other endpoint res.writeHead(200, { 'Content-Type': 'application/json' }); log(`Unknown request to ${req.url} - responding with default message`); res.end(JSON.stringify({ message: 'Ente Placeholder Server', path: req.url, server: 'Node.js Placeholder' })); }); // Start server try { server.listen(PORT, '0.0.0.0', () => { log(`Ente placeholder server running on port ${PORT}`); log(`Server is listening at http://0.0.0.0:${PORT}`); }); } catch (err) { log(`Failed to start server: ${err.message}`); process.exit(1); } // Handle errors server.on('error', (error) => { log(`Server error: ${error.message}`); if (error.code === 'EADDRINUSE') { log('Address already in use, retrying in 5 seconds...'); setTimeout(() => { server.close(); server.listen(PORT, '0.0.0.0'); }, 5000); } }); log('Ente placeholder server initialization complete'); EOF # Start the Node.js placeholder server log "INFO" "Starting Node.js placeholder server" cd /app/data/ente/server node placeholder.js > "$MUSEUM_LOG" 2>&1 & PLACEHOLDER_PID=$! log "INFO" "Started Node.js server with PID: $PLACEHOLDER_PID" # Wait for the server to start MAX_ATTEMPTS=30 ATTEMPT=0 SUCCESS=false while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do if curl -s http://localhost:3080/health > /dev/null 2>&1; then log "INFO" "Node.js placeholder server started successfully" SUCCESS=true break fi ATTEMPT=$((ATTEMPT+1)) log "INFO" "Waiting for server to start (attempt $ATTEMPT/$MAX_ATTEMPTS)" sleep 1 done if [ "$SUCCESS" = false ]; then log "ERROR" "Node.js placeholder server failed to start" log "ERROR" "Last 20 lines of log:" tail -n 20 "$MUSEUM_LOG" | while read -r line; do log "ERROR" " $line" done return 1 fi return 0 } # =============================================== # Start the appropriate server # =============================================== log "INFO" "Starting server" if [ "$USE_PLACEHOLDER" = true ]; then log "INFO" "Using Node.js placeholder server" create_nodejs_placeholder else log "INFO" "Starting actual Museum server" cd /app/data/ente/server "$MUSEUM_BIN" --config "$MUSEUM_CONFIG" > "$MUSEUM_LOG" 2>&1 & MUSEUM_PID=$! log "INFO" "Started Museum server with PID: $MUSEUM_PID" # Wait for the server to start MAX_ATTEMPTS=30 ATTEMPT=0 SUCCESS=false while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do if curl -s http://localhost:3080/health > /dev/null 2>&1; then log "INFO" "Museum server started successfully" SUCCESS=true break fi ATTEMPT=$((ATTEMPT+1)) log "INFO" "Waiting for Museum server to start (attempt $ATTEMPT/$MAX_ATTEMPTS)" sleep 1 done if [ "$SUCCESS" = false ]; then log "ERROR" "Museum server failed to start within $MAX_ATTEMPTS seconds" log "ERROR" "Last 20 lines of museum.log:" tail -n 20 "$MUSEUM_LOG" | while read -r line; do log "ERROR" " $line" done log "WARN" "Falling back to Node.js placeholder server" create_nodejs_placeholder fi fi # =============================================== # Setup Caddy web server # =============================================== log "INFO" "Setting up Caddy web server" # Create Caddy configuration CADDY_CONFIG="/app/data/Caddyfile" cat > "$CADDY_CONFIG" << EOF :3080 { log { output file /app/data/logs/caddy.log } # API endpoints handle /api/* { reverse_proxy localhost:3080 } # Public albums endpoint handle /public/* { reverse_proxy localhost:3080 } # Health check endpoint handle /health { reverse_proxy localhost:3080 } # Static web apps handle /photos/* { root * /app/data/web/photos try_files {path} /index.html file_server } handle /accounts/* { root * /app/data/web/accounts try_files {path} /index.html file_server } handle /auth/* { root * /app/data/web/auth try_files {path} /index.html file_server } handle /cast/* { root * /app/data/web/cast try_files {path} /index.html file_server } # Redirect root to photos handle { redir / /photos/ } } EOF log "INFO" "Starting Caddy web server" caddy run --config "$CADDY_CONFIG" > /app/data/logs/caddy.log 2>&1 & CADDY_PID=$! log "INFO" "Caddy web server started with PID: $CADDY_PID" # =============================================== # Finalization and monitoring # =============================================== log "INFO" "Setup complete" # Create startup instructions cat > /app/data/SETUP-INSTRUCTIONS.md << EOF # Ente Cloudron App - Setup Instructions ## Configuration 1. **S3 Storage**: Edit the configuration file at \`/app/data/s3.env\` with your S3-compatible storage credentials. 2. **Museum Server**: The server configuration is at \`/app/data/ente/server/museum.yaml\` if you need to customize settings. ## Troubleshooting - **Logs**: Check the logs at \`/app/data/logs/\` for any issues. - **Restart**: If you change configuration, restart the app to apply changes. ## Web Applications The following web applications are available: - Photos: https://${CLOUDRON_APP_FQDN}/photos/ - Accounts: https://${CLOUDRON_APP_FQDN}/accounts/ - Auth: https://${CLOUDRON_APP_FQDN}/auth/ - Cast: https://${CLOUDRON_APP_FQDN}/cast/ ## Support For more information, visit the [Ente GitHub repository](https://github.com/ente-io/ente). EOF # Remove startup flag rm -f /app/data/startup_in_progress # Verify running services log "INFO" "Verifying running services" if ps aux | grep -E "museum|placeholder" | grep -v grep > /dev/null; then log "INFO" "Server is running" else log "ERROR" "No server is running!" fi if ps aux | grep caddy | grep -v grep > /dev/null; then log "INFO" "Caddy server is running" else log "ERROR" "Caddy server is not running!" fi log "INFO" "Ente Cloudron app startup complete" # Keep the script running to prevent container exit exec tail -f "$MUSEUM_LOG"