#!/bin/bash set -e # Declare that we are using a Cloudron environment echo "==> Starting Ente Cloudron app..." echo "==> NOTE: Running in Cloudron environment with limited write access" echo "==> Writable directories: /app/data, /tmp, /run" # Create necessary data directories mkdir -p /app/data/logs mkdir -p /app/data/ente/web mkdir -p /app/data/ente/server mkdir -p /app/data/web/photos/static mkdir -p /app/data/web/accounts/static mkdir -p /app/data/web/auth/static mkdir -p /app/data/web/cast/static # Use the specified server directory or default to the data dir SERVER_DIR="/app/data/ente/server" echo "==> Using server directory: $SERVER_DIR" # Download Ente server if not already present if [ ! -d "$SERVER_DIR/museum" ] || [ ! -f "$SERVER_DIR/museum/museum" ]; then echo "==> Downloading Ente Museum server..." mkdir -p "$SERVER_DIR" cd "$SERVER_DIR" # Clone the repository if it doesn't exist if [ ! -d "$SERVER_DIR/museum" ]; then git clone https://github.com/ente-io/museum.git cd museum else cd museum git pull fi # Build the museum server echo "==> Building Ente Museum server..." go build -o museum if [ ! -f "$SERVER_DIR/museum/museum" ]; then echo "==> ERROR: Failed to build museum server" echo "==> Will attempt to download pre-built binary" # Try to download pre-built binary ARCH=$(uname -m) OS=$(uname -s | tr '[:upper:]' '[:lower:]') if [ "$ARCH" = "x86_64" ]; then ARCH="amd64" elif [ "$ARCH" = "aarch64" ]; then ARCH="arm64" fi RELEASE_URL="https://github.com/ente-io/museum/releases/latest/download/museum-$OS-$ARCH" curl -L -o "$SERVER_DIR/museum/museum" "$RELEASE_URL" chmod +x "$SERVER_DIR/museum/museum" if [ ! -f "$SERVER_DIR/museum/museum" ]; then echo "==> ERROR: Failed to download pre-built binary" echo "==> Will create directory structure for future installation" mkdir -p "$SERVER_DIR/museum/config" fi fi else echo "==> Ente Museum server already downloaded" fi # Configure S3 storage for Ente if [ -f "/app/data/s3_config.env" ]; then echo "==> Using existing S3 configuration" source /app/data/s3_config.env echo "==> S3 Configuration:" echo "Endpoint: $S3_ENDPOINT" echo "Region: $S3_REGION" echo "Bucket: $S3_BUCKET" else # Default to environment variables if they exist if [ -n "$CLOUDRON_S3_ENDPOINT" ] && [ -n "$CLOUDRON_S3_KEY" ] && [ -n "$CLOUDRON_S3_SECRET" ]; then echo "==> Using Cloudron S3 configuration" S3_ENDPOINT="$CLOUDRON_S3_ENDPOINT" S3_REGION="us-east-1" # Default region, can be overridden S3_BUCKET="${CLOUDRON_APP_DOMAIN//./-}-ente" S3_ACCESS_KEY="$CLOUDRON_S3_KEY" S3_SECRET_KEY="$CLOUDRON_S3_SECRET" # Save for future runs mkdir -p /app/data cat > /app/data/s3_config.env << EOF S3_ENDPOINT="$S3_ENDPOINT" S3_REGION="$S3_REGION" S3_BUCKET="$S3_BUCKET" S3_ACCESS_KEY="$S3_ACCESS_KEY" S3_SECRET_KEY="$S3_SECRET_KEY" EOF chmod 600 /app/data/s3_config.env echo "==> Created S3 configuration file" echo "==> S3 Configuration:" echo "Endpoint: $S3_ENDPOINT" echo "Region: $S3_REGION" echo "Bucket: $S3_BUCKET" else echo "==> WARNING: S3 configuration is not found" echo "==> Creating a template S3 configuration for you to fill in" mkdir -p /app/data cat > /app/data/s3_config.env.template << EOF # Rename this file to s3_config.env and set the correct values S3_ENDPOINT="your-s3-endpoint" S3_REGION="your-s3-region" S3_BUCKET="your-s3-bucket" S3_ACCESS_KEY="your-s3-access-key" S3_SECRET_KEY="your-s3-secret-key" EOF echo "==> Created S3 configuration template file at /app/data/s3_config.env.template" echo "==> Please fill in the values and rename it to s3_config.env" fi fi # Configure museum.yaml if [ -f "/app/data/museum.yaml" ]; then echo "==> Using existing museum.yaml configuration" else echo "==> Creating museum.yaml configuration" cat > /app/data/museum.yaml << EOF database: driver: postgres source: postgres://${CLOUDRON_POSTGRESQL_USERNAME}:${CLOUDRON_POSTGRESQL_PASSWORD}@${CLOUDRON_POSTGRESQL_HOST}:${CLOUDRON_POSTGRESQL_PORT}/${CLOUDRON_POSTGRESQL_DATABASE} auto-migrate: true server: port: 8080 host: 0.0.0.0 cors: origins: - https://${CLOUDRON_APP_DOMAIN} methods: - GET - POST - PUT - OPTIONS headers: - Content-Type - Authorization endpoints: photos: https://${CLOUDRON_APP_DOMAIN}/photos accounts: https://${CLOUDRON_APP_DOMAIN}/accounts auth: https://${CLOUDRON_APP_DOMAIN}/auth cast: https://${CLOUDRON_APP_DOMAIN}/cast "public-albums": https://${CLOUDRON_APP_DOMAIN}/public s3: endpoint: ${S3_ENDPOINT} region: ${S3_REGION} bucket: ${S3_BUCKET} access-key-id: ${S3_ACCESS_KEY} secret-access-key: ${S3_SECRET_KEY} cache-control: public, max-age=31536000 acme: enabled: false # Cloudron handles SSL smtp: 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}}>" reply-to: "Ente <${CLOUDRON_MAIL_FROM:-no-reply@${CLOUDRON_APP_DOMAIN}}>" logging: level: info file: /app/data/logs/museum.log EOF echo "==> Created museum.yaml configuration" fi # Download and install Ente web app if not already present if [ ! -d "/app/data/ente/web" ] || [ ! -f "/app/data/ente/web/photos/index.html" ]; then echo "==> Downloading Ente web app..." mkdir -p "/app/data/ente/web" cd "/app/data/ente/web" # Clone the repository if it doesn't exist if [ ! -d "/app/data/ente/web/photos" ]; then git clone https://github.com/ente-io/photos.git cd photos else cd photos git pull fi # Try to build the web app echo "==> Building Ente web app (this may take a while)..." if command -v npm &> /dev/null; then npm install npm run build else echo "==> WARNING: npm not found, cannot build web app" echo "==> Will continue with placeholder pages" fi else echo "==> Ente web app already downloaded" fi # Test PostgreSQL connectivity echo "==> Testing PostgreSQL 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 2>&1 if [ $? -eq 0 ]; then echo "==> PostgreSQL is ready" else echo "==> ERROR: Could not connect to PostgreSQL" echo "==> Please check your PostgreSQL configuration" exit 1 fi # Start the real Museum server mkdir -p "${SERVER_DIR}/museum/config" cp /app/data/museum.yaml "${SERVER_DIR}/museum/config/museum.yaml" if [ -f "${SERVER_DIR}/museum/museum" ]; then echo "==> Found Museum server at ${SERVER_DIR}/museum" # Make sure the museum binary is executable chmod +x "${SERVER_DIR}/museum/museum" # Start the real Museum server cd "${SERVER_DIR}/museum" ./museum --config "${SERVER_DIR}/museum/config/museum.yaml" > /app/data/logs/museum.log 2>&1 & MUSEUM_PID=$! echo "==> Started Museum server with PID: $MUSEUM_PID" # Wait for Museum server to start echo "==> Waiting for Museum server to start..." for i in {1..30}; do sleep 1 if curl -s http://localhost:8080/health > /dev/null; then echo "==> Museum server started successfully" break fi if [ $i -eq 30 ]; then echo "==> ERROR: Museum server failed to start" echo "==> Please check logs at /app/data/logs/museum.log" exit 1 fi done else echo "==> ERROR: Museum server not found at ${SERVER_DIR}/museum" echo "==> Please install the Museum server manually" exit 1 fi # Set up Caddy web server echo "==> Setting up Caddy web server" # Create Caddy configuration file cat > /app/data/Caddyfile << 'EOF' { admin off } :3080 { # API endpoints - proxy to Museum server handle /api/* { uri strip_prefix /api reverse_proxy localhost:8080 } # Web applications static content handle /photos/* { uri strip_prefix /photos root * /app/data/web/photos try_files {path} {path}/ /index.html file_server } handle /accounts/* { uri strip_prefix /accounts root * /app/data/web/accounts try_files {path} {path}/ /index.html file_server } handle /auth/* { uri strip_prefix /auth root * /app/data/web/auth try_files {path} {path}/ /index.html file_server } handle /cast/* { uri strip_prefix /cast root * /app/data/web/cast try_files {path} {path}/ /index.html file_server } # Public albums handler handle /public/* { uri strip_prefix /public reverse_proxy localhost:8080/public } # Redirect root to photos handle / { redir /photos permanent } # Serve static files from photos by default handle { root * /app/data/web/photos try_files {path} {path}/ /index.html file_server } # Error handling handle_errors { respond "{http.error.status_code} {http.error.status_text}" } # Logging log { output file /app/data/logs/access.log format console level info } } EOF # Create runtime-config.js in writable location echo "==> Creating runtime-config.js in writable location" cat > /app/data/web/photos/static/runtime-config.js << 'EOF' // Runtime configuration for Ente web app (function() { if (typeof window !== 'undefined') { // Polyfill process for browser environment if (!window.process) { window.process = { env: {}, nextTick: function(cb) { setTimeout(cb, 0); } }; } const BASE_URL = window.location.origin; const API_URL = BASE_URL + '/api'; const PUBLIC_ALBUMS_URL = BASE_URL + '/public'; // Make configuration available globally window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = API_URL; window.process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = PUBLIC_ALBUMS_URL; // Also maintain compatibility with older Ente code window.ENTE_CONFIG = { API_URL: API_URL, PUBLIC_ALBUMS_URL: PUBLIC_ALBUMS_URL }; console.log('Ente runtime config loaded from runtime-config.js with polyfills'); console.log('process.nextTick available:', typeof window.process.nextTick === 'function'); console.log('BASE_URL:', BASE_URL); console.log('API_URL (final):', API_URL); console.log('PUBLIC_ALBUMS_URL (final):', PUBLIC_ALBUMS_URL); console.log('NEXT_PUBLIC_ENTE_ENDPOINT (final):', window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT); } })(); EOF # Copy runtime-config.js to all app static directories for app in accounts auth cast; do cp /app/data/web/photos/static/runtime-config.js /app/data/web/$app/static/ done # Create URL and SRP patch file 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; } }; // 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 # Copy ente-patches.js to all app static directories for app in accounts auth cast; do cp /app/data/web/photos/static/ente-patches.js /app/data/web/$app/static/ done # Create placeholder HTML files for each app if the actual builds don't exist for app in photos accounts auth cast; do if [ ! -f "/app/data/ente/web/$app/index.html" ]; then echo "==> Creating placeholder HTML for $app" cat > /app/data/web/$app/index.html << EOF
End-to-end encrypted photo storage and sharing platform.
This is a placeholder page until the proper Ente build is created.
Check API Status