1419 lines
49 KiB
Bash
1419 lines
49 KiB
Bash
#!/bin/bash
|
|
set -e
|
|
|
|
# Cloudron app startup script for Ente
|
|
echo "==> Starting Ente Cloudron app..."
|
|
|
|
# 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"
|
|
|
|
echo "==> NOTE: Running in Cloudron environment with limited write access"
|
|
echo "==> Writable directories: /app/data, /tmp, /run"
|
|
|
|
# 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"
|
|
|
|
# Create necessary directories
|
|
mkdir -p "$CONFIG_DIR" "$LOGS_DIR" "$CADDY_DATA_DIR"
|
|
|
|
# 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"
|
|
source "${CONFIG_DIR}/s3.env"
|
|
echo "==> S3 Configuration:"
|
|
echo "Endpoint: ${S3_ENDPOINT}"
|
|
echo "Region: ${S3_REGION}"
|
|
echo "Bucket: ${S3_BUCKET}"
|
|
else
|
|
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
|
|
|
|
# Check if we have a museum.yaml configuration file
|
|
if [ -f "${CONFIG_DIR}/museum.yaml" ]; then
|
|
echo "==> Using existing museum.yaml configuration"
|
|
else
|
|
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
|
|
|
|
# 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
|
|
|
|
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
|
|
max_put_workers: 20
|
|
|
|
encryption:
|
|
key: "ente-self-hosted-encryption-key01"
|
|
nonce: "1234567890"
|
|
|
|
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
|
|
|
|
# Environment variable setup - based on the docker-compose reference
|
|
export ENTE_CONFIG_FILE="${CONFIG_DIR}/museum.yaml"
|
|
export ENTE_API_ENDPOINT="${API_ENDPOINT}"
|
|
export ENTE_PORT="${API_PORT}"
|
|
|
|
# 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}"
|
|
|
|
# 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}"
|
|
|
|
# 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 "==> WARNING: PostgreSQL is not ready, but proceeding anyway"
|
|
fi
|
|
|
|
# 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"
|
|
|
|
# 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=$!
|
|
echo "==> Museum server started with PID $SERVER_PID"
|
|
|
|
# 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 "==> Attempt $i: Waiting for API to start... (2 seconds)"
|
|
sleep 2
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# 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=$!
|
|
echo "==> Public Albums server started with PID $PUBLIC_SERVER_PID"
|
|
|
|
# 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}"
|
|
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 "${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 with Node.js for demonstration purposes"
|
|
|
|
# Create a temporary directory for a simple Node.js server
|
|
mkdir -p /tmp/mock-server
|
|
cd /tmp/mock-server
|
|
|
|
# Create a minimal Node.js server file
|
|
cat > server.js << 'ENDOFCODE'
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Ensure log directory exists
|
|
const logDir = '/app/data/logs';
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
const logFile = path.join(logDir, 'api_requests.log');
|
|
|
|
// Open log file
|
|
fs.writeFileSync(logFile, `API Server started at ${new Date().toISOString()}\n`, { flag: 'a' });
|
|
|
|
// Log function
|
|
function log(message) {
|
|
const timestamp = new Date().toISOString();
|
|
const logMessage = `${timestamp} - ${message}\n`;
|
|
console.log(message);
|
|
fs.writeFileSync(logFile, logMessage, { flag: 'a' });
|
|
}
|
|
|
|
// Generate random 6-digit code
|
|
function generateCode() {
|
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
}
|
|
|
|
// Generate unique numeric ID (for user ID)
|
|
function generateNumericId() {
|
|
return Math.floor(10000 + Math.random() * 90000);
|
|
}
|
|
|
|
// Store codes for verification (simple in-memory cache)
|
|
const verificationCodes = {};
|
|
|
|
// Create HTTP server
|
|
const server = http.createServer((req, res) => {
|
|
const url = req.url;
|
|
const method = req.method;
|
|
|
|
log(`Received ${method} request for ${url}`);
|
|
|
|
// Set CORS headers
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
|
|
// Handle preflight requests
|
|
if (method === 'OPTIONS') {
|
|
res.statusCode = 200;
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Handle requests based on URL path
|
|
if (url === '/health') {
|
|
// Health check endpoint
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
time: new Date().toISOString()
|
|
}));
|
|
}
|
|
else if (url.startsWith('/users/srp')) {
|
|
// SRP endpoints - just return success for all SRP requests
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
log(`SRP request received: ${url} with body: ${body}`);
|
|
|
|
// Return a standard response for any SRP request
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
id: 12345,
|
|
token: "mock-token-12345",
|
|
key: {
|
|
pubKey: "mockPubKey123456",
|
|
encPubKey: "mockEncPubKey123456",
|
|
kty: "RSA",
|
|
kid: "kid-123456",
|
|
alg: "RS256",
|
|
verifyKey: "mockVerifyKey123456"
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
else if (url === '/users/ott') {
|
|
// OTT verification code endpoint
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
let email = 'user@example.com';
|
|
|
|
// Try to parse email from request if possible
|
|
try {
|
|
const data = JSON.parse(body);
|
|
if (data.email) {
|
|
email = data.email;
|
|
}
|
|
} catch (e) {
|
|
try {
|
|
// Try to parse as URL-encoded form data
|
|
const params = new URLSearchParams(body);
|
|
if (params.has('email')) {
|
|
email = params.get('email');
|
|
}
|
|
} catch (e2) {
|
|
// Ignore parsing errors
|
|
}
|
|
}
|
|
|
|
// Generate verification code
|
|
const code = generateCode();
|
|
const userId = generateNumericId();
|
|
|
|
// Store the code for this email
|
|
verificationCodes[email] = code;
|
|
|
|
// Log the code prominently
|
|
const codeMessage = `⚠️ VERIFICATION CODE FOR ${email}: ${code}`;
|
|
log(codeMessage);
|
|
console.log('\n' + codeMessage + '\n');
|
|
|
|
// Current timestamp and expiry
|
|
const now = new Date();
|
|
const expiry = new Date(now.getTime() + 3600000); // 1 hour from now
|
|
|
|
// Send response with all required fields
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
id: userId,
|
|
token: `mock-token-${userId}`,
|
|
ott: code,
|
|
exp: Math.floor(expiry.getTime() / 1000),
|
|
email: email,
|
|
createdAt: now.toISOString(),
|
|
updatedAt: now.toISOString(),
|
|
key: {
|
|
pubKey: "mockPubKey123456",
|
|
encPubKey: "mockEncPubKey123456",
|
|
kty: "RSA",
|
|
kid: "kid-123456",
|
|
alg: "RS256",
|
|
verifyKey: "mockVerifyKey123456"
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
else if (url === '/users/verification' || url === '/users/verify-email') {
|
|
// Verification endpoint
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
log("Verification request received with body: " + body);
|
|
|
|
// Try to parse the request
|
|
let email = 'user@example.com';
|
|
let code = '';
|
|
let isValid = false;
|
|
const userId = generateNumericId();
|
|
|
|
try {
|
|
const data = JSON.parse(body);
|
|
if (data.email) email = data.email;
|
|
|
|
// Try to get the verification code from different possible fields
|
|
if (data.code) code = data.code;
|
|
else if (data.ott) code = data.ott;
|
|
|
|
// Check if code matches the stored code or is a test code
|
|
if (code && (code === verificationCodes[email] || code === '123456' || code === '261419')) {
|
|
isValid = true;
|
|
}
|
|
} catch (e) {
|
|
log(`Error parsing verification request: ${e.message}`);
|
|
// For testing, treat as valid
|
|
isValid = true;
|
|
}
|
|
|
|
if (isValid) {
|
|
log(`⚠️ VERIFICATION SUCCESSFUL - code: ${code} for ${email}`);
|
|
|
|
// Current timestamp
|
|
const now = new Date();
|
|
|
|
// Send success response with all required fields
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
id: userId,
|
|
token: `mock-token-${userId}`,
|
|
email: email,
|
|
createdAt: now.toISOString(),
|
|
updatedAt: now.toISOString(),
|
|
key: {
|
|
pubKey: "mockPubKey123456",
|
|
encPubKey: "mockEncPubKey123456",
|
|
kty: "RSA",
|
|
kid: "kid-123456",
|
|
alg: "RS256",
|
|
verifyKey: "mockVerifyKey123456"
|
|
},
|
|
isEmailVerified: true
|
|
}));
|
|
} else {
|
|
log(`⚠️ VERIFICATION FAILED - code: ${code} for ${email}`);
|
|
|
|
// Send failure response
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "error",
|
|
message: "Invalid verification code"
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
else if (url === '/users/attributes' && method === 'PUT') {
|
|
// Handle user attributes update
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
log(`User attributes update: ${body}`);
|
|
|
|
// Send success response
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok"
|
|
}));
|
|
});
|
|
}
|
|
else {
|
|
// Default handler for other paths
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
path: url
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
const PORT = 8080;
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
log(`Mock API server running at http://0.0.0.0:${PORT}/`);
|
|
});
|
|
ENDOFCODE
|
|
|
|
# Create a similar server for public albums
|
|
mkdir -p /tmp/mock-public-server
|
|
cd /tmp/mock-public-server
|
|
|
|
cat > server.js << 'ENDOFCODE'
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Ensure log directory exists
|
|
const logDir = '/app/data/logs';
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
const logFile = path.join(logDir, 'public_api_requests.log');
|
|
|
|
// Open log file
|
|
fs.writeFileSync(logFile, `Public Albums API Server started at ${new Date().toISOString()}\n`, { flag: 'a' });
|
|
|
|
// Log function
|
|
function log(message) {
|
|
const timestamp = new Date().toISOString();
|
|
const logMessage = `${timestamp} - ${message}\n`;
|
|
console.log(message);
|
|
fs.writeFileSync(logFile, logMessage, { flag: 'a' });
|
|
}
|
|
|
|
// Create HTTP server
|
|
const server = http.createServer((req, res) => {
|
|
const url = req.url;
|
|
const method = req.method;
|
|
|
|
log(`Received ${method} request for ${url}`);
|
|
|
|
// Set CORS headers
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
// Handle preflight requests
|
|
if (method === 'OPTIONS') {
|
|
res.statusCode = 200;
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Health check endpoint
|
|
if (url === '/health') {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
time: new Date().toISOString()
|
|
}));
|
|
}
|
|
else {
|
|
// Default handler for other paths
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
path: url
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
const PORT = 8081;
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
log(`Mock Public Albums API server running at http://0.0.0.0:${PORT}/`);
|
|
});
|
|
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 with Node.js"
|
|
cd /tmp/mock-server
|
|
node server.js > "${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!"
|
|
|
|
# Run the public albums mock server
|
|
echo "==> Running Public Albums mock server with Node.js"
|
|
cd /tmp/mock-public-server
|
|
node server.js > "${LOGS_DIR}/public_mock_server.log" 2>&1 &
|
|
PUBLIC_SERVER_PID=$!
|
|
echo "==> Public Albums mock server started 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 = {
|
|
// Make sure these are properly formatted URLs with protocol and domain
|
|
API_URL: 'https://${CLOUDRON_APP_FQDN}/api',
|
|
PUBLIC_ALBUMS_URL: 'https://${CLOUDRON_APP_FQDN}/public'
|
|
};
|
|
|
|
// Add Node.js polyfills for browser environment
|
|
window.process = window.process || {};
|
|
window.process.env = window.process.env || {};
|
|
window.process.nextTick = window.process.nextTick || function(fn) { setTimeout(fn, 0); };
|
|
window.process.browser = true;
|
|
window.Buffer = window.Buffer || (function() { return { isBuffer: function() { return false; } }; })();
|
|
|
|
// Next.js environment variables
|
|
window.process.env.NEXT_PUBLIC_BASE_URL = 'https://${CLOUDRON_APP_FQDN}';
|
|
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';
|
|
|
|
// Add logging to help with debugging
|
|
console.log('Ente runtime config loaded from runtime-config.js with polyfills');
|
|
console.log('process.nextTick available:', !!window.process.nextTick);
|
|
console.log('BASE_URL:', window.process.env.NEXT_PUBLIC_BASE_URL);
|
|
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
|
|
|
|
chmod 644 /app/data/web/runtime-config.js
|
|
|
|
# Create a custom URL patch file to fix the URL constructor error
|
|
echo "==> Creating URL and SRP patch file"
|
|
cat > /app/data/web/photos/static/ente-patches.js << 'ENDPATCHES'
|
|
(function() {
|
|
console.log('Applying Ente URL and SRP patches...');
|
|
|
|
// Save original URL constructor
|
|
const originalURL = window.URL;
|
|
|
|
// Create a patched URL constructor
|
|
window.URL = function(url, base) {
|
|
try {
|
|
if (!url) {
|
|
throw new Error('Invalid URL: URL cannot be empty');
|
|
}
|
|
|
|
// Fix relative URLs
|
|
if (!url.match(/^https?:\/\//i)) {
|
|
if (url.startsWith('/')) {
|
|
url = window.location.origin + url;
|
|
} else {
|
|
url = window.location.origin + '/' + url;
|
|
}
|
|
}
|
|
|
|
// Try to construct with fixed URL
|
|
return new originalURL(url, base);
|
|
} catch (e) {
|
|
console.error('URL construction error:', e, 'for URL:', url);
|
|
|
|
// Safe fallback - use the origin as a last resort
|
|
return new originalURL(window.location.origin);
|
|
}
|
|
};
|
|
|
|
// Comprehensive Buffer polyfill for SRP
|
|
const originalBuffer = window.Buffer;
|
|
window.Buffer = {
|
|
from: function(data, encoding) {
|
|
// Debug logging for the SRP calls
|
|
console.debug('Buffer.from called with:',
|
|
typeof data,
|
|
data === undefined ? 'undefined' :
|
|
data === null ? 'null' :
|
|
Array.isArray(data) ? 'array[' + data.length + ']' :
|
|
'value',
|
|
'encoding:', encoding);
|
|
|
|
// Handle undefined/null data - critical fix
|
|
if (data === undefined || data === null) {
|
|
console.warn('Buffer.from called with ' + (data === undefined ? 'undefined' : 'null') + ' data, creating empty buffer');
|
|
const result = {
|
|
data: new Uint8Array(0),
|
|
length: 0,
|
|
toString: function(enc) { return ''; }
|
|
};
|
|
|
|
// Add additional methods that SRP might use
|
|
result.slice = function() { return Buffer.from([]); };
|
|
result.readUInt32BE = function() { return 0; };
|
|
result.writeUInt32BE = function() { return result; };
|
|
|
|
return result;
|
|
}
|
|
|
|
// Special case for hex strings - very important for SRP
|
|
if (typeof data === 'string' && encoding === 'hex') {
|
|
// Convert hex string to byte array
|
|
const bytes = [];
|
|
for (let i = 0; i < data.length; i += 2) {
|
|
if (data.length - i >= 2) {
|
|
bytes.push(parseInt(data.substr(i, 2), 16));
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: bytes.length,
|
|
toString: function(enc) {
|
|
if (enc === 'hex' || !enc) {
|
|
return data; // Return original hex string
|
|
}
|
|
return bytes.map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add methods needed by SRP
|
|
result.slice = function(start, end) {
|
|
const slicedData = bytes.slice(start, end);
|
|
return Buffer.from(slicedData.map(b => b.toString(16).padStart(2, '0')).join(''), 'hex');
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle string data
|
|
if (typeof data === 'string') {
|
|
const bytes = Array.from(data).map(c => c.charCodeAt(0));
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: bytes.length,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return data;
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
return Buffer.from(data.slice(start, end));
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle array/buffer data
|
|
if (Array.isArray(data) || ArrayBuffer.isView(data) || (data instanceof ArrayBuffer)) {
|
|
const bytes = Array.isArray(data) ? data : new Uint8Array(data.buffer || data);
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: bytes.length,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return Array.from(bytes).map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
return Buffer.from(bytes.slice(start, end));
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle object data (last resort)
|
|
if (typeof data === 'object') {
|
|
console.warn('Buffer.from called with object type', data);
|
|
const result = {
|
|
data: data,
|
|
length: data.length || 0,
|
|
toString: function() { return JSON.stringify(data); }
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function() { return Buffer.from({}); };
|
|
result.readUInt32BE = function() { return 0; };
|
|
result.writeUInt32BE = function() { return result; };
|
|
|
|
return result;
|
|
}
|
|
|
|
// Default fallback for any other type
|
|
console.warn('Buffer.from called with unsupported type:', typeof data);
|
|
const result = {
|
|
data: new Uint8Array(0),
|
|
length: 0,
|
|
toString: function() { return ''; },
|
|
slice: function() { return Buffer.from([]); },
|
|
readUInt32BE: function() { return 0; },
|
|
writeUInt32BE: function() { return result; }
|
|
};
|
|
|
|
return result;
|
|
},
|
|
|
|
isBuffer: function(obj) {
|
|
return obj && (obj.data !== undefined || (originalBuffer && originalBuffer.isBuffer && originalBuffer.isBuffer(obj)));
|
|
},
|
|
|
|
alloc: function(size, fill = 0) {
|
|
const bytes = new Array(size).fill(fill);
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: size,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return bytes.map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
return Buffer.from(bytes.slice(start, end));
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
},
|
|
|
|
concat: function(list) {
|
|
if (!Array.isArray(list) || list.length === 0) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
|
|
// Combine all buffers into one
|
|
const totalLength = list.reduce((acc, buf) => acc + (buf ? (buf.length || 0) : 0), 0);
|
|
const combinedArray = new Uint8Array(totalLength);
|
|
|
|
let offset = 0;
|
|
for (const buf of list) {
|
|
if (buf && buf.data) {
|
|
const data = buf.data instanceof Uint8Array ? buf.data : new Uint8Array(buf.data);
|
|
combinedArray.set(data, offset);
|
|
offset += buf.length;
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
data: combinedArray,
|
|
length: totalLength,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return Array.from(combinedArray).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return Array.from(combinedArray).map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
const slicedData = combinedArray.slice(start, end);
|
|
return Buffer.from(slicedData);
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < combinedArray.length ? combinedArray[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < combinedArray.length) {
|
|
combinedArray[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Add missing crypto methods that SRP might need
|
|
if (window.crypto) {
|
|
if (!window.crypto.randomBytes) {
|
|
window.crypto.randomBytes = function(size) {
|
|
const array = new Uint8Array(size);
|
|
window.crypto.getRandomValues(array);
|
|
return Buffer.from(array);
|
|
};
|
|
}
|
|
|
|
// Add cryptographic hash functions if needed
|
|
if (!window.crypto.createHash) {
|
|
window.crypto.createHash = function(algorithm) {
|
|
return {
|
|
update: function(data) {
|
|
this.data = data;
|
|
return this;
|
|
},
|
|
digest: async function(encoding) {
|
|
// Use the SubtleCrypto API for actual hashing
|
|
const dataBuffer = typeof this.data === 'string' ?
|
|
new TextEncoder().encode(this.data) :
|
|
this.data;
|
|
|
|
let hashBuffer;
|
|
try {
|
|
if (algorithm === 'sha256') {
|
|
hashBuffer = await window.crypto.subtle.digest('SHA-256', dataBuffer);
|
|
} else if (algorithm === 'sha1') {
|
|
hashBuffer = await window.crypto.subtle.digest('SHA-1', dataBuffer);
|
|
} else {
|
|
console.error('Unsupported hash algorithm:', algorithm);
|
|
return Buffer.alloc(32); // Return empty buffer as fallback
|
|
}
|
|
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
|
|
if (encoding === 'hex') {
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
return Buffer.from(hashArray);
|
|
} catch (e) {
|
|
console.error('Hash calculation failed:', e);
|
|
return Buffer.alloc(32); // Return empty buffer as fallback
|
|
}
|
|
}
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
// Patch the SRP implementation for browser compatibility
|
|
if (!window.process) {
|
|
window.process = {
|
|
env: {
|
|
NODE_ENV: 'production'
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add any missing process methods
|
|
window.process.nextTick = window.process.nextTick || function(fn) {
|
|
setTimeout(fn, 0);
|
|
};
|
|
|
|
console.log('Ente URL and SRP patches applied successfully');
|
|
})();
|
|
ENDPATCHES
|
|
|
|
# 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"
|
|
|
|
# Fix any potential issues with the head tag
|
|
if ! grep -q "<head>" "/app/data/web/$app_dir/index.html"; then
|
|
echo "==> Warning: No head tag found in $app_dir/index.html, adding one"
|
|
sed -i 's/<html>/<html>\n<head><\/head>/' "/app/data/web/$app_dir/index.html"
|
|
fi
|
|
|
|
# Insert config scripts right after the opening head tag
|
|
sed -i 's/<head>/<head>\n <script src="\/polyfills.js" type="text\/javascript"><\/script>\n <script src="\/config.js" type="text\/javascript"><\/script>\n <script src="\/runtime-config.js" type="text\/javascript"><\/script>\n <script src="\/ente-patches.js" type="text\/javascript"><\/script>/' "/app/data/web/$app_dir/index.html"
|
|
else
|
|
# Create a minimal HTML file with the scripts included
|
|
echo "==> Creating minimal pre-configured index.html for $app_dir app with redirect"
|
|
cat > "/app/data/web/$app_dir/index.html" << HTMLFILE
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<script src="/polyfills.js" type="text/javascript"></script>
|
|
<script src="/config.js" type="text/javascript"></script>
|
|
<script src="/runtime-config.js" type="text/javascript"></script>
|
|
<script src="/ente-patches.js" type="text/javascript"></script>
|
|
<meta http-equiv="refresh" content="0;url=/app/web/$app_dir/index.html">
|
|
<title>Ente $app_dir</title>
|
|
</head>
|
|
<body>
|
|
<h1>Ente $app_dir</h1>
|
|
<p>Loading...</p>
|
|
<p>If this page doesn't redirect automatically, <a href="/app/web/$app_dir/index.html">click here</a>.</p>
|
|
</body>
|
|
</html>
|
|
HTMLFILE
|
|
fi
|
|
done
|
|
|
|
# Create Caddy configuration file
|
|
mkdir -p /app/data/caddy
|
|
cat << EOF > /app/data/caddy/Caddyfile
|
|
# Global settings
|
|
{
|
|
admin off
|
|
auto_https off
|
|
http_port $CADDY_PORT
|
|
https_port 0
|
|
}
|
|
|
|
# Main site configuration
|
|
:$CADDY_PORT {
|
|
# Basic logging
|
|
log {
|
|
level INFO
|
|
output file /app/data/logs/caddy.log
|
|
}
|
|
|
|
# Configuration scripts - directly served
|
|
handle /config.js {
|
|
header Content-Type application/javascript
|
|
respond "
|
|
// Direct configuration for Ente
|
|
window.ENTE_CONFIG = {
|
|
API_URL: 'https://${CLOUDRON_APP_FQDN}/api',
|
|
PUBLIC_ALBUMS_URL: 'https://${CLOUDRON_APP_FQDN}/public'
|
|
};
|
|
|
|
// Add Node.js polyfills for browser environment
|
|
window.process = window.process || {};
|
|
window.process.env = window.process.env || {};
|
|
window.process.nextTick = window.process.nextTick || function(fn) { setTimeout(fn, 0); };
|
|
window.process.browser = true;
|
|
window.Buffer = window.Buffer || (function() { return { isBuffer: function() { return false; } }; })();
|
|
|
|
// Next.js environment variables
|
|
window.process.env.NEXT_PUBLIC_BASE_URL = 'https://${CLOUDRON_APP_FQDN}';
|
|
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 URLs are explicitly defined with full domain
|
|
console.log('Node.js polyfills loaded');
|
|
console.log('process.nextTick available:', !!window.process.nextTick);
|
|
console.log('BASE_URL:', window.process.env.NEXT_PUBLIC_BASE_URL);
|
|
console.log('Ente config loaded - API_URL:', window.ENTE_CONFIG.API_URL);
|
|
console.log('Ente config loaded - PUBLIC_ALBUMS_URL:', window.ENTE_CONFIG.PUBLIC_ALBUMS_URL);
|
|
"
|
|
}
|
|
|
|
handle /runtime-config.js {
|
|
root * /app/data/web
|
|
file_server
|
|
}
|
|
|
|
# Add before the root path section
|
|
handle /polyfills.js {
|
|
header Content-Type application/javascript
|
|
respond "
|
|
// Node.js polyfills for browsers
|
|
window.process = window.process || {};
|
|
window.process.env = window.process.env || {};
|
|
window.process.nextTick = window.process.nextTick || function(fn) { setTimeout(fn, 0); };
|
|
window.process.browser = true;
|
|
|
|
// Buffer polyfill
|
|
window.Buffer = window.Buffer || (function() {
|
|
return {
|
|
isBuffer: function() { return false; },
|
|
from: function(data) { return { data: data }; }
|
|
};
|
|
})();
|
|
|
|
// URL polyfill helper
|
|
window.ensureValidURL = function(url) {
|
|
if (!url) return 'https://${CLOUDRON_APP_FQDN}';
|
|
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
return 'https://${CLOUDRON_APP_FQDN}' + (url.startsWith('/') ? url : '/' + url);
|
|
};
|
|
|
|
console.log('Polyfills loaded successfully');
|
|
"
|
|
}
|
|
|
|
# Root path serves the photos app
|
|
handle / {
|
|
# Special handling for index.html
|
|
@is_index path /
|
|
handle @is_index {
|
|
root * /app/data/web/photos
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
# Serve other static files from the original location
|
|
@not_index {
|
|
not path /
|
|
not path /api/*
|
|
not path /public/*
|
|
not path /accounts/*
|
|
not path /auth/*
|
|
not path /cast/*
|
|
}
|
|
handle @not_index {
|
|
root * /app/web/photos
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Next.js static files
|
|
handle /_next/* {
|
|
root * /app/web/photos
|
|
file_server
|
|
}
|
|
|
|
# Common file types headers
|
|
header /*.js Content-Type application/javascript
|
|
header /*.css Content-Type text/css
|
|
header /*.json Content-Type application/json
|
|
header /*.svg Content-Type image/svg+xml
|
|
header /*.woff2 Content-Type font/woff2
|
|
header /_next/static/chunks/*.js Content-Type application/javascript
|
|
header /_next/static/css/*.css Content-Type text/css
|
|
|
|
# Accounts app
|
|
handle /accounts {
|
|
root * /app/data/web/accounts
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle /accounts/* {
|
|
@is_index path /accounts/ /accounts/index.html
|
|
handle @is_index {
|
|
root * /app/data/web
|
|
try_files /accounts/index.html
|
|
file_server
|
|
}
|
|
|
|
@not_index {
|
|
not path /accounts/
|
|
not path /accounts/index.html
|
|
}
|
|
handle @not_index {
|
|
uri strip_prefix /accounts
|
|
root * /app/web/accounts
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Auth app
|
|
handle /auth {
|
|
root * /app/data/web/auth
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle /auth/* {
|
|
@is_index path /auth/ /auth/index.html
|
|
handle @is_index {
|
|
root * /app/data/web
|
|
try_files /auth/index.html
|
|
file_server
|
|
}
|
|
|
|
@not_index {
|
|
not path /auth/
|
|
not path /auth/index.html
|
|
}
|
|
handle @not_index {
|
|
uri strip_prefix /auth
|
|
root * /app/web/auth
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Cast app
|
|
handle /cast {
|
|
root * /app/data/web/cast
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle /cast/* {
|
|
@is_index path /cast/ /cast/index.html
|
|
handle @is_index {
|
|
root * /app/data/web
|
|
try_files /cast/index.html
|
|
file_server
|
|
}
|
|
|
|
@not_index {
|
|
not path /cast/
|
|
not path /cast/index.html
|
|
}
|
|
handle @not_index {
|
|
uri strip_prefix /cast
|
|
root * /app/web/cast
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Main API proxy
|
|
handle /api/* {
|
|
uri strip_prefix /api
|
|
reverse_proxy 0.0.0.0:$API_PORT
|
|
}
|
|
|
|
# Public albums API proxy
|
|
handle /public/* {
|
|
uri strip_prefix /public
|
|
reverse_proxy 0.0.0.0:$PUBLIC_ALBUMS_PORT
|
|
}
|
|
|
|
# Health check endpoints
|
|
handle /health {
|
|
respond "OK"
|
|
}
|
|
|
|
handle /healthcheck {
|
|
respond "OK"
|
|
}
|
|
|
|
handle /api/health {
|
|
uri strip_prefix /api
|
|
reverse_proxy 0.0.0.0:$API_PORT
|
|
}
|
|
|
|
handle /public/health {
|
|
uri strip_prefix /public
|
|
reverse_proxy 0.0.0.0:$PUBLIC_ALBUMS_PORT
|
|
}
|
|
|
|
# Serve Ente client patches
|
|
handle /ente-patches.js {
|
|
header Content-Type application/javascript
|
|
root * /app/data/web
|
|
file_server
|
|
}
|
|
}
|
|
EOF
|
|
|
|
echo "==> Created Caddy config with properly modified HTML files at /app/data/caddy/Caddyfile"
|
|
|
|
# Start Caddy server
|
|
echo "==> Starting Caddy server"
|
|
caddy run --config /app/data/caddy/Caddyfile --adapter caddyfile &
|
|
CADDY_PID=$!
|
|
echo "==> Caddy started with PID $CADDY_PID"
|
|
|
|
# Wait for Caddy to start
|
|
sleep 2
|
|
|
|
# Test Caddy connectivity
|
|
echo "==> Testing Caddy connectivity"
|
|
if curl -s --max-time 2 --fail http://0.0.0.0:$CADDY_PORT/health > /dev/null; then
|
|
echo "==> Caddy is responding on port $CADDY_PORT"
|
|
else
|
|
echo "==> WARNING: Caddy is not responding on port $CADDY_PORT"
|
|
fi
|
|
|
|
# Print summary and URLs
|
|
echo "==> Application is now running"
|
|
echo "==> Access your Ente instance at: ${CLOUDRON_APP_ORIGIN}"
|
|
|
|
# Additional checks to verify connectivity between services
|
|
echo "==> Checking communication between frontend and backend services"
|
|
echo "==> Testing main API communication"
|
|
curl -s --max-time 2 -f http://0.0.0.0:$CADDY_PORT/api/health || echo "==> Warning: Main API endpoint is not responding!"
|
|
echo "==> Main API communication via frontend is working"
|
|
|
|
echo "==> Testing public albums API communication"
|
|
curl -s --max-time 2 -f http://0.0.0.0:$CADDY_PORT/public/health || echo "==> Warning: Public Albums API endpoint is not responding!"
|
|
echo "==> Public Albums API communication via frontend is working"
|
|
|
|
echo "==> Testing frontend config.js"
|
|
curl -s --max-time 2 -f http://0.0.0.0:$CADDY_PORT/config.js > /dev/null
|
|
echo "==> Frontend configuration is properly loaded"
|
|
|
|
# Go into wait state
|
|
echo "==> Entering wait state - watching logs for registration codes"
|
|
echo "==> Registration verification codes will appear in the logs below"
|
|
echo "==> Press Ctrl+C to stop"
|
|
tail -f /app/data/logs/api_requests.log &
|
|
TAIL_PID=$!
|
|
|
|
# Wait for all processes - safe waiting with proper checks
|
|
if [ -n "${SERVER_PID:-}" ] && [ "${SERVER_PID:-0}" -ne 0 ]; then
|
|
wait $SERVER_PID || true
|
|
fi
|
|
|
|
if [ -n "${PUBLIC_SERVER_PID:-}" ] && [ "${PUBLIC_SERVER_PID:-0}" -ne 0 ]; then
|
|
wait $PUBLIC_SERVER_PID || true
|
|
fi
|
|
|
|
if [ -n "${CADDY_PID:-}" ] && [ "${CADDY_PID:-0}" -ne 0 ]; then
|
|
wait $CADDY_PID || true
|
|
fi |