move feature from meta-sort to meta-core
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# =============================================================================
|
||||
# meta-core Standalone Container
|
||||
# Multi-stage build for meta-core as a standalone service
|
||||
#
|
||||
# Includes:
|
||||
# - meta-core Go binary (leader election, Redis, HTTP API, Mount API, Watcher)
|
||||
# - nginx (dashboard, WebDAV, API proxy)
|
||||
# - Redis (leader-managed)
|
||||
# - rclone (remote mount daemon)
|
||||
# - supervisord (process management)
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 1: Build Go binary
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM golang:1.21-alpine AS go-builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy go mod files first for caching
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Download dependencies and update go.sum
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
# Build the binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o meta-core ./cmd/meta-core
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 2: Build Dashboard
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:21-alpine AS dashboard-builder
|
||||
WORKDIR /app
|
||||
COPY dashboard/package*.json ./
|
||||
RUN npm install
|
||||
COPY dashboard/ .
|
||||
RUN npm run build
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 3: Build Metadata Editor (standalone)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:21-alpine AS editor-builder
|
||||
WORKDIR /app
|
||||
COPY editor/package*.json ./
|
||||
RUN npm install
|
||||
COPY editor/ .
|
||||
RUN npm run build
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 4: Final runtime image
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM alpine:3.19
|
||||
|
||||
# Container registry metadata
|
||||
LABEL org.opencontainers.image.source=https://github.com/worph/meta-core
|
||||
LABEL org.opencontainers.image.description="MetaMesh meta-core - centralized data layer with leader election, Redis, WebDAV, mounts"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
# Core services
|
||||
redis \
|
||||
nginx \
|
||||
nginx-mod-http-dav-ext \
|
||||
supervisor \
|
||||
rclone \
|
||||
fuse3 \
|
||||
# Mount utilities
|
||||
nfs-utils \
|
||||
cifs-utils \
|
||||
# Tools
|
||||
bash \
|
||||
curl \
|
||||
python3 \
|
||||
ca-certificates \
|
||||
apache2-utils \
|
||||
# Process utilities
|
||||
procps
|
||||
|
||||
# Copy Go binary from builder
|
||||
COPY --from=go-builder /build/meta-core /usr/local/bin/meta-core
|
||||
RUN chmod +x /usr/local/bin/meta-core
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p \
|
||||
/meta-core/locks \
|
||||
/meta-core/db/redis \
|
||||
/meta-core/services \
|
||||
/meta-core/mounts/errors \
|
||||
/files \
|
||||
/app/dashboard/dist \
|
||||
/app/editor/dist \
|
||||
/var/log/supervisor \
|
||||
/var/run/supervisor \
|
||||
/etc/nginx/ssl
|
||||
|
||||
# Copy configuration files
|
||||
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/mount-watcher.sh /app/scripts/mount-watcher.sh
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Copy dashboard static files from builder
|
||||
COPY --from=dashboard-builder /app/dist /app/dashboard/dist
|
||||
|
||||
# Copy editor static files from builder
|
||||
COPY --from=editor-builder /app/dist /app/editor/dist
|
||||
|
||||
# Set permissions
|
||||
RUN chmod +x /app/scripts/mount-watcher.sh /entrypoint.sh
|
||||
|
||||
# Create htpasswd for rclone basic auth (admin:admin)
|
||||
RUN htpasswd -bc /etc/nginx/.htpasswd admin admin
|
||||
|
||||
# Environment defaults
|
||||
ENV META_CORE_PATH=/meta-core \
|
||||
FILES_PATH=/files \
|
||||
SERVICE_NAME=meta-core \
|
||||
SERVICE_VERSION=1.0.0 \
|
||||
META_CORE_HTTP_PORT=9000 \
|
||||
META_CORE_HTTP_HOST=0.0.0.0 \
|
||||
REDIS_PORT=6379 \
|
||||
WATCH_FOLDER_LIST=/files/ \
|
||||
WATCH_INTERVAL_MS=1000
|
||||
|
||||
# Expose ports
|
||||
# 80: nginx (dashboard + API proxy + WebDAV)
|
||||
# 6379: Redis (for other services)
|
||||
EXPOSE 80 6379
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD curl -sf http://localhost/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# meta-core Entrypoint Script
|
||||
#
|
||||
# Initializes directories and starts supervisord
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Create required directories
|
||||
mkdir -p /meta-core/locks
|
||||
mkdir -p /meta-core/db/redis
|
||||
mkdir -p /meta-core/services
|
||||
mkdir -p /meta-core/mounts/errors
|
||||
mkdir -p /files
|
||||
mkdir -p /var/log/supervisor
|
||||
mkdir -p /var/log/nginx
|
||||
mkdir -p /var/run/supervisor
|
||||
|
||||
# Ensure proper permissions
|
||||
chmod 755 /meta-core
|
||||
chmod 755 /files
|
||||
|
||||
# Initialize mounts config if not exists
|
||||
if [ ! -f /meta-core/mounts/mounts.json ]; then
|
||||
echo '{"version":1,"mounts":[]}' > /meta-core/mounts/mounts.json
|
||||
fi
|
||||
|
||||
echo "[entrypoint] meta-core starting..."
|
||||
echo "[entrypoint] META_CORE_PATH=${META_CORE_PATH:-/meta-core}"
|
||||
echo "[entrypoint] FILES_PATH=${FILES_PATH:-/files}"
|
||||
echo "[entrypoint] SERVICE_NAME=${SERVICE_NAME:-meta-core}"
|
||||
echo "[entrypoint] WATCH_FOLDER_LIST=${WATCH_FOLDER_LIST:-/files/}"
|
||||
|
||||
# Execute the main command (supervisord)
|
||||
exec "$@"
|
||||
@@ -0,0 +1,386 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# Mount Watcher Script
|
||||
#
|
||||
# Monitors /meta-core/mounts/mounts.json and manages NFS, SMB, and rclone mounts.
|
||||
# Runs as a supervisord program, polling every 5 seconds.
|
||||
#
|
||||
# Uses findmnt to check actual mount status (source of truth).
|
||||
# Writes errors to /meta-core/mounts/errors/{id}.error
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
CONFIG_DIR="${META_CORE_PATH:-/meta-core}/mounts"
|
||||
CONFIG_FILE="$CONFIG_DIR/mounts.json"
|
||||
ERROR_DIR="$CONFIG_DIR/errors"
|
||||
FILES_PATH="${FILES_PATH:-/files}"
|
||||
POLL_INTERVAL="${MOUNT_POLL_INTERVAL:-5}"
|
||||
|
||||
# ANSI colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() {
|
||||
echo -e "[$(date -Iseconds)] [${CYAN}mount-watcher${NC}] $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "[$(date -Iseconds)] [${CYAN}mount-watcher${NC}] ${GREEN}$1${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "[$(date -Iseconds)] [${CYAN}mount-watcher${NC}] ${YELLOW}$1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "[$(date -Iseconds)] [${CYAN}mount-watcher${NC}] ${RED}$1${NC}"
|
||||
}
|
||||
|
||||
# Initialize directories
|
||||
init_dirs() {
|
||||
mkdir -p "$CONFIG_DIR" "$ERROR_DIR"
|
||||
|
||||
# Initialize empty config if not exists
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo '{"version":1,"mounts":[]}' > "$CONFIG_FILE"
|
||||
log "Initialized empty mounts config"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if path is mounted
|
||||
is_mounted() {
|
||||
local mount_path="$1"
|
||||
findmnt -n "$mount_path" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Clear error file for mount
|
||||
clear_error() {
|
||||
local id="$1"
|
||||
rm -f "$ERROR_DIR/$id.error"
|
||||
}
|
||||
|
||||
# Write error to file
|
||||
write_error() {
|
||||
local id="$1"
|
||||
local error="$2"
|
||||
echo "$(date -Iseconds)" > "$ERROR_DIR/$id.error"
|
||||
echo "$error" >> "$ERROR_DIR/$id.error"
|
||||
}
|
||||
|
||||
# Mount NFS share (always read-only)
|
||||
do_mount_nfs() {
|
||||
local id="$1"
|
||||
local server="$2"
|
||||
local path="$3"
|
||||
local mount_path="$4"
|
||||
local options="${5:-}"
|
||||
|
||||
mkdir -p "$mount_path"
|
||||
|
||||
# Always mount read-only for safety
|
||||
local mount_opts="ro"
|
||||
[ -n "$options" ] && mount_opts="${mount_opts},${options}"
|
||||
|
||||
log "Mounting NFS (read-only): ${server}:${path} -> ${mount_path}"
|
||||
output=$(mount -t nfs -o "${mount_opts}" "${server}:${path}" "${mount_path}" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
clear_error "$id"
|
||||
log_success "NFS mount successful: $mount_path"
|
||||
else
|
||||
write_error "$id" "$output"
|
||||
log_error "NFS mount failed: $output"
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
# Mount SMB/CIFS share (always read-only)
|
||||
do_mount_smb() {
|
||||
local id="$1"
|
||||
local server="$2"
|
||||
local share="$3"
|
||||
local mount_path="$4"
|
||||
local username="${5:-}"
|
||||
local password_obscured="${6:-}"
|
||||
local domain="${7:-}"
|
||||
local options="${8:-}"
|
||||
|
||||
mkdir -p "$mount_path"
|
||||
|
||||
# Always mount read-only for safety
|
||||
local mount_opts="ro"
|
||||
|
||||
# If we have credentials, reveal password and build options
|
||||
if [ -n "$username" ]; then
|
||||
mount_opts="${mount_opts},username=${username}"
|
||||
|
||||
if [ -n "$password_obscured" ]; then
|
||||
# Reveal password using rclone (only exists in memory briefly)
|
||||
local password
|
||||
password=$(rclone reveal "$password_obscured" 2>/dev/null)
|
||||
if [ -n "$password" ]; then
|
||||
mount_opts="${mount_opts},password=${password}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$domain" ]; then
|
||||
mount_opts="${mount_opts},domain=${domain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add any additional options
|
||||
if [ -n "$options" ]; then
|
||||
if [ -n "$mount_opts" ]; then
|
||||
mount_opts="${mount_opts},${options}"
|
||||
else
|
||||
mount_opts="$options"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Mounting SMB (read-only): //${server}/${share} -> ${mount_path}"
|
||||
|
||||
# Execute mount directly (not via eval) to handle special chars in password
|
||||
local output
|
||||
local rc
|
||||
if [ -n "$mount_opts" ]; then
|
||||
output=$(mount -t cifs "//${server}/${share}" "${mount_path}" -o "${mount_opts}" 2>&1)
|
||||
rc=$?
|
||||
else
|
||||
output=$(mount -t cifs "//${server}/${share}" "${mount_path}" 2>&1)
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
# Clear password from any variables (best effort)
|
||||
password=""
|
||||
mount_opts=""
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
clear_error "$id"
|
||||
log_success "SMB mount successful: $mount_path"
|
||||
else
|
||||
write_error "$id" "$output"
|
||||
log_error "SMB mount failed: $output"
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
# Mount rclone remote using RC API (always read-only)
|
||||
do_mount_rclone() {
|
||||
local id="$1"
|
||||
local remote="$2"
|
||||
local remote_path="$3"
|
||||
local mount_path="$4"
|
||||
local options="${5:-}"
|
||||
|
||||
mkdir -p "$mount_path"
|
||||
|
||||
# Build the remote source string
|
||||
local fs="${remote}:${remote_path}"
|
||||
|
||||
# Use rclone RC API to mount (always read-only for safety)
|
||||
log "Mounting rclone (read-only): ${fs} -> ${mount_path}"
|
||||
|
||||
# Build JSON body for rclone mount with read-only option
|
||||
local json_body
|
||||
json_body=$(cat <<EOF
|
||||
{
|
||||
"fs": "${fs}",
|
||||
"mountPoint": "${mount_path}",
|
||||
"mountOpt": {
|
||||
"AllowOther": true,
|
||||
"ReadOnly": true
|
||||
},
|
||||
"vfsOpt": {
|
||||
"CacheMode": 2,
|
||||
"ReadOnly": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
output=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-u admin:admin \
|
||||
-d "$json_body" \
|
||||
"http://127.0.0.1:5572/mount/mount" 2>&1)
|
||||
|
||||
# Wait a moment for mount to establish
|
||||
sleep 2
|
||||
|
||||
if is_mounted "$mount_path"; then
|
||||
clear_error "$id"
|
||||
log_success "rclone mount successful: $mount_path"
|
||||
return 0
|
||||
else
|
||||
local error_msg="rclone mount failed"
|
||||
# Try to extract error from response
|
||||
if echo "$output" | grep -q "error"; then
|
||||
error_msg="$output"
|
||||
fi
|
||||
write_error "$id" "$error_msg"
|
||||
log_error "rclone mount failed: $error_msg"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Unmount a path
|
||||
do_unmount() {
|
||||
local id="$1"
|
||||
local mount_path="$2"
|
||||
local type="$3"
|
||||
|
||||
if ! is_mounted "$mount_path"; then
|
||||
log "Already unmounted: $mount_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$type" = "rclone" ]; then
|
||||
# Use rclone RC API to unmount
|
||||
log "Unmounting rclone: $mount_path"
|
||||
curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-u admin:admin \
|
||||
-d "{\"mountPoint\":\"${mount_path}\"}" \
|
||||
"http://127.0.0.1:5572/mount/unmount" > /dev/null 2>&1
|
||||
else
|
||||
log "Unmounting: $mount_path"
|
||||
umount "$mount_path" 2>&1
|
||||
fi
|
||||
|
||||
# Wait a moment
|
||||
sleep 1
|
||||
|
||||
# Check if still mounted
|
||||
if is_mounted "$mount_path"; then
|
||||
# Force unmount with lazy option
|
||||
log_warn "Force unmounting (lazy): $mount_path"
|
||||
umount -l "$mount_path" 2>&1
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
if is_mounted "$mount_path"; then
|
||||
log_error "Failed to unmount: $mount_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "Unmounted: $mount_path"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Process all mounts from config
|
||||
process_mounts() {
|
||||
# Check if config file exists
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Parse JSON using python3
|
||||
local mounts
|
||||
mounts=$(python3 -c '
|
||||
import sys, json
|
||||
|
||||
try:
|
||||
with open("'"$CONFIG_FILE"'", "r") as f:
|
||||
data = json.load(f)
|
||||
except:
|
||||
sys.exit(0)
|
||||
|
||||
for m in data.get("mounts", []):
|
||||
# Output pipe-separated values
|
||||
fields = [
|
||||
m.get("id", ""),
|
||||
m.get("type", ""),
|
||||
str(m.get("enabled", True)),
|
||||
str(m.get("desiredMounted", False)),
|
||||
m.get("mountPath", ""),
|
||||
m.get("nfsServer", ""),
|
||||
m.get("nfsPath", ""),
|
||||
m.get("smbServer", ""),
|
||||
m.get("smbShare", ""),
|
||||
m.get("smbUsername", ""),
|
||||
m.get("smbPasswordObscured", ""),
|
||||
m.get("smbDomain", ""),
|
||||
m.get("rcloneRemote", ""),
|
||||
m.get("rclonePath", ""),
|
||||
m.get("options", "")
|
||||
]
|
||||
print("|".join(fields))
|
||||
' 2>/dev/null)
|
||||
|
||||
if [ -z "$mounts" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS='|' read -r id type enabled desired mount_path nfs_server nfs_path smb_server smb_share smb_username smb_password_obscured smb_domain rclone_remote rclone_path options; do
|
||||
# Skip empty lines
|
||||
[ -z "$id" ] && continue
|
||||
|
||||
# Check actual mount status
|
||||
local is_mounted_now=false
|
||||
is_mounted "$mount_path" && is_mounted_now=true
|
||||
|
||||
# Handle disabled mounts - unmount if currently mounted
|
||||
if [ "$enabled" != "True" ] && [ "$enabled" != "true" ]; then
|
||||
if [ "$is_mounted_now" = true ]; then
|
||||
log "Mount $id ($mount_path) disabled, unmounting..."
|
||||
do_unmount "$id" "$mount_path" "$type"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# Handle desired state
|
||||
if [ "$desired" = "True" ] || [ "$desired" = "true" ]; then
|
||||
# Should be mounted
|
||||
if [ "$is_mounted_now" = false ]; then
|
||||
log "Mount $id ($mount_path) desired but not mounted, mounting..."
|
||||
case "$type" in
|
||||
nfs)
|
||||
do_mount_nfs "$id" "$nfs_server" "$nfs_path" "$mount_path" "$options"
|
||||
;;
|
||||
smb)
|
||||
do_mount_smb "$id" "$smb_server" "$smb_share" "$mount_path" "$smb_username" "$smb_password_obscured" "$smb_domain" "$options"
|
||||
;;
|
||||
rclone)
|
||||
do_mount_rclone "$id" "$rclone_remote" "$rclone_path" "$mount_path" "$options"
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown mount type: $type"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
# Should be unmounted
|
||||
if [ "$is_mounted_now" = true ]; then
|
||||
log "Mount $id ($mount_path) not desired, unmounting..."
|
||||
do_unmount "$id" "$mount_path" "$type"
|
||||
fi
|
||||
fi
|
||||
done <<< "$mounts"
|
||||
}
|
||||
|
||||
# Main loop
|
||||
main() {
|
||||
log "Starting mount watcher (poll interval: ${POLL_INTERVAL}s)"
|
||||
|
||||
init_dirs
|
||||
|
||||
while true; do
|
||||
process_mounts
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# Handle signals for graceful shutdown
|
||||
cleanup() {
|
||||
log "Shutting down mount watcher..."
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,231 @@
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
|
||||
# Upstream: meta-core Go API
|
||||
upstream meta-core-api {
|
||||
server 127.0.0.1:9000;
|
||||
}
|
||||
|
||||
# Upstream: rclone RC API
|
||||
upstream rclone {
|
||||
server 127.0.0.1:5572;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# =========================================================================
|
||||
# Dashboard (React SPA)
|
||||
# =========================================================================
|
||||
root /app/dashboard/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Go API routes
|
||||
# All /api/* routes are proxied to meta-core Go server
|
||||
# =========================================================================
|
||||
location /api/ {
|
||||
proxy_pass http://meta-core-api/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# SSE support for /api/events/subscribe
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Health check (direct to Go API)
|
||||
# =========================================================================
|
||||
location /health {
|
||||
proxy_pass http://meta-core-api/health;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Metadata API (for services to access metadata)
|
||||
# =========================================================================
|
||||
location /meta/ {
|
||||
proxy_pass http://meta-core-api/meta/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /meta {
|
||||
proxy_pass http://meta-core-api/meta;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# File/Data API endpoints
|
||||
# =========================================================================
|
||||
location /file/ {
|
||||
proxy_pass http://meta-core-api/file/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /data/ {
|
||||
proxy_pass http://meta-core-api/data/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Service discovery
|
||||
# =========================================================================
|
||||
location /services {
|
||||
proxy_pass http://meta-core-api/services;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /services/ {
|
||||
proxy_pass http://meta-core-api/services/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Leader/role endpoints
|
||||
# =========================================================================
|
||||
location /leader {
|
||||
proxy_pass http://meta-core-api/leader;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /role {
|
||||
proxy_pass http://meta-core-api/role;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# rclone Web UI (proxied with basic auth)
|
||||
# =========================================================================
|
||||
location /rclone/ {
|
||||
proxy_pass http://rclone/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Basic auth for rclone
|
||||
auth_basic "rclone";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Metadata Editor (standalone React app)
|
||||
# Serves static files for the metadata editor
|
||||
# API routes (/api/metadata/*, /api/kv/*) are handled by the Go API
|
||||
# via the /api/ proxy above
|
||||
# =========================================================================
|
||||
location /editor/ {
|
||||
alias /app/editor/dist/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /editor/index.html;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# WebDAV HTML browser (for iframe embedding in dashboard)
|
||||
# Returns HTML directory listings instead of JSON
|
||||
# =========================================================================
|
||||
location /webdav-browser/ {
|
||||
alias /files/;
|
||||
autoindex on;
|
||||
# No autoindex_format = defaults to HTML
|
||||
charset utf-8;
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# WebDAV endpoint for file access
|
||||
# Exposes /files directory for read/write operations
|
||||
# Supports: GET (read), PUT (write), DELETE, MKCOL (mkdir), PROPFIND
|
||||
# =========================================================================
|
||||
location /webdav/ {
|
||||
alias /files/;
|
||||
|
||||
# WebDAV methods
|
||||
dav_methods PUT DELETE MKCOL COPY MOVE;
|
||||
dav_access user:rw group:rw all:r;
|
||||
|
||||
# Enable autoindex for directory listings (alternative to PROPFIND)
|
||||
autoindex on;
|
||||
autoindex_format json;
|
||||
|
||||
# Allow creating directories automatically
|
||||
create_full_put_path on;
|
||||
|
||||
# No size limit for uploads (large media files)
|
||||
client_max_body_size 0;
|
||||
|
||||
# Optimize for large file transfers
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
|
||||
# Output buffering for large files
|
||||
output_buffers 1 512k;
|
||||
aio threads;
|
||||
|
||||
# CORS headers for cross-container access
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Depth, Range, If-Range' always;
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range, Accept-Ranges' always;
|
||||
|
||||
# Handle OPTIONS preflight
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Depth, Range, If-Range';
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
loglevel=info
|
||||
|
||||
[unix_http_server]
|
||||
file=/var/run/supervisor.sock
|
||||
chmod=0700
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///var/run/supervisor.sock
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
# =============================================================================
|
||||
# meta-core: Leader election, Redis management, HTTP API, Mount API, File Watcher
|
||||
# =============================================================================
|
||||
[program:meta-core]
|
||||
command=/usr/local/bin/meta-core
|
||||
priority=5
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startsecs=3
|
||||
environment=
|
||||
META_CORE_PATH="%(ENV_META_CORE_PATH)s",
|
||||
FILES_PATH="%(ENV_FILES_PATH)s",
|
||||
SERVICE_NAME="%(ENV_SERVICE_NAME)s",
|
||||
SERVICE_VERSION="%(ENV_SERVICE_VERSION)s",
|
||||
META_CORE_HTTP_PORT="%(ENV_META_CORE_HTTP_PORT)s",
|
||||
META_CORE_HTTP_HOST="%(ENV_META_CORE_HTTP_HOST)s",
|
||||
REDIS_PORT="%(ENV_REDIS_PORT)s",
|
||||
WATCH_FOLDER_LIST="%(ENV_WATCH_FOLDER_LIST)s",
|
||||
WATCH_INTERVAL_MS="%(ENV_WATCH_INTERVAL_MS)s",
|
||||
API_PORT="80",
|
||||
BASE_URL="%(ENV_BASE_URL)s"
|
||||
|
||||
# =============================================================================
|
||||
# nginx: Dashboard, API proxy, WebDAV file serving
|
||||
# =============================================================================
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g "daemon off;"
|
||||
priority=20
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/supervisor/nginx.log
|
||||
stderr_logfile=/var/log/supervisor/nginx_error.log
|
||||
startsecs=2
|
||||
|
||||
# =============================================================================
|
||||
# rclone: Remote storage mount daemon (RC API on port 5572)
|
||||
# =============================================================================
|
||||
[program:rclone]
|
||||
command=/usr/bin/rclone rcd --rc-web-gui --rc-web-gui-no-open-browser --rc-addr=:5572 --rc-user=admin --rc-pass=admin
|
||||
priority=30
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/supervisor/rclone.log
|
||||
stderr_logfile=/var/log/supervisor/rclone_error.log
|
||||
startsecs=3
|
||||
|
||||
# =============================================================================
|
||||
# mount-watcher: Monitors mounts.json and manages NFS/SMB/rclone mounts
|
||||
# =============================================================================
|
||||
[program:mount-watcher]
|
||||
command=/app/scripts/mount-watcher.sh
|
||||
priority=35
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/supervisor/mount-watcher.log
|
||||
stderr_logfile=/var/log/supervisor/mount-watcher_error.log
|
||||
startsecs=3
|
||||
environment=
|
||||
META_CORE_PATH="%(ENV_META_CORE_PATH)s",
|
||||
FILES_PATH="%(ENV_FILES_PATH)s"
|
||||
Reference in New Issue
Block a user