#!/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}" # Ensure HOME is writable (needed for CLI usage) HOME_DIR="/app/data/home" export HOME="$HOME_DIR" mkdir -p "$HOME" # 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 # =============================================== # 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" if [ -n "$CLOUDRON_APP_ORIGIN" ]; then BASE_URL="$CLOUDRON_APP_ORIGIN" else BASE_URL="https://${CLOUDRON_APP_DOMAIN:-localhost}" fi RP_ID="${CLOUDRON_APP_FQDN:-${CLOUDRON_APP_DOMAIN:-localhost}}" # S3 configuration (overridable post-install) DEFAULT_S3_ACCESS_KEY="QZ5M3VMBUHDTIFDFCD8E" DEFAULT_S3_SECRET_KEY="pz1eHYjU1NwAbbruedc7swzCuszd57p1rGSFVzjv" DEFAULT_S3_ENDPOINT="https://s3.eu-central-2.wasabisys.com" DEFAULT_S3_REGION="eu-central-2" DEFAULT_S3_BUCKET="ente-due-ren" S3_CONFIG_DIR="/app/data/config" S3_CONFIG_FILE="$S3_CONFIG_DIR/s3.env" write_default_s3_template() { cat > "$S3_CONFIG_FILE" << 'EOF' # S3 configuration overrides for Ente on Cloudron. # Uncomment and set any of the variables below to override the packaged defaults. # After editing this file, restart the Ente app from the Cloudron dashboard. # # Example (previous Wasabi defaults bundled with this package): #S3_ACCESS_KEY=QZ5M3VMBUHDTIFDFCD8E #S3_SECRET_KEY=pz1eHYjU1NwAbbruedc7swzCuszd57p1rGSFVzjv #S3_ENDPOINT=https://s3.eu-central-2.wasabisys.com #S3_REGION=eu-central-2 #S3_BUCKET=ente-due-ren # # Example (Cloudflare R2 — replace placeholders): #S3_ACCESS_KEY=R2_ACCESS_KEY #S3_SECRET_KEY=R2_SECRET_KEY #S3_ENDPOINT=https://.r2.cloudflarestorage.com #S3_REGION=auto #S3_BUCKET= # #S3_ACCESS_KEY= #S3_SECRET_KEY= #S3_ENDPOINT= #S3_REGION= #S3_BUCKET= EOF chown cloudron:cloudron "$S3_CONFIG_FILE" || true } mkdir -p "$S3_CONFIG_DIR" if [ -f "$S3_CONFIG_FILE" ]; then if ! grep -q "previous Wasabi defaults" "$S3_CONFIG_FILE" && ! grep -Eq '^[[:space:]]*[^#[:space:]]' "$S3_CONFIG_FILE"; then log "INFO" "Refreshing S3 configuration template with example values" write_default_s3_template fi log "INFO" "Loading S3 configuration overrides from $S3_CONFIG_FILE" # shellcheck disable=SC1090 set -a . "$S3_CONFIG_FILE" set +a else log "INFO" "S3 configuration file not found, writing template to $S3_CONFIG_FILE" write_default_s3_template fi # Seed Ente CLI configuration directory ENTE_CLI_CONFIG_DIR="$HOME/.ente" ENTE_CLI_CONFIG_FILE="$ENTE_CLI_CONFIG_DIR/config.yaml" if [ ! -f "$ENTE_CLI_CONFIG_FILE" ]; then mkdir -p "$ENTE_CLI_CONFIG_DIR" cat > "$ENTE_CLI_CONFIG_FILE" << EOF # Ente CLI configuration # Uncomment and set the host to point the CLI to this Cloudron deployment. #host: https://${CLOUDRON_APP_DOMAIN:-localhost} EOF chown -R cloudron:cloudron "$HOME_DIR" || true fi S3_ACCESS_KEY="${S3_ACCESS_KEY:-$DEFAULT_S3_ACCESS_KEY}" S3_SECRET_KEY="${S3_SECRET_KEY:-$DEFAULT_S3_SECRET_KEY}" S3_ENDPOINT="${S3_ENDPOINT:-$DEFAULT_S3_ENDPOINT}" S3_REGION="${S3_REGION:-$DEFAULT_S3_REGION}" S3_BUCKET="${S3_BUCKET:-$DEFAULT_S3_BUCKET}" S3_ENDPOINT_HOST="${S3_ENDPOINT#https://}" S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST#http://}" if [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ] || [ -z "$S3_ENDPOINT" ] || [ -z "$S3_REGION" ] || [ -z "$S3_BUCKET" ]; then log "ERROR" "Incomplete S3 configuration detected. Please update $S3_CONFIG_FILE or set environment variables." exit 1 fi log "INFO" "Using S3 configuration" log "INFO" "S3 Endpoint: $S3_ENDPOINT" log "INFO" "S3 Region: $S3_REGION" log "INFO" "S3 Bucket: $S3_BUCKET" ENABLE_SMTP=${ENABLE_SMTP:-false} SMTP_HOST="" SMTP_PORT="" SMTP_ENCRYPTION="" if [ "$ENABLE_SMTP" = "true" ]; then SMTP_HOST="${CLOUDRON_MAIL_SMTP_SERVER:-}" SMTP_PORT="${CLOUDRON_MAIL_SMTP_PORT:-25}" SMTP_ENCRYPTION="${CLOUDRON_MAIL_SMTP_ENCRYPTION:-}" if [ -n "${CLOUDRON_MAIL_SMTPS_PORT:-}" ]; then SMTP_PORT="${CLOUDRON_MAIL_SMTPS_PORT}" SMTP_ENCRYPTION="tls" fi if [ "${SMTP_ENCRYPTION}" = "tls" ] && [ -n "${CLOUDRON_MAIL_DOMAIN:-}" ]; then SMTP_HOST="mail.${CLOUDRON_MAIL_DOMAIN}" fi else log "INFO" "EMAIL_DISABLED: Skipping SMTP configuration (ENABLE_SMTP=false)" fi SMTP_SENDER_NAME="${CLOUDRON_MAIL_FROM_DISPLAY_NAME:-Ente}" # Museum server configuration - create configurations directory structure MUSEUM_CONFIG_DIR="/app/data/ente/server/configurations" MUSEUM_CONFIG="$MUSEUM_CONFIG_DIR/local.yaml" mkdir -p "$MUSEUM_CONFIG_DIR" log "INFO" "Rendering Museum server configuration" cat > "$MUSEUM_CONFIG" << EOF # Museum server configuration # Server settings log-file: "" http: port: 8080 use-tls: false apps: public-albums: "${BASE_URL}/photos" public-locker: "${BASE_URL}/photos" accounts: "${BASE_URL}/accounts" cast: "${BASE_URL}/cast" family: "${BASE_URL}/photos" custom-domain: cname: "${CLOUDRON_APP_DOMAIN:-localhost}" # Database configuration db: host: ${CLOUDRON_POSTGRESQL_HOST} port: ${CLOUDRON_POSTGRESQL_PORT} name: ${CLOUDRON_POSTGRESQL_DATABASE} user: ${CLOUDRON_POSTGRESQL_USERNAME} password: ${CLOUDRON_POSTGRESQL_PASSWORD} sslmode: disable # S3 storage configuration s3: are_local_buckets: false use_path_style_urls: true hot_storage: primary: wasabi-eu-central-2-v3 secondary: wasabi-eu-central-2-v3 b2-eu-cen: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" wasabi-eu-central-2: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" wasabi-eu-central-2-v3: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" compliance: false wasabi-eu-central-2-derived: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" scw-eu-fr: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" scw-eu-fr-locked: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" scw-eu-fr-v3: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" b5: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" b6: key: "${S3_ACCESS_KEY}" secret: "${S3_SECRET_KEY}" endpoint: "${S3_ENDPOINT_HOST}" region: "${S3_REGION}" bucket: "${S3_BUCKET}" derived-storage: wasabi-eu-central-2-v3 # Email settings smtp: host: "${SMTP_HOST}" port: "${SMTP_PORT}" username: "${CLOUDRON_MAIL_SMTP_USERNAME:-}" password: "${CLOUDRON_MAIL_SMTP_PASSWORD:-}" email: "${CLOUDRON_MAIL_FROM:-no-reply@${CLOUDRON_APP_FQDN:-localhost}}" sender-name: "${SMTP_SENDER_NAME}" encryption: "${SMTP_ENCRYPTION}" internal: silent: true disable-registration: false # WebAuthn configuration for passkey support webauthn: rpid: "${RP_ID}" rporigins: - "https://${RP_ID}" key: encryption: yvmG/RnzKrbCb9L3mgsmoxXr9H7i2Z4qlbT0mL3ln4w= hash: KXYiG07wC7GIgvCSdg+WmyWdXDAn6XKYJtp/wkEU7x573+byBRAYtpTP0wwvi8i/4l37uicX1dVTUzwH3sLZyw== jwt: secret: i2DecQmfGreG6q1vBj5tCokhlN41gcfS2cjOs9Po-u8= jobs: cron: skip: true EOF chmod 600 "$MUSEUM_CONFIG" log "INFO" "Wrote Museum configuration to ${MUSEUM_CONFIG}" # =============================================== # 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=${FORCE_PLACEHOLDER:-false} log "INFO" "Setting up Museum server binary" # Copy Museum binary from build location to data directory MUSEUM_BUILD_BIN="/app/museum-bin/museum" log "INFO" "Checking for pre-built Museum binary at: $MUSEUM_BUILD_BIN" if [ -f "$MUSEUM_BUILD_BIN" ]; then log "INFO" "Found pre-built Museum binary, copying to data directory" cp "$MUSEUM_BUILD_BIN" "$MUSEUM_BIN" chmod +x "$MUSEUM_BIN" log "INFO" "Copied Museum binary to $MUSEUM_BIN" else log "WARN" "Pre-built Museum binary not found at $MUSEUM_BUILD_BIN" fi # Copy migration files to Museum working directory MUSEUM_MIGRATIONS_DIR="/app/data/ente/server/migrations" REPO_MIGRATIONS_DIR="/app/data/ente/repository/server/migrations" if [ ! -d "$MUSEUM_MIGRATIONS_DIR" ] && [ -d "$REPO_MIGRATIONS_DIR" ]; then log "INFO" "Copying database migration files" cp -r "$REPO_MIGRATIONS_DIR" "$MUSEUM_MIGRATIONS_DIR" log "INFO" "Copied migration files to $MUSEUM_MIGRATIONS_DIR" else log "INFO" "Migration files already exist or source not available" fi # Copy web templates to Museum working directory MUSEUM_WEB_TEMPLATES_DIR="/app/data/ente/server/web-templates" REPO_WEB_TEMPLATES_DIR="/app/data/ente/repository/server/web-templates" if [ ! -d "$MUSEUM_WEB_TEMPLATES_DIR" ] && [ -d "$REPO_WEB_TEMPLATES_DIR" ]; then log "INFO" "Copying web templates" cp -r "$REPO_WEB_TEMPLATES_DIR" "$MUSEUM_WEB_TEMPLATES_DIR" log "INFO" "Copied web templates to $MUSEUM_WEB_TEMPLATES_DIR" else log "INFO" "Web templates already exist or source not available" fi # Copy mail templates for transactional emails MUSEUM_MAIL_TEMPLATES_DIR="/app/data/ente/server/mail-templates" REPO_MAIL_TEMPLATES_DIR="/app/data/ente/repository/server/mail-templates" if [ ! -d "$MUSEUM_MAIL_TEMPLATES_DIR" ] && [ -d "$REPO_MAIL_TEMPLATES_DIR" ]; then log "INFO" "Copying mail templates" cp -r "$REPO_MAIL_TEMPLATES_DIR" "$MUSEUM_MAIL_TEMPLATES_DIR" log "INFO" "Copied mail templates to $MUSEUM_MAIL_TEMPLATES_DIR" else log "INFO" "Mail templates already exist or source not available" fi # Check if Museum binary exists and is valid log "INFO" "Checking for Museum binary at: $MUSEUM_BIN" if [ -f "$MUSEUM_BIN" ]; then log "INFO" "Museum binary file exists" if [ -x "$MUSEUM_BIN" ]; then log "INFO" "Museum binary is executable" # Since Museum's --help and --version commands trigger full startup (including DB migration), # we'll trust that an existing executable binary should work log "INFO" "Museum binary is ready to use" else log "INFO" "Museum binary exists but is not executable, fixing permissions" chmod +x "$MUSEUM_BIN" if [ -x "$MUSEUM_BIN" ]; then log "INFO" "Fixed permissions, Museum binary is ready to use" else log "WARN" "Failed to fix permissions, using placeholder" USE_PLACEHOLDER=true fi fi else log "WARN" "Museum binary file not found at $MUSEUM_BIN" log "INFO" "Checking directory contents: $(ls -la $(dirname $MUSEUM_BIN) 2>/dev/null || echo 'Directory not found')" USE_PLACEHOLDER=true fi # =============================================== # Web Application Setup # =============================================== log "INFO" "Setting up web applications with writable directory" # Copy web apps to writable data directory first WRITABLE_WEB_DIR="/app/data/web" if [ ! -d "$WRITABLE_WEB_DIR" ]; then log "INFO" "Copying web applications to writable directory" mkdir -p "$WRITABLE_WEB_DIR" cp -r /app/web/* "$WRITABLE_WEB_DIR/" chown -R cloudron:cloudron "$WRITABLE_WEB_DIR" log "INFO" "Web applications copied to $WRITABLE_WEB_DIR" else log "INFO" "Web applications already exist in writable directory" fi # Fix API endpoint configuration in built JavaScript files log "INFO" "Updating API endpoint configuration in web apps" ACTUAL_ENDPOINT="${BASE_URL}/api" log "INFO" "Setting API endpoint to: $ACTUAL_ENDPOINT" declare -a PLACEHOLDER_ENDPOINTS=( "https://example.com/api" "https://placeholder.invalid/api" "https://api.ente.io" "https://api.ente.io/api" ) declare -A HOST_REWRITES=( ["https://accounts.ente.io"]="${BASE_URL}/accounts" ["https://auth.ente.io"]="${BASE_URL}/auth" ["https://cast.ente.io"]="${BASE_URL}/cast" ["https://photos.ente.io"]="${BASE_URL}/photos" ["https://web.ente.io"]="${BASE_URL}/photos" ) for webapp in photos accounts auth cast; do WEB_DIR="$WRITABLE_WEB_DIR/${webapp}" if [ -d "$WEB_DIR" ]; then log "INFO" "Processing ${webapp} app for endpoint rewrites" for placeholder in "${PLACEHOLDER_ENDPOINTS[@]}"; do find "$WEB_DIR" -name "*.js" -type f -exec sed -i "s|${placeholder}|${ACTUAL_ENDPOINT}|g" {} \; done for source in "${!HOST_REWRITES[@]}"; do target="${HOST_REWRITES[$source]}" find "$WEB_DIR" -name "*.js" -type f -exec sed -i "s|${source}|${target}|g" {} \; done log "INFO" "Endpoint rewrites complete for ${webapp}" else log "WARN" "Web directory not found for ${webapp}" 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 = 8080; 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 (returns minimal structure expected by UI) '/api/users/verify-email': (req, res) => { const buildResponse = (emailAddress) => { const email = emailAddress || 'unknown@example.com'; const stableId = Math.abs(Buffer.from(email).reduce((acc, byte) => (acc * 31 + byte) % 100000, 17)) || 1; return { id: stableId, token: `placeholder-token-${stableId}`, encryptedToken: `placeholder-encrypted-token-${stableId}`, accountsUrl: `${process.env.CLOUDRON_APP_ORIGIN || 'https://example.com'}/accounts`, twoFactorSessionID: undefined, twoFactorSessionIDV2: undefined, passkeySessionID: undefined, keyAttributes: undefined }; }; if (req.method !== 'POST') { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); return; } let rawBody = ''; req.on('data', chunk => { rawBody += chunk.toString(); }); req.on('end', () => { let email = 'unknown@example.com'; let ott = 'unknown'; try { const payload = JSON.parse(rawBody || '{}'); if (payload.email) { email = payload.email; } if (payload.ott) { ott = payload.ott; } } catch (err) { log(`Failed to parse verify-email request body: ${err.message}`); } const responsePayload = buildResponse(email); log(`Verifying OTT ${ott} for ${email}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(responsePayload)); }); }, // 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' })); } }, // OTT endpoint '/users/ott': (req, res) => { if (req.method !== 'POST') { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); return; } let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { let email = 'unknown@example.com'; try { const payload = JSON.parse(body || '{}'); if (payload.email) { email = payload.email; } } catch (err) { log(`Failed to parse OTT request body: ${err.message}`); } const ott = ('' + Math.floor(100000 + Math.random() * 900000)).slice(-6); log(`Generated OTT ${ott} for ${email}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, ott, email })); }); }, '/api/users/ott': (req, res) => { apiHandlers['/users/ott'](req, res); }, '/users/verify-email': (req, res) => { apiHandlers['/api/users/verify-email'](req, res); }, '/api/users/verify': (req, res) => { apiHandlers['/api/users/verify-email'](req, res); }, '/users/verify': (req, res) => { apiHandlers['/api/users/verify-email'](req, res); }, '/ping': (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); log('Ping request - responding with status OK'); res.end(JSON.stringify({ status: 'OK', server: 'Ente Placeholder', time: new Date().toISOString() })); }, // 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:8080/ping > /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 export ENVIRONMENT="${MUSEUM_ENVIRONMENT:-local}" stdbuf -oL "$MUSEUM_BIN" 2>&1 | tee -a "$MUSEUM_LOG" & MUSEUM_PID=$! log "INFO" "Started Museum server (pipeline 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:8080/ping > /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" if ps -p "$MUSEUM_PID" > /dev/null 2>&1; then log "INFO" "Stopping Museum server pipeline" kill "$MUSEUM_PID" || true fi log "ERROR" "Last 20 lines of museum.log:" tail -n 20 "$MUSEUM_LOG" | while read -r line; do log "ERROR" " $line" done exit 1 fi fi # =============================================== # Setup Caddy web server # =============================================== log "INFO" "Setting up Caddy web server" # Create Caddy configuration # Note: Caddy listens on port 3080 (Cloudron's httpPort) and proxies API requests to Museum on port 8080 CADDY_CONFIG="/app/data/Caddyfile" cat > "$CADDY_CONFIG" << EOF :3080 { log { output file /app/data/logs/caddy.log level INFO } # Enable compression encode gzip # CORS preflight handling @options { method OPTIONS } handle @options { header { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" Access-Control-Allow-Headers "*" Access-Control-Max-Age "3600" } respond 204 } # API endpoints with CORS handle_path /api/* { reverse_proxy localhost:8080 { header_up Host {http.request.host} header_up X-Real-IP {http.request.remote} header_up X-Forwarded-For {http.request.remote} header_up X-Forwarded-Proto {http.request.scheme} } header { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" Access-Control-Allow-Headers "*" Access-Control-Allow-Credentials "true" } } # Public albums endpoint handle /public/* { reverse_proxy localhost:8080 header { Access-Control-Allow-Origin "*" } } # Health check endpoint handle /health { reverse_proxy localhost:8080 } handle /images/* { rewrite * /photos{path} root * /app/data/web file_server } # Static files for Next.js assets shared across apps handle /_next/* { root * /app/data/web try_files photos{path} accounts{path} auth{path} cast{path} {path} file_server header { Cache-Control "public, max-age=31536000" Access-Control-Allow-Origin "*" } } # Default to serve SPA assets handle { root * /app/data/web try_files {path}/index.html {path} /photos/index.html file_server } } EOF log "INFO" "Validating Caddy configuration" if caddy validate --config "$CADDY_CONFIG" 2>&1 | tee -a "$LOG_FILE"; then log "INFO" "Caddy configuration is valid" else log "ERROR" "Caddy configuration validation failed!" log "ERROR" "Caddyfile contents:" cat "$CADDY_CONFIG" | while read -r line; do log "ERROR" " $line" done fi 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" # Wait a moment to see if Caddy stays running sleep 2 if ps -p $CADDY_PID > /dev/null; then log "INFO" "Caddy is still running after 2 seconds" else log "ERROR" "Caddy has crashed! Last 20 lines of Caddy log:" tail -n 20 /app/data/logs/caddy.log | while read -r line; do log "ERROR" " $line" done fi # =============================================== # 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/config/s3.env\` (uncomment lines and add your values) 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"