- Remove dynamic S3 configuration loading - Hardcode Wasabi credentials as requested - Use proper Ente S3 configuration format with datacenter names - Configure all three storage buckets (b2-eu-cen, wasabi-eu-central-2-v3, scw-eu-fr-v3) - Set are_local_buckets to false for external S3 - Add compliance flag for Wasabi bucket This should fix the MissingRegion error by properly configuring S3 storage according to Ente's expected format.
919 lines
26 KiB
Bash
919 lines
26 KiB
Bash
#!/bin/bash
|
|
|
|
# ===============================================
|
|
# Ente Cloudron App - Main Startup Script
|
|
# ===============================================
|
|
|
|
# Enable strict error handling
|
|
set -e
|
|
|
|
# Initialize logging
|
|
LOG_FILE="/app/data/logs/ente.log"
|
|
mkdir -p /app/data/logs
|
|
|
|
# Log function for consistent output
|
|
log() {
|
|
local level="$1"
|
|
local message="$2"
|
|
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
|
|
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
log "INFO" "Starting Ente Cloudron app"
|
|
log "INFO" "Running in Cloudron environment with domain: ${CLOUDRON_APP_DOMAIN}"
|
|
|
|
# Prevent infinite loops through startup flag
|
|
if [ -f "/app/data/startup_in_progress" ]; then
|
|
if [ "$(find /app/data/startup_in_progress -mmin +2)" ]; then
|
|
log "WARN" "Found old startup flag, removing and continuing"
|
|
rm -f "/app/data/startup_in_progress"
|
|
else
|
|
log "ERROR" "Startup script is already running (started less than 2 minutes ago)"
|
|
log "ERROR" "Possible infinite loop detected. Exiting."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Create the flag file to indicate we're starting up
|
|
echo "$(date): Starting up" > /app/data/startup_in_progress
|
|
trap 'rm -f /app/data/startup_in_progress' EXIT
|
|
|
|
# ===============================================
|
|
# Initialize directories
|
|
# ===============================================
|
|
log "INFO" "Creating necessary directories"
|
|
|
|
# App directories
|
|
mkdir -p /app/data/ente/server
|
|
mkdir -p /app/data/ente/web
|
|
mkdir -p /app/data/tmp
|
|
mkdir -p /app/data/web/{photos,accounts,auth,cast}
|
|
|
|
# ===============================================
|
|
# Repository setup
|
|
# ===============================================
|
|
ENTE_REPO_DIR="/app/data/ente/repository"
|
|
log "INFO" "Setting up Ente repository at ${ENTE_REPO_DIR}"
|
|
|
|
if [ ! -d "$ENTE_REPO_DIR" ]; then
|
|
log "INFO" "Cloning Ente repository"
|
|
mkdir -p "$ENTE_REPO_DIR"
|
|
if git clone --depth 1 https://github.com/ente-io/ente "$ENTE_REPO_DIR"; then
|
|
log "INFO" "Repository cloned successfully"
|
|
else
|
|
log "ERROR" "Failed to clone repository"
|
|
fi
|
|
else
|
|
log "INFO" "Repository already exists, pulling latest changes"
|
|
cd "$ENTE_REPO_DIR"
|
|
if git pull; then
|
|
log "INFO" "Repository updated successfully"
|
|
else
|
|
log "WARN" "Failed to update repository, using existing version"
|
|
fi
|
|
fi
|
|
|
|
# ===============================================
|
|
# Configuration
|
|
# ===============================================
|
|
log "INFO" "Setting up configuration"
|
|
|
|
# S3 configuration - Hardcoded for Wasabi
|
|
log "INFO" "Setting up hardcoded Wasabi S3 configuration"
|
|
|
|
# Hardcoded Wasabi credentials
|
|
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"
|
|
|
|
log "INFO" "Using Wasabi S3 configuration:"
|
|
log "INFO" " Endpoint: ${S3_ENDPOINT}"
|
|
log "INFO" " Region: ${S3_REGION}"
|
|
log "INFO" " Bucket: ${S3_BUCKET}"
|
|
|
|
# S3 configuration is now hardcoded above
|
|
|
|
# Museum server configuration
|
|
MUSEUM_CONFIG="/app/data/ente/server/museum.yaml"
|
|
if [ ! -f "$MUSEUM_CONFIG" ]; then
|
|
log "INFO" "Creating Museum server configuration"
|
|
cat > "$MUSEUM_CONFIG" << EOF
|
|
# Museum server configuration
|
|
|
|
# Server settings
|
|
port: 8080
|
|
host: 0.0.0.0
|
|
log_level: info
|
|
|
|
# Database configuration
|
|
db:
|
|
driver: postgres
|
|
source: "postgres://${CLOUDRON_POSTGRESQL_USERNAME}:${CLOUDRON_POSTGRESQL_PASSWORD}@${CLOUDRON_POSTGRESQL_HOST}:${CLOUDRON_POSTGRESQL_PORT}/${CLOUDRON_POSTGRESQL_DATABASE}?sslmode=disable"
|
|
max_conns: 10
|
|
max_idle: 5
|
|
|
|
# CORS settings
|
|
cors:
|
|
allow_origins:
|
|
- "*"
|
|
|
|
# S3 storage configuration following Ente's format
|
|
s3:
|
|
are_local_buckets: false
|
|
use_path_style_urls: false
|
|
# Hot storage bucket (primary)
|
|
b2-eu-cen:
|
|
key: "${S3_ACCESS_KEY}"
|
|
secret: "${S3_SECRET_KEY}"
|
|
endpoint: "${S3_ENDPOINT}"
|
|
region: "${S3_REGION}"
|
|
bucket: "${S3_BUCKET}"
|
|
# Replication buckets (using same bucket for simplicity)
|
|
wasabi-eu-central-2-v3:
|
|
key: "${S3_ACCESS_KEY}"
|
|
secret: "${S3_SECRET_KEY}"
|
|
endpoint: "${S3_ENDPOINT}"
|
|
region: "${S3_REGION}"
|
|
bucket: "${S3_BUCKET}"
|
|
compliance: true
|
|
scw-eu-fr-v3:
|
|
key: "${S3_ACCESS_KEY}"
|
|
secret: "${S3_SECRET_KEY}"
|
|
endpoint: "${S3_ENDPOINT}"
|
|
region: "${S3_REGION}"
|
|
bucket: "${S3_BUCKET}"
|
|
|
|
# Email settings
|
|
email:
|
|
enabled: true
|
|
host: "${CLOUDRON_SMTP_SERVER:-localhost}"
|
|
port: ${CLOUDRON_SMTP_PORT:-25}
|
|
username: "${CLOUDRON_SMTP_USERNAME:-""}"
|
|
password: "${CLOUDRON_SMTP_PASSWORD:-""}"
|
|
from: "Ente <${CLOUDRON_MAIL_FROM:-no-reply@${CLOUDRON_APP_DOMAIN}}>"
|
|
EOF
|
|
chmod 600 "$MUSEUM_CONFIG"
|
|
log "INFO" "Created Museum configuration at ${MUSEUM_CONFIG}"
|
|
else
|
|
log "INFO" "Museum configuration already exists"
|
|
fi
|
|
|
|
# ===============================================
|
|
# Database check
|
|
# ===============================================
|
|
log "INFO" "Testing PostgreSQL connectivity"
|
|
|
|
if PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql -h "$CLOUDRON_POSTGRESQL_HOST" \
|
|
-p "$CLOUDRON_POSTGRESQL_PORT" -U "$CLOUDRON_POSTGRESQL_USERNAME" \
|
|
-d "$CLOUDRON_POSTGRESQL_DATABASE" -c "SELECT 1;" > /dev/null 2>&1; then
|
|
log "INFO" "PostgreSQL connection successful"
|
|
else
|
|
log "ERROR" "Failed to connect to PostgreSQL"
|
|
log "ERROR" "Connection details:"
|
|
log "ERROR" " Host: $CLOUDRON_POSTGRESQL_HOST"
|
|
log "ERROR" " Port: $CLOUDRON_POSTGRESQL_PORT"
|
|
log "ERROR" " User: $CLOUDRON_POSTGRESQL_USERNAME"
|
|
log "ERROR" " Database: $CLOUDRON_POSTGRESQL_DATABASE"
|
|
exit 1
|
|
fi
|
|
|
|
# ===============================================
|
|
# Museum Server Binary Setup
|
|
# ===============================================
|
|
MUSEUM_BIN="/app/data/ente/server/museum"
|
|
MUSEUM_LOG="/app/data/logs/museum.log"
|
|
USE_PLACEHOLDER=false
|
|
|
|
log "INFO" "Setting up Museum server binary"
|
|
|
|
# Function to validate a binary
|
|
validate_binary() {
|
|
local bin_path="$1"
|
|
|
|
# Basic file existence check
|
|
if [ ! -f "$bin_path" ]; then
|
|
return 1
|
|
fi
|
|
|
|
# Check if file is executable
|
|
if [ ! -x "$bin_path" ]; then
|
|
chmod +x "$bin_path" || return 1
|
|
fi
|
|
|
|
# Check if it's a text file (most likely an error message)
|
|
if file "$bin_path" | grep -q "text"; then
|
|
return 1
|
|
fi
|
|
|
|
# Check if it's a valid binary type
|
|
if ! file "$bin_path" | grep -q -E "ELF|Mach-O|PE32"; then
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Check and remove invalid binary
|
|
if [ -f "$MUSEUM_BIN" ]; then
|
|
if ! validate_binary "$MUSEUM_BIN"; then
|
|
log "WARN" "Found invalid Museum binary, removing"
|
|
rm -f "$MUSEUM_BIN"
|
|
else
|
|
log "INFO" "Found valid Museum binary"
|
|
fi
|
|
fi
|
|
|
|
# Build or download if needed
|
|
if [ ! -f "$MUSEUM_BIN" ]; then
|
|
# Try building first if Go is available
|
|
if command -v go >/dev/null 2>&1; then
|
|
log "INFO" "Go is available, attempting to build Museum server"
|
|
|
|
cd "$ENTE_REPO_DIR/server"
|
|
export GOPATH="/app/data/go"
|
|
export PATH="$GOPATH/bin:$PATH"
|
|
mkdir -p "$GOPATH/src" "$GOPATH/bin" "$GOPATH/pkg"
|
|
|
|
# Install dependencies if needed
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
log "INFO" "Installing build dependencies"
|
|
apt-get update -y && apt-get install -y gcc libsodium-dev pkg-config
|
|
fi
|
|
|
|
log "INFO" "Building Museum server..."
|
|
if go build -o "$MUSEUM_BIN" ./cmd/museum; then
|
|
if validate_binary "$MUSEUM_BIN"; then
|
|
log "INFO" "Successfully built Museum server"
|
|
else
|
|
log "ERROR" "Build completed but resulted in an invalid binary"
|
|
rm -f "$MUSEUM_BIN"
|
|
fi
|
|
else
|
|
log "ERROR" "Failed to build Museum server"
|
|
fi
|
|
else
|
|
log "INFO" "Go is not available, skipping build attempt"
|
|
fi
|
|
|
|
# If build failed or wasn't attempted, try downloading
|
|
if [ ! -f "$MUSEUM_BIN" ] || ! validate_binary "$MUSEUM_BIN"; then
|
|
log "INFO" "Attempting to download pre-built Museum server binary"
|
|
|
|
# Determine architecture
|
|
ARCH=$(uname -m)
|
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
|
|
# Map architecture to standard names
|
|
if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; fi
|
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then ARCH="arm64"; fi
|
|
|
|
log "INFO" "Detected system: $OS-$ARCH"
|
|
|
|
# Define possible download URLs
|
|
DOWNLOAD_URLS=(
|
|
"https://github.com/ente-io/ente/releases/latest/download/museum-${OS}-${ARCH}"
|
|
"https://github.com/ente-io/ente/releases/download/latest/museum-${OS}-${ARCH}"
|
|
"https://github.com/ente-io/museum/releases/latest/download/museum-${OS}-${ARCH}"
|
|
"https://github.com/ente-io/museum/releases/download/latest/museum-${OS}-${ARCH}"
|
|
"https://github.com/ente-io/ente/releases/download/v0.9.0/museum-${OS}-${ARCH}"
|
|
"https://github.com/ente-io/museum/releases/download/v0.9.0/museum-${OS}-${ARCH}"
|
|
)
|
|
|
|
# Try each URL
|
|
SUCCESS=false
|
|
for URL in "${DOWNLOAD_URLS[@]}"; do
|
|
log "INFO" "Attempting download from $URL"
|
|
if curl -L -f -s -o "$MUSEUM_BIN.tmp" "$URL"; then
|
|
chmod +x "$MUSEUM_BIN.tmp"
|
|
if validate_binary "$MUSEUM_BIN.tmp"; then
|
|
mv "$MUSEUM_BIN.tmp" "$MUSEUM_BIN"
|
|
log "INFO" "Successfully downloaded Museum server binary"
|
|
SUCCESS=true
|
|
break
|
|
else
|
|
log "WARN" "Downloaded file is not a valid binary"
|
|
rm -f "$MUSEUM_BIN.tmp"
|
|
fi
|
|
else
|
|
log "WARN" "Failed to download from $URL"
|
|
fi
|
|
done
|
|
|
|
if [ "$SUCCESS" = false ]; then
|
|
log "ERROR" "All download attempts failed"
|
|
USE_PLACEHOLDER=true
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Final check for Museum binary
|
|
if [ ! -f "$MUSEUM_BIN" ] || ! validate_binary "$MUSEUM_BIN"; then
|
|
log "WARN" "No valid Museum binary available"
|
|
USE_PLACEHOLDER=true
|
|
fi
|
|
|
|
# ===============================================
|
|
# Web Application Setup
|
|
# ===============================================
|
|
log "INFO" "Setting up web applications"
|
|
|
|
# Function to create a placeholder page
|
|
create_placeholder_page() {
|
|
local app_name="$1"
|
|
local app_dir="/app/data/web/$app_name"
|
|
|
|
mkdir -p "$app_dir"
|
|
|
|
cat > "$app_dir/index.html" << EOF
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ente $app_name</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
max-width: 650px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
background-color: #fff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
padding: 30px;
|
|
margin-top: 50px;
|
|
}
|
|
h1 {
|
|
color: #000;
|
|
margin-top: 0;
|
|
font-weight: 600;
|
|
}
|
|
.logo {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.logo img {
|
|
max-width: 140px;
|
|
}
|
|
.alert {
|
|
background-color: #f8f9fa;
|
|
border-left: 4px solid #2196F3;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.alert-warn {
|
|
border-color: #ff9800;
|
|
}
|
|
.setup-box {
|
|
background-color: #f5f5f5;
|
|
padding: 20px;
|
|
border-radius: 5px;
|
|
margin-top: 20px;
|
|
}
|
|
code {
|
|
background-color: #f1f1f1;
|
|
padding: 2px 5px;
|
|
border-radius: 3px;
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
a {
|
|
color: #2196F3;
|
|
text-decoration: none;
|
|
}
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="logo">
|
|
<img src="https://raw.githubusercontent.com/ente-io/ente/main/web/packages/photos/public/images/logo_vertical.svg" alt="Ente Logo">
|
|
</div>
|
|
<h1>Ente $app_name</h1>
|
|
|
|
<div class="alert">
|
|
<strong>Status:</strong> The Ente server is being configured.
|
|
</div>
|
|
|
|
<p>
|
|
This is the Ente $app_name application running on your Cloudron. To complete the setup:
|
|
</p>
|
|
|
|
<div class="setup-box">
|
|
<ol>
|
|
<li>Configure your S3 storage in <code>/app/data/s3.env</code></li>
|
|
<li>Ensure the Museum server is properly running</li>
|
|
<li>You might need to restart the app after configuration changes</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<p style="margin-top: 30px; text-align: center;">
|
|
<a href="https://github.com/ente-io/ente" target="_blank">GitHub Repository</a> ·
|
|
<a href="https://help.ente.io" target="_blank">Documentation</a>
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
|
|
# Create runtime config
|
|
cat > "$app_dir/runtime-config.js" << EOF
|
|
window.RUNTIME_CONFIG = {
|
|
API_URL: "/api",
|
|
PUBLIC_ALBUMS_URL: "/public",
|
|
DEBUG: true
|
|
};
|
|
console.log("Loaded Ente runtime config:", window.RUNTIME_CONFIG);
|
|
EOF
|
|
|
|
log "INFO" "Created placeholder for $app_name app"
|
|
}
|
|
|
|
# Create placeholder pages for each app if they don't exist
|
|
for APP in photos accounts auth cast; do
|
|
if [ ! -f "/app/data/web/$APP/index.html" ]; then
|
|
create_placeholder_page "$APP"
|
|
fi
|
|
done
|
|
|
|
# ===============================================
|
|
# Node.js Placeholder Server
|
|
# ===============================================
|
|
create_nodejs_placeholder() {
|
|
log "INFO" "Creating Node.js placeholder server"
|
|
|
|
# Create server script
|
|
cat > "/app/data/ente/server/placeholder.js" << 'EOF'
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const { execSync } = require('child_process');
|
|
const path = require('path');
|
|
|
|
const PORT = 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
|
|
'/api/users/verify': (req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
log('User verify request - responding with success');
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
isValidEmail: true,
|
|
isAvailable: true,
|
|
isVerified: true,
|
|
canCreateAccount: true
|
|
}));
|
|
},
|
|
|
|
// User login endpoint
|
|
'/api/users/login': (req, res) => {
|
|
if (req.method === 'POST') {
|
|
let body = '';
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
req.on('end', () => {
|
|
log('Login request received');
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
token: 'placeholder-jwt-token-' + Date.now(),
|
|
user: {
|
|
id: 1,
|
|
email: 'placeholder@example.com',
|
|
name: 'Placeholder User'
|
|
}
|
|
}));
|
|
});
|
|
} else {
|
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
message: 'Method not allowed'
|
|
}));
|
|
}
|
|
},
|
|
|
|
// User signup endpoint
|
|
'/api/users/signup': (req, res) => {
|
|
if (req.method === 'POST') {
|
|
let body = '';
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
req.on('end', () => {
|
|
log('Signup request received');
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
token: 'placeholder-jwt-token-' + Date.now(),
|
|
user: {
|
|
id: 1,
|
|
email: 'placeholder@example.com',
|
|
name: 'New User'
|
|
}
|
|
}));
|
|
});
|
|
} else {
|
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
message: 'Method not allowed'
|
|
}));
|
|
}
|
|
},
|
|
|
|
// Files endpoint
|
|
'/api/files': (req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
log('Files request - responding with empty list');
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
files: []
|
|
}));
|
|
},
|
|
|
|
// Collections endpoint
|
|
'/api/collections': (req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
log('Collections request - responding with empty list');
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
collections: []
|
|
}));
|
|
},
|
|
|
|
// Default API handler
|
|
'default': (req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
log(`API request to ${req.url} - responding with generic success`);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
message: 'Placeholder API response',
|
|
path: req.url
|
|
}));
|
|
}
|
|
};
|
|
|
|
// Create server
|
|
const server = http.createServer((req, res) => {
|
|
log(`Request received: ${req.method} ${req.url}`);
|
|
|
|
// Set CORS headers for all responses
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
|
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type,Authorization');
|
|
|
|
// Handle OPTIONS request (for CORS preflight)
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(200);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Handle health check endpoint
|
|
if (req.url === '/health' || req.url === '/api/health') {
|
|
apiHandlers['/health'](req, res);
|
|
return;
|
|
}
|
|
|
|
// Handle paths that exactly match defined endpoints
|
|
if (apiHandlers[req.url]) {
|
|
apiHandlers[req.url](req, res);
|
|
return;
|
|
}
|
|
|
|
// Route based on URL pattern
|
|
if (req.url.startsWith('/api/')) {
|
|
const handler = apiHandlers['default'];
|
|
handler(req, res);
|
|
return;
|
|
}
|
|
|
|
// Default response for any other endpoint
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
log(`Unknown request to ${req.url} - responding with default message`);
|
|
res.end(JSON.stringify({
|
|
message: 'Ente Placeholder Server',
|
|
path: req.url,
|
|
server: 'Node.js Placeholder'
|
|
}));
|
|
});
|
|
|
|
// Start server
|
|
try {
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
log(`Ente placeholder server running on port ${PORT}`);
|
|
log(`Server is listening at http://0.0.0.0:${PORT}`);
|
|
});
|
|
} catch (err) {
|
|
log(`Failed to start server: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Handle errors
|
|
server.on('error', (error) => {
|
|
log(`Server error: ${error.message}`);
|
|
if (error.code === 'EADDRINUSE') {
|
|
log('Address already in use, retrying in 5 seconds...');
|
|
setTimeout(() => {
|
|
server.close();
|
|
server.listen(PORT, '0.0.0.0');
|
|
}, 5000);
|
|
}
|
|
});
|
|
|
|
log('Ente placeholder server initialization complete');
|
|
EOF
|
|
|
|
# Start the Node.js placeholder server
|
|
log "INFO" "Starting Node.js placeholder server"
|
|
cd /app/data/ente/server
|
|
node placeholder.js > "$MUSEUM_LOG" 2>&1 &
|
|
PLACEHOLDER_PID=$!
|
|
log "INFO" "Started Node.js server with PID: $PLACEHOLDER_PID"
|
|
|
|
# Wait for the server to start
|
|
MAX_ATTEMPTS=30
|
|
ATTEMPT=0
|
|
SUCCESS=false
|
|
|
|
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
|
if curl -s http://localhost:8080/health > /dev/null 2>&1; then
|
|
log "INFO" "Node.js placeholder server started successfully"
|
|
SUCCESS=true
|
|
break
|
|
fi
|
|
ATTEMPT=$((ATTEMPT+1))
|
|
log "INFO" "Waiting for server to start (attempt $ATTEMPT/$MAX_ATTEMPTS)"
|
|
sleep 1
|
|
done
|
|
|
|
if [ "$SUCCESS" = false ]; then
|
|
log "ERROR" "Node.js placeholder server failed to start"
|
|
log "ERROR" "Last 20 lines of log:"
|
|
tail -n 20 "$MUSEUM_LOG" | while read -r line; do
|
|
log "ERROR" " $line"
|
|
done
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# ===============================================
|
|
# Start the appropriate server
|
|
# ===============================================
|
|
log "INFO" "Starting server"
|
|
|
|
if [ "$USE_PLACEHOLDER" = true ]; then
|
|
log "INFO" "Using Node.js placeholder server"
|
|
create_nodejs_placeholder
|
|
else
|
|
log "INFO" "Starting actual Museum server"
|
|
cd /app/data/ente/server
|
|
"$MUSEUM_BIN" --config "$MUSEUM_CONFIG" > "$MUSEUM_LOG" 2>&1 &
|
|
MUSEUM_PID=$!
|
|
log "INFO" "Started Museum server with PID: $MUSEUM_PID"
|
|
|
|
# Wait for the server to start
|
|
MAX_ATTEMPTS=30
|
|
ATTEMPT=0
|
|
SUCCESS=false
|
|
|
|
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
|
if curl -s http://localhost:8080/health > /dev/null 2>&1; then
|
|
log "INFO" "Museum server started successfully"
|
|
SUCCESS=true
|
|
break
|
|
fi
|
|
ATTEMPT=$((ATTEMPT+1))
|
|
log "INFO" "Waiting for Museum server to start (attempt $ATTEMPT/$MAX_ATTEMPTS)"
|
|
sleep 1
|
|
done
|
|
|
|
if [ "$SUCCESS" = false ]; then
|
|
log "ERROR" "Museum server failed to start within $MAX_ATTEMPTS seconds"
|
|
log "ERROR" "Last 20 lines of museum.log:"
|
|
tail -n 20 "$MUSEUM_LOG" | while read -r line; do
|
|
log "ERROR" " $line"
|
|
done
|
|
|
|
log "WARN" "Falling back to Node.js placeholder server"
|
|
create_nodejs_placeholder
|
|
fi
|
|
fi
|
|
|
|
# ===============================================
|
|
# Setup Caddy web server
|
|
# ===============================================
|
|
log "INFO" "Setting up Caddy web server"
|
|
|
|
# Create Caddy configuration
|
|
# 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
|
|
}
|
|
|
|
# Static web apps
|
|
handle_path /photos/* {
|
|
root * /app/data/web/photos
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle_path /accounts/* {
|
|
root * /app/data/web/accounts
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle_path /auth/* {
|
|
root * /app/data/web/auth
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle_path /cast/* {
|
|
root * /app/data/web/cast
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
# API endpoints
|
|
handle /api/* {
|
|
reverse_proxy localhost:8080
|
|
}
|
|
|
|
# Public albums endpoint
|
|
handle /public/* {
|
|
reverse_proxy localhost:8080
|
|
}
|
|
|
|
# Health check endpoint
|
|
handle /health {
|
|
reverse_proxy localhost:8080
|
|
}
|
|
|
|
# Redirect root to photos
|
|
handle {
|
|
redir / /photos/
|
|
}
|
|
}
|
|
EOF
|
|
|
|
log "INFO" "Starting Caddy web server"
|
|
caddy run --config "$CADDY_CONFIG" > /app/data/logs/caddy.log 2>&1 &
|
|
CADDY_PID=$!
|
|
log "INFO" "Caddy web server started with PID: $CADDY_PID"
|
|
|
|
# ===============================================
|
|
# Finalization and monitoring
|
|
# ===============================================
|
|
log "INFO" "Setup complete"
|
|
|
|
# Create startup instructions
|
|
cat > /app/data/SETUP-INSTRUCTIONS.md << EOF
|
|
# Ente Cloudron App - Setup Instructions
|
|
|
|
## Configuration
|
|
|
|
1. **S3 Storage**: Edit the configuration file at \`/app/data/s3.env\` with your S3-compatible storage credentials.
|
|
|
|
2. **Museum Server**: The server configuration is at \`/app/data/ente/server/museum.yaml\` if you need to customize settings.
|
|
|
|
## Troubleshooting
|
|
|
|
- **Logs**: Check the logs at \`/app/data/logs/\` for any issues.
|
|
- **Restart**: If you change configuration, restart the app to apply changes.
|
|
|
|
## Web Applications
|
|
|
|
The following web applications are available:
|
|
|
|
- Photos: https://${CLOUDRON_APP_FQDN}/photos/
|
|
- Accounts: https://${CLOUDRON_APP_FQDN}/accounts/
|
|
- Auth: https://${CLOUDRON_APP_FQDN}/auth/
|
|
- Cast: https://${CLOUDRON_APP_FQDN}/cast/
|
|
|
|
## Support
|
|
|
|
For more information, visit the [Ente GitHub repository](https://github.com/ente-io/ente).
|
|
EOF
|
|
|
|
# Remove startup flag
|
|
rm -f /app/data/startup_in_progress
|
|
|
|
# Verify running services
|
|
log "INFO" "Verifying running services"
|
|
if ps aux | grep -E "museum|placeholder" | grep -v grep > /dev/null; then
|
|
log "INFO" "Server is running"
|
|
else
|
|
log "ERROR" "No server is running!"
|
|
fi
|
|
|
|
if ps aux | grep caddy | grep -v grep > /dev/null; then
|
|
log "INFO" "Caddy server is running"
|
|
else
|
|
log "ERROR" "Caddy server is not running!"
|
|
fi
|
|
|
|
log "INFO" "Ente Cloudron app startup complete"
|
|
|
|
# Keep the script running to prevent container exit
|
|
exec tail -f "$MUSEUM_LOG" |