813 lines
27 KiB
Bash
813 lines
27 KiB
Bash
#!/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
|
|
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 "==> ERROR: S3 configuration is required for Ente"
|
|
echo "==> Please configure S3 storage in the Cloudron app settings"
|
|
echo "==> or create /app/data/s3_config.env manually"
|
|
exit 1
|
|
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
|
|
}
|
|
|
|
:80 {
|
|
# 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
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ente $app</title>
|
|
<script src="/static/runtime-config.js"></script>
|
|
<script src="/static/ente-patches.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #121212;
|
|
color: #fff;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 16px;
|
|
}
|
|
p {
|
|
font-size: 1.1rem;
|
|
line-height: 1.5;
|
|
margin-bottom: 24px;
|
|
color: #ccc;
|
|
}
|
|
.button {
|
|
display: inline-block;
|
|
background-color: #7745ff;
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: bold;
|
|
transition: background-color 0.3s;
|
|
}
|
|
.button:hover {
|
|
background-color: #6535e0;
|
|
}
|
|
.logo {
|
|
width: 120px;
|
|
height: 120px;
|
|
margin-bottom: 24px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<svg class="logo" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="100" height="100" rx="20" fill="#7745FF"/>
|
|
<path d="M50 30C38.9543 30 30 38.9543 30 50C30 61.0457 38.9543 70 50 70C61.0457 70 70 61.0457 70 50C70 38.9543 61.0457 30 50 30ZM50 60C44.4772 60 40 55.5228 40 50C40 44.4772 44.4772 40 50 40C55.5228 40 60 44.4772 60 50C60 55.5228 55.5228 60 50 60Z" fill="white"/>
|
|
</svg>
|
|
<h1>Ente $app</h1>
|
|
<p>End-to-end encrypted photo storage and sharing platform.</p>
|
|
<p>This is a placeholder page until the proper Ente build is created.</p>
|
|
<a href="/api/health" class="button">Check API Status</a>
|
|
</div>
|
|
<script>
|
|
// Check if the API is responsive
|
|
fetch('/api/health')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log('API health check:', data);
|
|
})
|
|
.catch(error => {
|
|
console.error('API health check failed:', error);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
else
|
|
echo "==> Using existing $app web app"
|
|
cp -r "/app/data/ente/web/$app/*" "/app/data/web/$app/"
|
|
fi
|
|
done
|
|
|
|
# Start Caddy
|
|
echo "==> Starting Caddy server"
|
|
caddy run --config /app/data/Caddyfile --adapter caddyfile &
|
|
CADDY_PID=$!
|
|
echo "==> Caddy server started with PID: $CADDY_PID"
|
|
|
|
# Enter a wait state to catch signals
|
|
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/*.log |