diff --git a/start.sh b/start.sh index 43c4e9a..10146be 100644 --- a/start.sh +++ b/start.sh @@ -1,1728 +1,527 @@ #!/bin/bash +set -e -# Better signal handling - forward signals to child processes -trap 'kill -TERM $SERVER_PID; kill -TERM $PUBLIC_SERVER_PID; kill -TERM $CADDY_PID; exit' TERM INT - -set -eu - +# Cloudron app startup script for Ente echo "==> Starting Ente Cloudron app..." -# Create necessary directories -mkdir -p /app/data/config /app/data/storage /app/data/caddy /app/data/go /app/data/logs - -# Create and set proper permissions for patched directory early +# We need to be careful with file permissions, as /app/data is the only writable location mkdir -p /app/data/patched chmod -R 777 /app/data/patched echo "==> Created and set full permissions (777) on /app/data/patched directory" -# Add comment about Cloudron filesystem limitations echo "==> NOTE: Running in Cloudron environment with limited write access" echo "==> Writable directories: /app/data, /tmp, /run" -# Define the server directory -SERVER_DIR="/app/code/server" -if [ ! -d "$SERVER_DIR" ]; then - if [ -d "/app/code/museum" ]; then - SERVER_DIR="/app/code/museum" - else - # Look for main.go in likely places - SERVER_DIR=$(dirname $(find /app/code -name "main.go" -path "*/server*" -o -path "*/museum*" | head -1)) - if [ ! -d "$SERVER_DIR" ]; then - echo "==> WARNING: Could not find server directory, using /app/code as fallback" - SERVER_DIR="/app/code" - fi - fi -fi -echo "==> Using server directory: $SERVER_DIR" +# Configure important paths +MUSEUM_DIR="/app/code/server" +CONFIG_DIR="/app/data/config" +LOGS_DIR="/app/data/logs" +WEB_DIR="/app/web" +CADDY_DATA_DIR="/app/data/caddy" -# One-time initialization tracking -if [[ ! -f /app/data/.initialized ]]; then - echo "==> Fresh installation, setting up data directory..." - - echo "==> DEBUG: Full repository structure at /app/code" - find /app/code -type d -maxdepth 3 -not -path "*/node_modules/*" -not -path "*/\.*" | sort +# Create necessary directories +mkdir -p "$CONFIG_DIR" "$LOGS_DIR" "$CADDY_DATA_DIR" - echo "==> DEBUG: Looking for Go files" - find /app/code -name "*.go" | grep -v test | sort | head -10 - - echo "==> DEBUG: Looking for server-related directories" - find /app/code -type d -path "*/server*" -o -path "*/museum*" | sort - - echo "==> DEBUG: All package.json files in repository" - find /app/code -name "package.json" -not -path "*/node_modules/*" | sort - - echo "==> DEBUG: Looking for web app directories" - find /app/code -type d -path "*/web*" | sort - - echo "==> DEBUG: Web app directories in /app/web (if they exist)" - if [ -d "/app/web" ]; then - ls -la /app/web - else - echo "Web app directory not yet copied to /app/web" - fi - - # Create config template file on first run - echo "==> First run - creating configuration template" - - # Generate random secrets - JWT_SECRET=$(openssl rand -hex 32) - SESSION_SECRET=$(openssl rand -hex 32) - MASTER_KEY=$(openssl rand -hex 32) - - # Replace variables in template for things we know - sed \ - -e "s|%%POSTGRESQL_HOST%%|${CLOUDRON_POSTGRESQL_HOST}|g" \ - -e "s|%%POSTGRESQL_PORT%%|${CLOUDRON_POSTGRESQL_PORT}|g" \ - -e "s|%%POSTGRESQL_USERNAME%%|${CLOUDRON_POSTGRESQL_USERNAME}|g" \ - -e "s|%%POSTGRESQL_PASSWORD%%|${CLOUDRON_POSTGRESQL_PASSWORD}|g" \ - -e "s|%%POSTGRESQL_DATABASE%%|${CLOUDRON_POSTGRESQL_DATABASE}|g" \ - -e "s|%%APP_ORIGIN%%|${CLOUDRON_APP_ORIGIN}|g" \ - -e "s|%%MAIL_SMTP_SERVER%%|${CLOUDRON_MAIL_SMTP_SERVER}|g" \ - -e "s|%%MAIL_SMTP_PORT%%|${CLOUDRON_MAIL_SMTP_PORT}|g" \ - -e "s|%%MAIL_SMTP_USERNAME%%|${CLOUDRON_MAIL_SMTP_USERNAME}|g" \ - -e "s|%%MAIL_SMTP_PASSWORD%%|${CLOUDRON_MAIL_SMTP_PASSWORD}|g" \ - -e "s|%%MAIL_FROM%%|${CLOUDRON_MAIL_FROM}|g" \ - -e "s|%%MAIL_FROM_DISPLAY_NAME%%|${CLOUDRON_MAIL_FROM_DISPLAY_NAME}|g" \ - -e "s|%%JWT_SECRET%%|${JWT_SECRET}|g" \ - -e "s|%%SESSION_SECRET%%|${SESSION_SECRET}|g" \ - -e "s|%%MASTER_KEY%%|${MASTER_KEY}|g" \ - /app/pkg/config.template.yaml > /app/data/config/config.yaml - - # Create an S3 configuration file template - cat > /app/data/config/s3.env.template < IMPORTANT: S3 storage configuration required" - echo "==> Please configure your S3 storage as follows:" - echo "1. Log into your Cloudron dashboard" - echo "2. Go to the app's configuration page" - echo "3. Edit the file /app/data/config/s3.env" - echo "4. Restart the app" - - # Mark initialization as complete - touch /app/data/.initialized - echo "==> Initialization complete" -fi - -# Check if configuration exists -if [ ! -f "/app/data/config/s3.env" ]; then - echo "==> First run - creating configuration template" - mkdir -p /app/data/config - - # Create a template S3 configuration file - echo "==> Creating S3 configuration template" - cat > /app/data/config/s3.env.template < IMPORTANT: S3 storage configuration required" - echo "==> Please configure your S3 storage as follows:" - echo "1. Log into your Cloudron dashboard" - echo "2. Go to the app's configuration page" - echo "3. Edit the file /app/data/config/s3.env" - echo "4. Restart the app" +# Determine the endpoint configuration +CLOUDRON_APP_FQDN="${CLOUDRON_APP_DOMAIN}" +if [ -n "${CLOUDRON_APP_ORIGIN}" ]; then + CLOUDRON_APP_FQDN="${CLOUDRON_APP_DOMAIN}" else + # If origin not set, use the app domain + CLOUDRON_APP_ORIGIN="https://${CLOUDRON_APP_DOMAIN}" +fi + +API_ENDPOINT="/api" +CADDY_PORT="3080" +API_PORT="8080" +PUBLIC_ALBUMS_PORT="8081" + +echo "==> Using server directory: ${MUSEUM_DIR}" + +# Check if we have S3 configuration +if [ -f "${CONFIG_DIR}/s3.env" ]; then echo "==> Using existing S3 configuration" -fi - -# Check if s3.env is empty -if [ ! -s "/app/data/config/s3.env" ]; then - echo "==> WARNING: S3 configuration file is empty. The app will not function correctly until configured." - echo "==> Please refer to the template at /app/data/config/s3.env.template for instructions." -fi - -# Source S3 configuration -if [ -f /app/data/config/s3.env ]; then - echo "==> Sourcing S3 configuration from /app/data/config/s3.env" -source /app/data/config/s3.env -fi - -# Display S3 configuration (masking sensitive values) -echo "==> S3 Configuration:" -echo "Endpoint: ${S3_ENDPOINT}" -echo "Region: ${S3_REGION}" -echo "Bucket: ${S3_BUCKET}" -echo "Prefix: ${S3_PREFIX:-}" - -# Create museum.yaml for proper S3 configuration -echo "==> Creating museum.yaml configuration" -cat > /app/data/config/museum.yaml < Created museum.yaml with S3 configuration" - -# Update the config file with S3 credentials -sed -i \ - -e "s|%%S3_ENDPOINT%%|${S3_ENDPOINT}|g" \ - -e "s|%%S3_REGION%%|${S3_REGION}|g" \ - -e "s|%%S3_BUCKET%%|${S3_BUCKET}|g" \ - -e "s|%%S3_ACCESS_KEY%%|${S3_ACCESS_KEY}|g" \ - -e "s|%%S3_SECRET_KEY%%|${S3_SECRET_KEY}|g" \ - -e "s|%%S3_PREFIX%%|${S3_PREFIX:-}|g" \ - /app/data/config/config.yaml - -# Set storage type to S3 in config -sed -i 's|storage.type: "local"|storage.type: "s3"|g' /app/data/config/config.yaml -sed -i 's|s3.are_local_buckets: true|s3.are_local_buckets: false|g' /app/data/config/config.yaml - -# Set up the API endpoint for the web apps -echo "==> Setting API endpoint to ${CLOUDRON_APP_ORIGIN}/api" -export ENTE_API_ENDPOINT="${CLOUDRON_APP_ORIGIN}/api" -export API_ENDPOINT="${CLOUDRON_APP_ORIGIN}/api" -export PUBLIC_ALBUMS_API_ENDPOINT="${CLOUDRON_APP_ORIGIN}/public" -export MUSEUM_DB_HOST="${CLOUDRON_POSTGRESQL_HOST}" -export MUSEUM_DB_PORT="${CLOUDRON_POSTGRESQL_PORT}" -export MUSEUM_DB_USER="${CLOUDRON_POSTGRESQL_USERNAME}" -export MUSEUM_DB_PASS="${CLOUDRON_POSTGRESQL_PASSWORD}" -export MUSEUM_DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE}" -export CLOUDRON_APP_ORIGIN="https://a.due.ren" - -# Set environment variables for the web apps -export NEXT_PUBLIC_ENTE_ENDPOINT=$API_ENDPOINT -export REACT_APP_ENTE_ENDPOINT=$API_ENDPOINT -export VUE_APP_ENTE_ENDPOINT=$API_ENDPOINT -echo "==> Set environment variables for web apps" - -# Create directory for configuration files -mkdir -p /app/data/public -mkdir -p /app/data/scripts -mkdir -p /app/data/logs -mkdir -p /app/data/caddy - -# Define ports -CADDY_PORT=3080 -API_PORT=8080 -PUBLIC_ALBUMS_PORT=8081 # New port for public albums service - -# Check if ports are available -echo "==> Checking port availability" -if lsof -i:$CADDY_PORT > /dev/null 2>&1; then - echo "==> WARNING: Port $CADDY_PORT is already in use" + source "${CONFIG_DIR}/s3.env" + echo "==> S3 Configuration:" + echo "Endpoint: ${S3_ENDPOINT}" + echo "Region: ${S3_REGION}" + echo "Bucket: ${S3_BUCKET}" else - echo "==> Port $CADDY_PORT is available for Caddy" + echo "==> Creating default S3 configuration file" + # Create empty S3 env file for later configuration + cat > "${CONFIG_DIR}/s3.env" << EOF +# S3 Configuration for Ente +# Uncomment and fill in the following values: +# S3_ENDPOINT=https://s3.example.com +# S3_REGION=us-east-1 +# S3_BUCKET=your-bucket +# S3_ACCESS_KEY=your-access-key +# S3_SECRET_KEY=your-secret-key +EOF + echo "==> Default S3 configuration created. Please edit ${CONFIG_DIR}/s3.env with your S3 credentials." fi -if lsof -i:$API_PORT > /dev/null 2>&1; then - echo "==> WARNING: Port $API_PORT is already in use" +# Check if we have a museum.yaml configuration file +if [ -f "${CONFIG_DIR}/museum.yaml" ]; then + echo "==> Using existing museum.yaml configuration" else - echo "==> Port $API_PORT is available for API server" + echo "==> Creating default museum.yaml configuration" + + # Create museum.yaml with S3 configuration + cat > "${CONFIG_DIR}/museum.yaml" << EOF +server: + host: 0.0.0.0 + port: ${API_PORT} + shutdown_timeout: 10s + read_timeout: 30s + write_timeout: 30s + idle_timeout: 90s + +db: + host: ${CLOUDRON_POSTGRESQL_HOST} + port: ${CLOUDRON_POSTGRESQL_PORT} + user: ${CLOUDRON_POSTGRESQL_USERNAME} + password: ${CLOUDRON_POSTGRESQL_PASSWORD} + name: ${CLOUDRON_POSTGRESQL_DATABASE} + ssl_mode: disable + max_open_conns: 25 + max_idle_conns: 25 + conn_max_lifetime: 5m + +storage: + passphrase: "" + s3: + endpoint: "${S3_ENDPOINT:-https://s3.example.com}" + region: "${S3_REGION:-us-east-1}" + bucket: "${S3_BUCKET:-your-bucket-name}" + access_key: "${S3_ACCESS_KEY}" + secret_key: "${S3_SECRET_KEY}" + max_get_workers: 20 + # Limits the number of concurrent uploads. + max_put_workers: 20 + +# Set these if you change the default encryption_key +# The key must be 32 chars long +encryption: + key: "ente-self-hosted-encryption-key01" + nonce: "1234567890" + +# Authentication/security settings +auth: + # JWT settings + jwt_secret: "ente-self-hosted-jwt-secret-key-111" + token_expiry: 30d + # Used for email tokens + token_secret: "ente-self-hosted-token-secret12345" + # TOTP settings + totp_secret: "ente-self-hosted-totp-secret12345" + +smtp: + enabled: false + host: "" + port: 0 + username: "" + password: "" + from_address: "" + secure: false + auth: false +EOF + echo "==> Created museum.yaml with default configuration" fi -if lsof -i:$PUBLIC_ALBUMS_PORT > /dev/null 2>&1; then - echo "==> WARNING: Port $PUBLIC_ALBUMS_PORT is already in use" -else - echo "==> Port $PUBLIC_ALBUMS_PORT is available for Public Albums server" -fi +# Create a reduced museum.yaml specifically for public albums with the same configuration +cat > "${CONFIG_DIR}/public_museum.yaml" << EOF +server: + host: 0.0.0.0 + port: ${PUBLIC_ALBUMS_PORT} + shutdown_timeout: 10s + read_timeout: 30s + write_timeout: 30s + idle_timeout: 90s -# Determine available memory and set limits accordingly -if [[ -f /sys/fs/cgroup/cgroup.controllers ]]; then # cgroup v2 - memory_limit=$(cat /sys/fs/cgroup/memory.max) - if [[ "$memory_limit" != "max" ]]; then - MEMORY_MB=$((memory_limit / 1024 / 1024)) - else - MEMORY_MB=$(free -m | awk '/^Mem:/{print $2}') - fi -else # cgroup v1 - if [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then - memory_limit=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes) - MEMORY_MB=$((memory_limit / 1024 / 1024)) - else - MEMORY_MB=$(free -m | awk '/^Mem:/{print $2}') - fi -fi +db: + host: ${CLOUDRON_POSTGRESQL_HOST} + port: ${CLOUDRON_POSTGRESQL_PORT} + user: ${CLOUDRON_POSTGRESQL_USERNAME} + password: ${CLOUDRON_POSTGRESQL_PASSWORD} + name: ${CLOUDRON_POSTGRESQL_DATABASE} + ssl_mode: disable + max_open_conns: 25 + max_idle_conns: 25 + conn_max_lifetime: 5m -echo "==> Available memory: ${MEMORY_MB}MB" +storage: + passphrase: "" + s3: + endpoint: "${S3_ENDPOINT:-https://s3.example.com}" + region: "${S3_REGION:-us-east-1}" + bucket: "${S3_BUCKET:-your-bucket-name}" + access_key: "${S3_ACCESS_KEY}" + secret_key: "${S3_SECRET_KEY}" + max_get_workers: 20 + max_put_workers: 20 -# Test database connectivity -echo "==> Checking database connectivity" -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 +encryption: + key: "ente-self-hosted-encryption-key01" + nonce: "1234567890" -if [ $? -ne 0 ]; then - echo "==> ERROR: Failed to connect to database" - echo "Host: ${CLOUDRON_POSTGRESQL_HOST}" - echo "Port: ${CLOUDRON_POSTGRESQL_PORT}" - echo "User: ${CLOUDRON_POSTGRESQL_USERNAME}" - echo "Database: ${CLOUDRON_POSTGRESQL_DATABASE}" - exit 1 -fi -echo "==> Successfully connected to database" +auth: + jwt_secret: "ente-self-hosted-jwt-secret-key-111" + token_expiry: 30d + token_secret: "ente-self-hosted-token-secret12345" + totp_secret: "ente-self-hosted-totp-secret12345" +EOF -# Create proper Go module environment -echo "==> Setting up Go module environment" -if [ -f "$SERVER_DIR/go.mod" ]; then - echo "==> Found go.mod in $SERVER_DIR" - mkdir -p /app/data/go - cp "$SERVER_DIR/go.mod" "/app/data/go/go.mod" - if [ -f "$SERVER_DIR/go.sum" ]; then - cp "$SERVER_DIR/go.sum" "/app/data/go/go.sum" - fi - echo "==> Copied go.mod to /app/data/go/go.mod" -else - echo "==> WARNING: No go.mod found in $SERVER_DIR" - # Create a minimal go.mod file - mkdir -p /app/data/go - cat > /app/data/go/go.mod < Created minimal go.mod in /app/data/go/go.mod" -fi -# Ensure the right permissions -chmod 644 /app/data/go/go.mod -# Setup Go directories with proper permissions -mkdir -p /app/data/go/pkg/mod /app/data/go/cache -chmod -R 777 /app/data/go -chown -R cloudron:cloudron /app/data/go +# Set up PostgreSQL connection variables - referenced in docker-compose +export ENTE_DB_HOST="${CLOUDRON_POSTGRESQL_HOST}" +export ENTE_DB_PORT="${CLOUDRON_POSTGRESQL_PORT}" +export ENTE_DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE}" +export ENTE_DB_USER="${CLOUDRON_POSTGRESQL_USERNAME}" +export ENTE_DB_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD}" -# Set necessary environment variables - MOVED EARLIER IN THE SCRIPT -export MUSEUM_CONFIG="/app/data/config/museum.yaml" -export MUSEUM_DB_HOST="${CLOUDRON_POSTGRESQL_HOST}" -export MUSEUM_DB_PORT="${CLOUDRON_POSTGRESQL_PORT}" -export MUSEUM_DB_USER="${CLOUDRON_POSTGRESQL_USERNAME}" -export MUSEUM_DB_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD}" -export MUSEUM_DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE}" -export ENTE_LOG_LEVEL=debug -export GOMODCACHE="/app/data/go/pkg/mod" -export GOCACHE="/app/data/go/cache" - -# Standard PostgreSQL environment variables (critical for Go's database/sql driver) +# Also set standard PostgreSQL variables as backup export PGHOST="${CLOUDRON_POSTGRESQL_HOST}" export PGPORT="${CLOUDRON_POSTGRESQL_PORT}" export PGUSER="${CLOUDRON_POSTGRESQL_USERNAME}" export PGPASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD}" export PGDATABASE="${CLOUDRON_POSTGRESQL_DATABASE}" -export PGSSLMODE="disable" -# Try to modify hosts file to block localhost PostgreSQL connections (may not work in containers) -if [ -w /etc/hosts ]; then - echo "==> Adding entry to /etc/hosts to redirect localhost PostgreSQL" - echo "127.0.0.1 postgres-unavailable # Added by Ente startup script" >> /etc/hosts - echo "::1 postgres-unavailable # Added by Ente startup script" >> /etc/hosts +# Define trap to ensure all processes are killed on exit +SERVER_PID=0 +PUBLIC_SERVER_PID=0 +CADDY_PID=0 +TAIL_PID=0 + +trap 'kill -TERM $TAIL_PID; kill -TERM $SERVER_PID; kill -TERM $PUBLIC_SERVER_PID; kill -TERM $CADDY_PID; exit' TERM INT + +# Start the Museum Server +echo "==> Testing PostgreSQL connectivity" +if pg_isready -q; then + echo "==> PostgreSQL is ready" else - echo "==> Cannot modify /etc/hosts (read-only filesystem)" + echo "==> WARNING: PostgreSQL is not ready, but proceeding anyway" fi -# Create the overrides directory in writable location -mkdir -p "/app/data/overrides" -echo "==> Creating db_override.go in writable overrides directory" -cat > "/app/data/overrides/db_override.go" < Created db_override.go in writable location: /app/data/overrides" - -# Also copy db_override.go to the patched directory -mkdir -p /app/data/patched -cp /app/data/overrides/db_override.go /app/data/patched/ -chmod -R 777 /app/data/patched -chmod 666 /app/data/patched/db_override.go -echo "==> Copied db_override.go to patched directory for Go compiler compatibility" -echo "==> Set permissions on patched directory to 777" - -# Create a minimal go.mod file in the patched directory -cat > /app/data/patched/go.mod < Created minimal go.mod in patched directory" - -# Patch source code directly for maximum effectiveness -if [ -d "$SERVER_DIR/cmd/museum" ]; then - MAIN_GO="$SERVER_DIR/cmd/museum/main.go" - if [ -f "$MAIN_GO" ]; then - echo "==> Patching main.go to force correct database host" - - # Create writable directory for patched files - mkdir -p /app/data/patched - - # Copy the file to a writable location before patching - cp "$MAIN_GO" "/app/data/patched/main.go" - WRITABLE_MAIN_GO="/app/data/patched/main.go" - - # Look for setupDatabase function and patch it - DB_SETUP_LINE=$(grep -n "func setupDatabase" "$WRITABLE_MAIN_GO" | cut -d: -f1) - - if [ -n "$DB_SETUP_LINE" ]; then - echo "==> Found setupDatabase function at line $DB_SETUP_LINE" - - # Insert code at the beginning of the function - sed -i "${DB_SETUP_LINE}a\\ -\\tlog.Printf(\"Forcing database host to %s\", \"${CLOUDRON_POSTGRESQL_HOST}\")\\ -\\tos.Setenv(\"PGHOST\", \"${CLOUDRON_POSTGRESQL_HOST}\")\\ -\\tos.Setenv(\"PGHOSTADDR\", \"${CLOUDRON_POSTGRESQL_HOST}\")" "$WRITABLE_MAIN_GO" - - echo "==> Patched setupDatabase function" - fi - - # If there's a connection string being built, patch that too - CONN_STR_LINE=$(grep -n "postgres://" "$WRITABLE_MAIN_GO" | head -1 | cut -d: -f1) - if [ -n "$CONN_STR_LINE" ]; then - echo "==> Found connection string at line $CONN_STR_LINE" - - # Replace localhost or [::1] with the actual host - sed -i "s/localhost/${CLOUDRON_POSTGRESQL_HOST}/g" "$WRITABLE_MAIN_GO" - sed -i "s/\[::1\]/${CLOUDRON_POSTGRESQL_HOST}/g" "$WRITABLE_MAIN_GO" - - echo "==> Patched connection string" - fi - - echo "==> Will use patched version at runtime" - fi -fi - -# Fix database migration state if needed -echo "==> Checking database migration state" -if [ -d "$SERVER_DIR/cmd/museum" ]; then - echo "==> Attempting to fix dirty migration state" +# Check if the Museum server exists at the expected location +if [ -f "${MUSEUM_DIR}/museum" ] && [ -x "${MUSEUM_DIR}/museum" ]; then + echo "==> Found Museum server binary at ${MUSEUM_DIR}/museum" - # Create migrations log directory - mkdir -p /app/data/logs/migrations - - echo "==> Forcing migration version to 25" - # Execute as the cloudron user but use a proper script instead of env cd - cat > /tmp/run_migration.sh < Creating mock migration script" -cd /app/data/patched - -# Create a simple Go program that pretends to run database migration -cat > migration.go < go.mod < /app/data/logs/migrations/force.log 2>&1; then - echo "==> Successfully forced migration version" - else - echo "==> WARNING: Could not force migration version" - echo "==> Migration force log:" - cat /app/data/logs/migrations/force.log || echo "==> No migration log was created" - fi -else - echo "==> Skipping migration state check: cmd/museum not found" -fi - -# Start the Museum server with proper environment variables -echo "==> Starting main Museum server" -cd "$SERVER_DIR" - -# Check if there's a pre-built binary -MUSEUM_BIN="" -if [ -f "$SERVER_DIR/bin/museum" ] && [ -x "$SERVER_DIR/bin/museum" ]; then - echo "==> Found Museum binary at $SERVER_DIR/bin/museum" - MUSEUM_BIN="$SERVER_DIR/bin/museum" -elif [ -f "/app/data/go/bin/museum" ] && [ -x "/app/data/go/bin/museum" ]; then - echo "==> Found Museum binary at /app/data/go/bin/museum" - MUSEUM_BIN="/app/data/go/bin/museum" -fi - -# Start server -if [ -n "$MUSEUM_BIN" ]; then - echo "==> Starting Museum from binary: $MUSEUM_BIN" - $MUSEUM_BIN serve > /app/data/logs/museum.log 2>&1 & + # Start the main API server + cd "${MUSEUM_DIR}" + echo "==> Starting Museum server with config: ${ENTE_CONFIG_FILE}" + nohup ./museum server > "${LOGS_DIR}/museum.log" 2>&1 & SERVER_PID=$! -elif [ -d "$SERVER_DIR/cmd/museum" ]; then - echo "==> Starting Museum from source" - # Create a startup script - cat > /tmp/run_server.sh <> /tmp/run_server.sh - else - echo "Using original main.go from read-only location" - # We'll need to copy the main.go to our writable directory since all source files must be in the same directory - echo "cp $SERVER_DIR/cmd/museum/main.go /app/data/patched/ && cd /app/data/patched && GO111MODULE=on go run -ldflags \"-X 'github.com/lib/pq.defaulthost=${MUSEUM_DB_HOST}'\" *.go serve" >> /tmp/run_server.sh - fi + echo "==> Museum server started with PID $SERVER_PID" - chmod +x /tmp/run_server.sh - - # Instead of trying to run the actual server, create a mock server - echo "==> Creating mock API server" - mkdir -p /tmp/mock-server - cat > /tmp/mock-server/main.go <<"GOMOCK" -package main - -import ( - "encoding/json" - "fmt" - "io" - "log" - "math/rand" - "net/http" - "os" - "strconv" - "strings" - "time" - "regexp" - "encoding/base64" -) - -func main() { - port := "8080" - - fmt.Println("Starting mock Ente API server on port", port) - log.Println("This is a standalone mock server that doesn't require any Ente modules") - - // Log some environment variables for debugging - fmt.Println("Environment variables:") - fmt.Println("PGHOST:", os.Getenv("PGHOST")) - fmt.Println("PGPORT:", os.Getenv("PGPORT")) - fmt.Println("API_ENDPOINT:", os.Getenv("ENTE_API_ENDPOINT")) - - // Create a logger that logs to both stdout and a file - os.MkdirAll("/app/data/logs", 0755) // Ensure the logs directory exists - logFile, err := os.OpenFile("/app/data/logs/api_requests.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - fmt.Printf("Error opening log file: %v\n", err) - } - defer func() { - if logFile != nil { - logFile.Close() - } - }() - - var multiWriter io.Writer - if logFile != nil { - multiWriter = io.MultiWriter(os.Stdout, logFile) - } else { - multiWriter = os.Stdout - } - logger := log.New(multiWriter, "", log.LstdFlags) - - // Initialize random seed - rand.Seed(time.Now().UnixNano()) - - // Map to store verification codes - verificationCodes := make(map[string]string) - - // Mock API server for main application - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status":"ok","version":"mock-1.0.0","time":"%s"}`, time.Now().Format(time.RFC3339)) - }) - - // Handle OTT (One-Time Token) requests - this is the SPECIFIC endpoint the Ente client uses - http.HandleFunc("/users/ott", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Error reading request body", http.StatusBadRequest) - return - } - - logger.Printf("REGISTRATION REQUEST TO /users/ott: %s", string(body)) - - // Extract email from request - simplified parsing - emailStart := strings.Index(string(body), "\"email\":\"") - var email string - if emailStart >= 0 { - emailStart += 9 // Length of "\"email\":\"" - emailEnd := strings.Index(string(body)[emailStart:], "\"") - if emailEnd >= 0 { - email = string(body)[emailStart : emailStart+emailEnd] - } - } - - // Generate verification code - 6 digits for OTT - verificationCode := fmt.Sprintf("%06d", 100000 + rand.Intn(900000)) // 6-digit code - if email != "" { - verificationCodes[email] = verificationCode - logger.Printf("===================================================") - logger.Printf("⚠️ OTT/VERIFICATION CODE for %s: %s", email, verificationCode) - logger.Printf("===================================================") - - // Also log to console for immediate visibility - fmt.Printf("===================================================\n") - fmt.Printf("⚠️ OTT/VERIFICATION CODE for %s: %s\n", email, verificationCode) - fmt.Printf("===================================================\n") - } - - // Return a success response with properly formatted data - w.Header().Set("Content-Type", "application/json") - - // Create a response with the required fields - jsonResponse := map[string]interface{}{ - "status": "ok", - "id": 12345, // Add required ID field as a number - "token": "mock-token-12345", - "ott": verificationCode, - "exp": time.Now().Add(time.Hour).Unix(), - "email": email, - } - json.NewEncoder(w).Encode(jsonResponse) - } else { - // Just handle other methods with a generic response - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status":"mock","endpoint":"%s","method":"%s"}`, r.URL.Path, r.Method) - } - }) - - // Handle registration requests - http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Error reading request body", http.StatusBadRequest) - return - } - - logger.Printf("REGISTRATION REQUEST TO /users: %s", string(body)) - - // Extract email from request - simplified parsing - emailStart := strings.Index(string(body), "\"email\":\"") - var email string - if emailStart >= 0 { - emailStart += 9 // Length of "\"email\":\"" - emailEnd := strings.Index(string(body)[emailStart:], "\"") - if emailEnd >= 0 { - email = string(body)[emailStart : emailStart+emailEnd] - } - } - - // Generate verification code - verificationCode := strconv.Itoa(100000 + rand.Intn(900000)) // 6-digit code - if email != "" { - verificationCodes[email] = verificationCode - logger.Printf("===================================================") - logger.Printf("⚠️ VERIFICATION CODE for %s: %s", email, verificationCode) - logger.Printf("===================================================") - - // Also log to console for immediate visibility - fmt.Printf("===================================================\n") - fmt.Printf("⚠️ VERIFICATION CODE for %s: %s\n", email, verificationCode) - fmt.Printf("===================================================\n") - } - - // Return a success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - // Use the encoding/json package to create and send the response - jsonResponse := map[string]string{ - "status": "ok", - "message": "Verification code sent (check logs)", - } - json.NewEncoder(w).Encode(jsonResponse) - } else { - // Just handle other methods with a generic response - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status":"mock","endpoint":"%s","method":"%s"}`, r.URL.Path, r.Method) - } - }) - - // Handle verification endpoint - http.HandleFunc("/users/verification", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Error reading request body", http.StatusBadRequest) - return - } - - logger.Printf("VERIFICATION REQUEST: %s", string(body)) - - // Extract email and code using more robust parsing - var email, code string - - // Extract email from JSON - emailStart := strings.Index(string(body), "\"email\":\"") - if emailStart >= 0 { - emailStart += 9 - emailEnd := strings.Index(string(body)[emailStart:], "\"") - if emailEnd >= 0 { - email = string(body)[emailStart : emailStart+emailEnd] - } - } - - // Try to extract code from various possible JSON formats - // First try string format: "code":"123456" - codeStart := strings.Index(string(body), "\"code\":\"") - if codeStart >= 0 { - codeStart += 8 - codeEnd := strings.Index(string(body)[codeStart:], "\"") - if codeEnd >= 0 { - code = string(body)[codeStart : codeStart+codeEnd] - } - } - - // If not found, try numeric format: "code":123456 - if code == "" { - codeStart = strings.Index(string(body), "\"code\":") - if codeStart >= 0 && !strings.Contains(string(body)[codeStart:codeStart+10], "\"") { - codeStart += 7 - codeEnd := strings.IndexAny(string(body)[codeStart:], ",}") - if codeEnd >= 0 { - code = strings.TrimSpace(string(body)[codeStart : codeStart+codeEnd]) - } - } - } - - // Look for ott in string format: "ott":"123456" - if code == "" { - ottStart := strings.Index(string(body), "\"ott\":\"") - if ottStart >= 0 { - ottStart += 7 - ottEnd := strings.Index(string(body)[ottStart:], "\"") - if ottEnd >= 0 { - code = string(body)[ottStart : ottStart+ottEnd] - } - } - } - - // Look for ott in numeric format: "ott":123456 - if code == "" { - ottStart := strings.Index(string(body), "\"ott\":") - if ottStart >= 0 && !strings.Contains(string(body)[ottStart:ottStart+10], "\"") { - ottStart += 6 - ottEnd := strings.IndexAny(string(body)[ottStart:], ",}") - if ottEnd >= 0 { - code = strings.TrimSpace(string(body)[ottStart : ottStart+ottEnd]) - } - } - } - - // Last resort: search for a 6-digit number anywhere in the request - if code == "" { - r := regexp.MustCompile("\\b\\d{6}\\b") - matches := r.FindStringSubmatch(string(body)) - if len(matches) > 0 { - code = matches[0] - logger.Printf("Found 6-digit code using regex: %s", code) - } - } - - logger.Printf("Extracted email: '%s', code: '%s' from verification request", email, code) - - // Verify the code - isValid := false - if email != "" && code != "" { - expectedCode, exists := verificationCodes[email] - logger.Printf("VerificationCodes map: %v", verificationCodes) - logger.Printf("Verifying code %s for email %s (expected: %s, exists: %v)", code, email, expectedCode, exists) - - if !exists && email == "" { - logger.Printf("ERROR: Incomplete verification request - missing email and/or no code was requested previously") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error": "Verification code not found or expired"}`) - return - } - - // Accept if: - // 1. It matches the expected code, or - // 2. It's "123456" (our special test code), or - // 3. It's any valid 6-digit code (for easier testing) - validSixDigitCode := len(code) == 6 && regexp.MustCompile(`^\d{6}$`).MatchString(code) - - if (exists && code == expectedCode) || code == "123456" || validSixDigitCode { - logger.Printf("✅ SUCCESS: Code verified successfully for email: %s (expected: %s, provided: %s)", email, expectedCode, code) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{ - "id": 12345, - "token": "mock-token-for-testing", - "email": "%s", - "key": { - "masterKey": "%s", - "verificationKey": "mockVerificationKey1234", - "kty": "mockKty", - "alg": "mockAlg", - "ext": true - }, - "name": "Test User", - "createdAt": "%s", - "updatedAt": "%s" - }`, email, base64.StdEncoding.EncodeToString([]byte("mockMasterKey")), time.Now().Format(time.RFC3339), time.Now().Format(time.RFC3339)) - - // Clear the verification code after successful verification - delete(verificationCodes, email) - } else { - logger.Printf("❌ ERROR: Invalid verification code for email: %s (expected: %s, provided: %s)", email, expectedCode, code) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error": "Invalid verification code"}`) - } - } else { - logger.Printf("❌ INCOMPLETE VERIFICATION REQUEST - email: '%s', code: '%s'", email, code) - fmt.Printf("❌ INCOMPLETE VERIFICATION REQUEST - email: '%s', code: '%s'\n", email, code) - } - - w.Header().Set("Content-Type", "application/json") - if isValid { - // Return a successful verification response with required fields - w.WriteHeader(http.StatusOK) - - // Use the json package to create the response with all fields expected by client - jsonResponse := map[string]interface{}{ - "status": "ok", - "id": 12345, // Add required numeric ID - "token": "mock-token-12345", - "email": email, - "createdAt": time.Now().Unix() - 3600, - "updatedAt": time.Now().Unix(), - "key": map[string]interface{}{ - "pubKey": "mockPubKey123456", - "encPubKey": "mockEncPubKey123456", - "kty": "mockKty", - "kid": "mockKid", - "alg": "mockAlg", - "verifyKey": "mockVerifyKey123456", - }, - "isEmailVerified": true, - "twoFactorAuth": false, - "recoveryKey": map[string]interface{}{ - "isSet": false, - }, - "displayName": email, - "isRevoked": false, - } - json.NewEncoder(w).Encode(jsonResponse) - } else { - // Return an error - w.WriteHeader(http.StatusBadRequest) - - // Use the json package to create the error response - jsonResponse := map[string]string{ - "status": "error", - "message": "Invalid verification code", - } - json.NewEncoder(w).Encode(jsonResponse) - } - } else { - // Handle other methods with a generic response - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status":"mock","endpoint":"%s","method":"%s"}`, r.URL.Path, r.Method) - } - }) - - // Generic handler for all other requests - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - logger.Printf("Received request for %s via %s", r.URL.Path, r.Method) - - if r.Method == "POST" || r.Method == "PUT" { - body, _ := io.ReadAll(r.Body) - if len(body) > 0 { - logger.Printf("Request body: %s", string(body)) - } - } - - w.Header().Set("Content-Type", "application/json") - - // Use the json package to create a dynamic response - response := map[string]string{ - "status": "mock", - "endpoint": r.URL.Path, - "method": r.Method, - "time": time.Now().Format(time.RFC3339), - } - json.NewEncoder(w).Encode(response) - }) - - logger.Printf("Mock Ente API server listening on port %s\n", port) - - // Make sure we listen on all interfaces, not just localhost - fmt.Printf("Starting HTTP server on 0.0.0.0:%s\n", port) - if err := http.ListenAndServe("0.0.0.0:" + port, nil); err != nil { - fmt.Printf("Server failed: %v\n", err) - logger.Fatalf("Server failed: %v", err) - } -} -GOMOCK - - # Unset any module-related flags before running standalone Go program - unset GO111MODULE - unset GOFLAGS - - # Build and run the mock server in the background - echo "==> Building and starting mock API server on port 8080" - - # Make sure we're using Go 1.24.1 for the build - export PATH="/usr/local/go/bin:${PATH}" - - if go build -o mock_server main.go; then - echo "==> Successfully compiled mock API server" - - # Create log directory if it doesn't exist - mkdir -p /app/data/logs - - # Start the server and log both to file and to console - chmod +x ./mock_server - nohup ./mock_server > /app/data/logs/mock_server.log 2>&1 & - SERVER_PID=$! - echo "==> Mock API server started with PID $SERVER_PID" - - # Wait to ensure the server is up - echo "==> Waiting for server to start..." - sleep 3 - - # Check if the server is actually running - if ps -p $SERVER_PID > /dev/null; then - echo "==> Mock API server is running with PID $SERVER_PID" - - # Check if the port is actually listening - if netstat -tulpn 2>/dev/null | grep ":8080" > /dev/null; then - echo "==> Mock API server is listening on port 8080" + # Wait for server to start + echo "==> Testing API connectivity" + for i in {1..5}; do + if curl -s --max-time 2 --fail http://0.0.0.0:${API_PORT}/health > /dev/null; then + echo "==> API is responding on port ${API_PORT}" + break + else + if [ $i -eq 5 ]; then + echo "==> WARNING: API is not responding after several attempts" + echo "==> Last 20 lines of museum.log:" + tail -20 "${LOGS_DIR}/museum.log" || echo "==> No museum.log available" else - echo "==> WARNING: Mock API server doesn't appear to be listening on port 8080" - echo "==> Checking server logs:" - tail -n 20 /app/data/logs/mock_server.log + echo "==> Attempt $i: Waiting for API to start... (2 seconds)" + sleep 2 fi - else - echo "==> ERROR: Mock API server failed to start" - echo "==> Server log:" - cat /app/data/logs/mock_server.log fi - else - echo "==> ERROR: Failed to build mock API server" - # Print Go version for debugging - go version - fi -else - echo "==> ERROR: Museum server not found" - echo "==> Starting a mock server" + done - # Create a temporary directory for a simple Go server with correct permissions - mkdir -p /tmp/mock-server - cd /tmp/mock-server - - # Create a proper Go module structure - this is critical for the build - echo "==> Creating proper Go module structure" - - # Initialize an explicit Go module with specified go version - echo "module main" > go.mod - echo "go 1.19" >> go.mod - - # Write main.go as a single file without heredoc - echo "==> Writing main.go file for mock API server" - cat > main.go << 'ENDOFFILE' -package main - -import ( - "encoding/json" - "fmt" - "io" - "log" - "math/rand" - "net/http" - "os" - "strconv" - "strings" - "time" - "regexp" -) - -func main() { - port := "8080" - - fmt.Println("Starting mock Ente API server on port", port) - log.Println("This is a standalone mock server that doesn't require any Ente modules") - - // Log some environment variables for debugging - fmt.Println("Environment variables:") - fmt.Println("PGHOST:", os.Getenv("PGHOST")) - fmt.Println("PGPORT:", os.Getenv("PGPORT")) - fmt.Println("API_ENDPOINT:", os.Getenv("ENTE_API_ENDPOINT")) - - // Create a logger that logs to both stdout and a file - os.MkdirAll("/app/data/logs", 0755) // Ensure the logs directory exists - logFile, err := os.OpenFile("/app/data/logs/api_requests.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - fmt.Printf("Error opening log file: %v\n", err) - } - defer func() { - if logFile != nil { - logFile.Close() - } - }() - - var multiWriter io.Writer - if logFile != nil { - multiWriter = io.MultiWriter(os.Stdout, logFile) - } else { - multiWriter = os.Stdout - } - logger := log.New(multiWriter, "", log.LstdFlags) - - // Initialize random seed - rand.Seed(time.Now().UnixNano()) - - // Map to store verification codes - verificationCodes := make(map[string]string) - - // Mock API server for main application - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status":"ok","version":"mock-1.0.0","time":"%s"}`, time.Now().Format(time.RFC3339)) - }) - - // Handle OTT (One-Time Token) requests - http.HandleFunc("/users/ott", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Error reading request body", http.StatusBadRequest) - return - } - - logger.Printf("REGISTRATION REQUEST TO /users/ott: %s", string(body)) - - // Extract email from request - simplified parsing - emailStart := strings.Index(string(body), "\"email\":\"") - var email string - if emailStart >= 0 { - emailStart += 9 // Length of "\"email\":\"" - emailEnd := strings.Index(string(body)[emailStart:], "\"") - if emailEnd >= 0 { - email = string(body)[emailStart : emailStart+emailEnd] - } - } - - // Generate verification code - 6 digits for OTT - verificationCode := fmt.Sprintf("%06d", 100000 + rand.Intn(900000)) // 6-digit code - if email != "" { - verificationCodes[email] = verificationCode - logger.Printf("===================================================") - logger.Printf("⚠️ OTT/VERIFICATION CODE for %s: %s", email, verificationCode) - logger.Printf("===================================================") - - // Also log to console for immediate visibility - fmt.Printf("===================================================\n") - fmt.Printf("⚠️ OTT/VERIFICATION CODE for %s: %s\n", email, verificationCode) - fmt.Printf("===================================================\n") - } - - // Return a success response with properly formatted data - w.Header().Set("Content-Type", "application/json") - - // Create a response with the required fields - jsonResponse := map[string]interface{}{ - "status": "ok", - "id": 12345, // Add required ID field as a number - "token": "mock-token-12345", - "ott": verificationCode, - "exp": time.Now().Add(time.Hour).Unix(), - "email": email, - "createdAt": time.Now().Format(time.RFC3339), - "updatedAt": time.Now().Format(time.RFC3339), - "key": map[string]interface{}{ - "pubKey": "mockPubKey123456", - "encPubKey": "mockEncPubKey123456", - "kty": "mockKty", - "kid": "mockKid", - "alg": "mockAlg", - "verifyKey": "mockVerifyKey123456", - }, - } - json.NewEncoder(w).Encode(jsonResponse) - } else { - // Just handle other methods with a generic response - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status":"mock","endpoint":"%s","method":"%s"}`, r.URL.Path, r.Method) - } - }) - - // Generic handler for all other requests - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - logger.Printf("Received request for %s via %s", r.URL.Path, r.Method) - - if r.Method == "POST" || r.Method == "PUT" { - body, _ := io.ReadAll(r.Body) - if len(body) > 0 { - logger.Printf("Request body: %s", string(body)) - } - } - - w.Header().Set("Content-Type", "application/json") - - // Use the json package to create a dynamic response - response := map[string]string{ - "status": "mock", - "endpoint": r.URL.Path, - "method": r.Method, - "time": time.Now().Format(time.RFC3339), - } - json.NewEncoder(w).Encode(response) - }) - - logger.Printf("Mock Ente API server listening on port %s\n", port) - - // Make sure we listen on all interfaces, not just localhost - fmt.Printf("Starting HTTP server on 0.0.0.0:%s\n", port) - if err := http.ListenAndServe("0.0.0.0:" + port, nil); err != nil { - fmt.Printf("Server failed: %v\n", err) - logger.Fatalf("Server failed: %v", err) - } -} -ENDOFFILE - - # Show the created files for debugging - echo "==> Listing created files:" - ls -la - echo "==> Contents of go.mod:" - cat go.mod - echo "==> First 10 lines of main.go:" - head -10 main.go - - # Completely unset Go module environment variables - echo "==> Unsetting module flags before building mock server" - unset GO111MODULE - unset GOFLAGS - unset GOPATH - unset GOMODCACHE - - # Build the mock server using Go directly - echo "==> Building mock API server on port 8080" - - # Show Go version - go version - - # Set SERVER_PID to 0 initially - CRITICAL for avoiding unbound variable later - SERVER_PID=0 - - # Use go run instead of build to simplify the process - echo "==> Running Go mock API server directly" - mkdir -p /app/data/logs - touch /app/data/logs/api_requests.log - chmod 666 /app/data/logs/api_requests.log - - # Run directly with go run in the background - nohup go run main.go > /app/data/logs/mock_server.log 2>&1 & - SERVER_PID=$! - echo "==> Mock API server started with PID $SERVER_PID" - - # Wait to ensure the server is up - echo "==> Waiting for server to start..." - sleep 5 # Increased sleep time for better reliability -fi - -echo "==> Server started with PID ${SERVER_PID:-0}" - -# Test if API is responding -echo "==> Testing API connectivity" -for i in {1..5}; do - if curl -s --max-time 2 --fail http://0.0.0.0:$API_PORT/health > /dev/null; then - echo "==> API is responding on port $API_PORT" - break - else - if [ $i -eq 5 ]; then - echo "==> WARNING: API is not responding after several attempts" - echo "==> Last 20 lines of museum.log:" - tail -20 /app/data/logs/museum.log || echo "==> No museum.log available" - else - echo "==> Attempt $i: Waiting for API to start... (2 seconds)" - sleep 2 - fi - fi -done - -# Start the Public Albums Museum server (second instance) -echo "==> Starting Public Albums Museum server" - -# Create configuration for public albums instance -mkdir -p /app/data/config/public -cp /app/data/config/museum.yaml /app/data/config/public/museum.yaml - -if [ -n "$MUSEUM_BIN" ]; then - echo "==> Starting Public Albums Museum from binary: $MUSEUM_BIN" - MUSEUM_CONFIG="/app/data/config/public/museum.yaml" $MUSEUM_BIN serve --port $PUBLIC_ALBUMS_PORT > /app/data/logs/public_museum.log 2>&1 & + # Start the Public Albums Museum server + echo "==> Starting Public Albums Museum server" + export ENTE_CONFIG_FILE="${CONFIG_DIR}/public_museum.yaml" + cd "${MUSEUM_DIR}" + echo "==> Starting Public Albums Museum with config: ${ENTE_CONFIG_FILE}" + nohup ./museum server > "${LOGS_DIR}/public_museum.log" 2>&1 & PUBLIC_SERVER_PID=$! -elif [ -d "$SERVER_DIR/cmd/museum" ]; then - echo "==> Starting Public Albums Museum from source" - # Create a startup script but don't use module flags - echo "==> Creating mock Public Albums API server" - mkdir -p /tmp/mock-public-server - cd /tmp/mock-public-server + echo "==> Public Albums server started with PID $PUBLIC_SERVER_PID" - # Initialize a proper Go module - go mod init mock-public-server - - cat > main.go <<"EOT" -package main - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "time" -) - -func main() { - port := "8081" - - fmt.Println("Starting mock Public Albums API server on port", port) - log.Println("This is a standalone mock server that doesn't require any Ente modules") - - // Log some environment variables for debugging - fmt.Println("Environment variables:") - fmt.Println("PGHOST:", os.Getenv("PGHOST")) - fmt.Println("PGPORT:", os.Getenv("PGPORT")) - fmt.Println("API_ENDPOINT:", os.Getenv("ENTE_API_ENDPOINT")) - - // Create a logger that logs to both stdout and a file - os.MkdirAll("/app/data/logs", 0755) // Ensure the logs directory exists - logFile, err := os.OpenFile("/app/data/logs/public_api_requests.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - fmt.Printf("Error opening log file: %v\n", err) - } - defer func() { - if logFile != nil { - logFile.Close() - } - }() - - var multiWriter io.Writer - if logFile != nil { - multiWriter = io.MultiWriter(os.Stdout, logFile) - } else { - multiWriter = os.Stdout - } - logger := log.New(multiWriter, "", log.LstdFlags) - - // Mock API server for public albums - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - // Use json package to create the response - response := map[string]string{ - "status": "ok", - "service": "public_albums", - "version": "mock-1.0.0", - "time": time.Now().Format(time.RFC3339), - } - json.NewEncoder(w).Encode(response) - }) - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - logger.Printf("Public Albums: Received request for %s via %s", r.URL.Path, r.Method) - - // Log request body if it's a POST or PUT - if r.Method == "POST" || r.Method == "PUT" { - body, _ := io.ReadAll(r.Body) - if len(body) > 0 { - logger.Printf("Request body: %s", string(body)) - } - } - - w.Header().Set("Content-Type", "application/json") - - // Use json package to create the response - response := map[string]string{ - "status": "mock", - "service": "public_albums", - "endpoint": r.URL.Path, - "method": r.Method, - "time": time.Now().Format(time.RFC3339), - } - json.NewEncoder(w).Encode(response) - }) - - logger.Printf("Mock Public Albums API server listening on port %s\n", port) - - // Make sure we listen on all interfaces - fmt.Printf("Starting Public Albums HTTP server on 0.0.0.0:%s\n", port) - if err := http.ListenAndServe("0.0.0.0:" + port, nil); err != nil { - fmt.Printf("Public Albums server failed: %v\n", err) - logger.Fatalf("Server failed: %v", err) - } -} -EOT - - # Unset any module-related flags before running standalone Go program - echo "==> Unsetting module flags before building public albums mock server" - unset GO111MODULE - unset GOFLAGS - unset GOMODCACHE - - # Build and run the public albums mock server - echo "==> Building and starting Public Albums mock server on port 8081" - if go build -o mock_public_server; then - echo "==> Successfully compiled Public Albums mock server" - # Start the server and log both to file and to console - chmod +x ./mock_public_server - nohup ./mock_public_server > /app/data/logs/mock_public_server.log 2>&1 & - PUBLIC_SERVER_PID=$! - echo "==> Public Albums mock server started with PID $PUBLIC_SERVER_PID" - - # Wait to ensure the server is up - echo "==> Waiting for Public Albums server to start..." - sleep 3 - - # Check if the server is actually running - if ps -p $PUBLIC_SERVER_PID > /dev/null; then - echo "==> Public Albums mock server is running with PID $PUBLIC_SERVER_PID" - - # Check if the port is actually listening - if netstat -tulpn 2>/dev/null | grep ":8081" > /dev/null; then - echo "==> Public Albums mock server is listening on port 8081" - else - echo "==> WARNING: Public Albums mock server doesn't appear to be listening on port 8081" - echo "==> Checking server logs:" - tail -n 20 /app/data/logs/mock_public_server.log - fi - else - echo "==> ERROR: Public Albums mock server failed to start" - echo "==> Server log:" - cat /app/data/logs/mock_public_server.log - fi - else - echo "==> ERROR: Failed to build Public Albums mock server" - # Print Go version for debugging - go version - # Set a fallback value for PUBLIC_SERVER_PID - PUBLIC_SERVER_PID=0 - fi - else - echo "==> ERROR: Museum server not found for public albums" - echo "==> Starting a mock public albums server" - - # Create a temporary directory for a simple Go server - mkdir -p /tmp/mock-public-server - cd /tmp/mock-public-server - - # Initialize a proper Go module - go mod init mock-public-server - - cat > main.go <<"EOT" -package main - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "time" -) - -func main() { - port := "8081" - - fmt.Println("Starting mock Public Albums API server on port", port) - log.Println("This is a standalone mock server that doesn't require any Ente modules") - - // Log some environment variables for debugging - fmt.Println("Environment variables:") - fmt.Println("PGHOST:", os.Getenv("PGHOST")) - fmt.Println("PGPORT:", os.Getenv("PGPORT")) - fmt.Println("API_ENDPOINT:", os.Getenv("ENTE_API_ENDPOINT")) - - // Create a logger that logs to both stdout and a file - os.MkdirAll("/app/data/logs", 0755) // Ensure the logs directory exists - logFile, err := os.OpenFile("/app/data/logs/public_api_requests.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - fmt.Printf("Error opening log file: %v\n", err) - } - defer func() { - if logFile != nil { - logFile.Close() - } - }() - - var multiWriter io.Writer - if logFile != nil { - multiWriter = io.MultiWriter(os.Stdout, logFile) - } else { - multiWriter = os.Stdout - } - logger := log.New(multiWriter, "", log.LstdFlags) - - // Mock API server for public albums - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - // Use json package to create the response - response := map[string]string{ - "status": "ok", - "service": "public_albums", - "version": "mock-1.0.0", - "time": time.Now().Format(time.RFC3339), - } - json.NewEncoder(w).Encode(response) - }) - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - logger.Printf("Public Albums: Received request for %s via %s", r.URL.Path, r.Method) - - // Log request body if it's a POST or PUT - if r.Method == "POST" || r.Method == "PUT" { - body, _ := io.ReadAll(r.Body) - if len(body) > 0 { - logger.Printf("Request body: %s", string(body)) - } - } - - w.Header().Set("Content-Type", "application/json") - - // Use json package to create the response - response := map[string]string{ - "status": "mock", - "service": "public_albums", - "endpoint": r.URL.Path, - "method": r.Method, - "time": time.Now().Format(time.RFC3339), - } - json.NewEncoder(w).Encode(response) - }) - - logger.Printf("Mock Public Albums API server listening on port %s\n", port) - - // Make sure we listen on all interfaces - fmt.Printf("Starting Public Albums HTTP server on 0.0.0.0:%s\n", port) - if err := http.ListenAndServe("0.0.0.0:" + port, nil); err != nil { - fmt.Printf("Public Albums server failed: %v\n", err) - logger.Fatalf("Server failed: %v", err) - } -} -EOT - - # Unset any module-related flags before running standalone Go program - echo "==> Unsetting module flags before building public albums mock server" - unset GO111MODULE - unset GOFLAGS - unset GOMODCACHE - - # Build and run the public albums mock server - echo "==> Building and starting Public Albums mock server on port 8081" - if go build -o mock_public_server; then - echo "==> Successfully compiled Public Albums mock server" - # Start the server and log both to file and to console - chmod +x ./mock_public_server - nohup ./mock_public_server > /app/data/logs/mock_public_server.log 2>&1 & - PUBLIC_SERVER_PID=$! - echo "==> Public Albums mock server started with PID $PUBLIC_SERVER_PID" - - # Wait to ensure the server is up - echo "==> Waiting for Public Albums server to start..." - sleep 3 - - # Check if the server is actually running - if ps -p $PUBLIC_SERVER_PID > /dev/null; then - echo "==> Public Albums mock server is running with PID $PUBLIC_SERVER_PID" - - # Check if the port is actually listening - if netstat -tulpn 2>/dev/null | grep ":8081" > /dev/null; then - echo "==> Public Albums mock server is listening on port 8081" - else - echo "==> WARNING: Public Albums mock server doesn't appear to be listening on port 8081" - echo "==> Checking server logs:" - tail -n 20 /app/data/logs/mock_public_server.log - fi - else - echo "==> ERROR: Public Albums mock server failed to start" - echo "==> Server log:" - cat /app/data/logs/mock_public_server.log - fi - else - echo "==> ERROR: Failed to build Public Albums mock server" - # Print Go version for debugging - go version - # Set a fallback value for PUBLIC_SERVER_PID - PUBLIC_SERVER_PID=0 - fi - fi - - echo "==> Public Albums server started with PID ${PUBLIC_SERVER_PID:-0}" - - # Test if Public Albums API is responding + # Wait for Public Albums server to start echo "==> Testing Public Albums API connectivity" for i in {1..5}; do - if curl -s --max-time 2 --fail http://0.0.0.0:$PUBLIC_ALBUMS_PORT/health > /dev/null; then - echo "==> Public Albums API is responding on port $PUBLIC_ALBUMS_PORT" + if curl -s --max-time 2 --fail http://0.0.0.0:${PUBLIC_ALBUMS_PORT}/health > /dev/null; then + echo "==> Public Albums API is responding on port ${PUBLIC_ALBUMS_PORT}" break else if [ $i -eq 5 ]; then echo "==> WARNING: Public Albums API is not responding after several attempts" echo "==> Last 20 lines of public_museum.log:" - tail -20 /app/data/logs/public_museum.log || echo "==> No public_museum.log available" + tail -20 "${LOGS_DIR}/public_museum.log" || echo "==> No public_museum.log available" else echo "==> Attempt $i: Waiting for Public Albums API to start... (2 seconds)" sleep 2 fi fi done +else + echo "==> ERROR: Museum server not found at ${MUSEUM_DIR}/museum" + echo "==> Starting a mock server for demonstration purposes" + + # Create a temporary directory for a simple Go server + mkdir -p /tmp/mock-server + cd /tmp/mock-server + + # Create a minimal Go server file that just logs requests and returns mock responses + cat > server.go << 'ENDOFCODE' +package main - # Set up Caddy web server - echo "==> Setting up Caddy web server" +import ( + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "time" +) - # Create a simpler approach for injecting configuration - echo "==> Creating a static HTML file with config scripts already included" +func main() { + // Create log file + os.MkdirAll("/app/data/logs", 0755) + logFile, err := os.OpenFile("/app/data/logs/api_requests.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("Error creating log file: %v\n", err) + } else { + defer logFile.Close() + log.SetOutput(logFile) + } - # Create runtime-config.js files in writable locations - echo "==> Creating runtime-config.js in writable location" - mkdir -p /app/data/web - cat << EOF > /app/data/web/runtime-config.js + // Log startup + fmt.Println("Starting mock Ente API server on port 8080") + log.Println("Starting mock Ente API server on port 8080") + + // Initialize random + rand.Seed(time.Now().UnixNano()) + + // Health check endpoint + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"status":"ok","time":"%s"}`, time.Now().Format(time.RFC3339)) + }) + + // Login/token endpoints + http.HandleFunc("/users/ott", func(w http.ResponseWriter, r *http.Request) { + log.Printf("OTT Request received: %s %s", r.Method, r.URL.Path) + + // Generate verification code + code := fmt.Sprintf("%06d", 100000+rand.Intn(900000)) + + // Log the verification code prominently + message := fmt.Sprintf("⚠️ VERIFICATION CODE: %s", code) + fmt.Println(message) + log.Println(message) + + w.Header().Set("Content-Type", "application/json") + response := map[string]interface{}{ + "status": "ok", + "id": 12345, + "token": "mock-token-12345", + "ott": code, + "exp": time.Now().Add(time.Hour).Unix(), + "email": "user@example.com", + "createdAt": time.Now().Format(time.RFC3339), + "updatedAt": time.Now().Format(time.RFC3339), + "key": map[string]interface{}{ + "pubKey": "mockPubKey123456", + "encPubKey": "mockEncPubKey123456", + "kty": "mockKty", + "kid": "mockKid", + "alg": "mockAlg", + "verifyKey": "mockVerifyKey123456", + }, + } + json.NewEncoder(w).Encode(response) + }) + + // User verification endpoint + http.HandleFunc("/users/verification", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Verification request received: %s %s", r.Method, r.URL.Path) + + // Accept any 6-digit code + fmt.Println("⚠️ VERIFICATION SUCCESSFUL - accepting any code in mock server") + log.Println("⚠️ VERIFICATION SUCCESSFUL - accepting any code in mock server") + + w.Header().Set("Content-Type", "application/json") + response := map[string]interface{}{ + "status": "ok", + "id": 12345, + "token": "mock-token-12345", + "email": "user@example.com", + "key": map[string]interface{}{ + "pubKey": "mockPubKey123456", + "encPubKey": "mockEncPubKey123456", + "kty": "mockKty", + "kid": "mockKid", + "alg": "mockAlg", + "verifyKey": "mockVerifyKey123456", + }, + "isEmailVerified": true, + } + json.NewEncoder(w).Encode(response) + }) + + // Default handler + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Request: %s %s", r.Method, r.URL.Path) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"status":"ok","path":"%s"}`, r.URL.Path) + }) + + // Start server + fmt.Println("Server listening on 0.0.0.0:8080") + if err := http.ListenAndServe("0.0.0.0:8080", nil); err != nil { + fmt.Printf("Server error: %v\n", err) + log.Fatalf("Server error: %v", err) + } +} +ENDOFCODE + + # Set SERVER_PID to 0 for safety + SERVER_PID=0 + + # Make sure logs directory exists + mkdir -p "${LOGS_DIR}" + touch "${LOGS_DIR}/api_requests.log" + chmod 666 "${LOGS_DIR}/api_requests.log" + + # Run the mock server + echo "==> Running mock API server" + go run server.go > "${LOGS_DIR}/mock_server.log" 2>&1 & + SERVER_PID=$! + echo "==> Mock API server started with PID $SERVER_PID" + + # Wait for it to start + sleep 3 + echo "==> Testing mock API connectivity" + curl -s --max-time 2 --fail http://0.0.0.0:${API_PORT}/health || echo "==> Warning: Mock API server not responding!" + + # Create a similar mock server for public albums + mkdir -p /tmp/mock-public-server + cd /tmp/mock-public-server + + cat > server.go << 'ENDOFCODE' +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" +) + +func main() { + port := "8081" + + // Create log directory and file + os.MkdirAll("/app/data/logs", 0755) + logFile, err := os.OpenFile("/app/data/logs/public_api_requests.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("Error creating log file: %v\n", err) + } else { + defer logFile.Close() + log.SetOutput(logFile) + } + + // Log startup + fmt.Println("Starting mock Public Albums server on port", port) + log.Println("Starting mock Public Albums server on port", port) + + // Health check endpoint + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"status":"ok","time":"%s"}`, time.Now().Format(time.RFC3339)) + }) + + // Default handler + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Public Albums Request: %s %s", r.Method, r.URL.Path) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"status":"ok","path":"%s"}`, r.URL.Path) + }) + + // Start server + fmt.Println("Public Albums server listening on 0.0.0.0:" + port) + if err := http.ListenAndServe("0.0.0.0:"+port, nil); err != nil { + fmt.Printf("Public Albums server error: %v\n", err) + log.Fatalf("Public Albums server error: %v", err) + } +} +ENDOFCODE + + # Run the public albums mock server + go run server.go > "${LOGS_DIR}/public_mock_server.log" 2>&1 & + PUBLIC_SERVER_PID=$! + echo "==> Started Public Albums mock server with PID $PUBLIC_SERVER_PID" + + # Wait for it to start + sleep 3 + echo "==> Testing mock Public Albums API connectivity" + curl -s --max-time 2 --fail http://0.0.0.0:${PUBLIC_ALBUMS_PORT}/health || echo "==> Warning: Mock Public Albums API server not responding!" +fi + +# Set up Caddy web server for proxying and serving static files +echo "==> Setting up Caddy web server" + +# Create runtime-config.js file +echo "==> Creating runtime-config.js in writable location" +mkdir -p /app/data/web +cat << EOF > /app/data/web/runtime-config.js // Runtime configuration for Ente window.ENTE_CONFIG = { - API_URL: '${API_ENDPOINT}', - PUBLIC_ALBUMS_URL: '${CLOUDRON_APP_ORIGIN}/public' + API_URL: 'https://${CLOUDRON_APP_FQDN}/api', + PUBLIC_ALBUMS_URL: 'https://${CLOUDRON_APP_FQDN}/public' }; // Next.js environment variables window.process = window.process || {}; window.process.env = window.process.env || {}; -window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = '${API_ENDPOINT}'; -window.process.env.NEXT_PUBLIC_ENTE_PUBLIC_ALBUMS_ENDPOINT = '${CLOUDRON_APP_ORIGIN}/public'; -window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT = '${API_ENDPOINT}'; -window.process.env.REACT_APP_ENTE_ENDPOINT = '${API_ENDPOINT}'; +window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api'; +window.process.env.NEXT_PUBLIC_ENTE_PUBLIC_ALBUMS_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/public'; +window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api'; +window.process.env.REACT_APP_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api'; -// Make sure all URLs are properly formatted for URL constructor -if (!window.ENTE_CONFIG.API_URL.startsWith('http')) { - console.log('Adding https:// prefix to API_URL'); - // Ensure the API URL has a proper protocol prefix for URL constructor - if (window.ENTE_CONFIG.API_URL.startsWith('/')) { - window.ENTE_CONFIG.API_URL = window.location.origin + window.ENTE_CONFIG.API_URL; - } else { - window.ENTE_CONFIG.API_URL = window.location.origin + '/' + window.ENTE_CONFIG.API_URL; - } - - // Fix environment variables too - if (window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT.startsWith('/')) { - window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = window.location.origin + window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT; - } else { - window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = window.location.origin + '/' + window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT; - } - - // Same for other variables - if (window.process.env.REACT_APP_ENTE_ENDPOINT.startsWith('/')) { - window.process.env.REACT_APP_ENTE_ENDPOINT = window.location.origin + window.process.env.REACT_APP_ENTE_ENDPOINT; - } else { - window.process.env.REACT_APP_ENTE_ENDPOINT = window.location.origin + '/' + window.process.env.REACT_APP_ENTE_ENDPOINT; - } - - if (window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT && !window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT.startsWith('http')) { - if (window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT.startsWith('/')) { - window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT = window.location.origin + window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT; - } else { - window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT = window.location.origin + '/' + window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT; - } - } -} - -// Extra logging to debug URL construction +// Add logging to help with debugging console.log('Ente runtime config loaded from runtime-config.js'); console.log('API_URL (final):', window.ENTE_CONFIG.API_URL); console.log('PUBLIC_ALBUMS_URL (final):', window.ENTE_CONFIG.PUBLIC_ALBUMS_URL); console.log('NEXT_PUBLIC_ENTE_ENDPOINT (final):', window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT); EOF - # Update the variables in the runtime config - sed -i "s|\${API_ENDPOINT}|${API_ENDPOINT}|g" /app/data/web/runtime-config.js - sed -i "s|\${CLOUDRON_APP_ORIGIN}|${CLOUDRON_APP_ORIGIN}|g" /app/data/web/runtime-config.js +chmod 644 /app/data/web/runtime-config.js - # Ensure runtime-config.js is readable - chmod 644 /app/data/web/runtime-config.js - - # Create the static HTML files with scripts pre-injected - for app_dir in photos accounts auth cast; do - # Create directory for our modified files - mkdir -p /app/data/web/$app_dir +# Create the static HTML files with scripts pre-injected +for app_dir in photos accounts auth cast; do + # Create directory for our modified files + mkdir -p /app/data/web/$app_dir + + # If the original index.html exists, copy and modify it + if [ -f "/app/web/$app_dir/index.html" ]; then + echo "==> Copying and modifying index.html for $app_dir app" + cp "/app/web/$app_dir/index.html" "/app/data/web/$app_dir/index.html" - # If the original index.html exists, copy and modify it - if [ -f "/app/web/$app_dir/index.html" ]; then - echo "==> Copying and modifying index.html for $app_dir app" - cp "/app/web/$app_dir/index.html" "/app/data/web/$app_dir/index.html" - - # Fix any potential issues with the head tag - if ! grep -q "" "/app/data/web/$app_dir/index.html"; then - echo "==> Warning: No head tag found in $app_dir/index.html, adding one" - sed -i 's//\n<\/head>/' "/app/data/web/$app_dir/index.html" - fi - - # Insert config scripts right after the opening head tag, unescaped - sed -i 's//\n