Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.7.65
1.7.68
154 changes: 86 additions & 68 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
from flask import Flask, send_from_directory, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
Expand Down Expand Up @@ -649,74 +650,91 @@ def create_app(config_name=None):
import logging
logging.getLogger(__name__).warning(f'Extension backend acquisition: {e}')

# Start metrics history collection in background
from app.services.metrics_history_service import MetricsHistoryService
if not MetricsHistoryService.is_running():
MetricsHistoryService.start_collection(app)

# Start queue-bus webhook consumer
from app.queue_bus.consumers import start_webhook_consumer
start_webhook_consumer(app)

# Start queue-bus notification consumer (delivers in-app/email/chat)
from app.notifications.consumer import start_notification_consumer
start_notification_consumer(app)

# Start the API analytics flush thread (a 5s buffer flush — a real-time
# stream, deliberately NOT modeled as a job).
from app.middleware.api_analytics import start_analytics_flush_thread
start_analytics_flush_thread(app)

# Start the unified job system: ONE consumer runs every enqueued Job and
# ONE scheduler ticks all periodic work. This supersedes the former set
# of per-domain daemon scheduler threads (auto-sync, snapshot-retention,
# workflow, health-check, wp-update, api-background, pairing-prune,
# registrar-expiry) — they are now ScheduledJob rows backed by the
# built-in handlers in app/jobs/builtin_handlers.py.
from app.jobs import start_job_system
from app.jobs.builtin_handlers import register_builtin_handlers, seed_builtin_schedules
register_builtin_handlers()
# Register event-driven job handlers (deployment installs, workflow runs,
# scheduled backups).
from app.services.deployment_job_service import DeploymentJobService
DeploymentJobService.register_jobs()
# WorkflowEngine.register_jobs() removed in plan 45 Phase 4 (engine retired).
from app.services.backup_service import BackupService
BackupService.register_jobs()
from app.services.backup_policy_service import BackupPolicyService
BackupPolicyService.register_jobs()
from app.services.server_onboarding_service import ServerOnboardingService
ServerOnboardingService.register_jobs()
from app.services.preview_service import PreviewService
PreviewService.register_jobs()
from app.services.metadata_guard_service import MetadataGuardService
MetadataGuardService.register_jobs()
if not app.config.get('TESTING'):
MetadataGuardService.ensure() # converge the metadata egress rule (no-op when unsupported)
from app.services.speed_test_service import SpeedTestService
SpeedTestService.register_jobs()
from app.services import login_link_service
login_link_service.register_jobs()
from app.services.db_admin_sso_service import DbAdminSsoService
DbAdminSsoService.register_jobs()
from app.services.site_import_service import SiteImportService
SiteImportService.register_jobs()
from app.services.drift_service import DriftService
DriftService.register_jobs()
from app.services.doctor_service import DoctorService
DoctorService.register_jobs()
from app.services.file_integrity_service import FileIntegrityService
FileIntegrityService.register_jobs()
from app.services.malware_scan_service import MalwareScanService
MalwareScanService.register_jobs()
from app.services.bandwidth_service import BandwidthService
BandwidthService.register_jobs()
start_job_system(app, seed=seed_builtin_schedules)

# Resume the embedded agent when this panel is linked to a master
# ServerKit panel (ServerKit-to-ServerKit peering).
from app.services.linked_panel_service import LinkedPanelService
LinkedPanelService.start_client_if_linked(app)
# Background daemons (metrics collector, queue consumers, analytics
# flush, the job system, the linked-panel client) only make sense in a
# long-running server process. When the app is loaded by a Flask CLI
# one-shot — crucially `flask db upgrade` during an update — they must
# NOT start: they query the database before migrations have run, and
# any failure there (a corrupt DB, or pre-migration schema the new code
# doesn't match yet) aborts the CLI command and sinks the whole update.
# SERVERKIT_SKIP_BACKGROUND=1 forces the same skip; the updater sets it
# as an explicit contract when running migrations.
_cli_one_shot = (
os.path.basename(sys.argv[0] or '').startswith('flask')
and (len(sys.argv) < 2 or sys.argv[1] != 'run')
)
_skip_background = (
os.environ.get('SERVERKIT_SKIP_BACKGROUND') == '1' or _cli_one_shot
)
if not _skip_background:
# Start metrics history collection in background
from app.services.metrics_history_service import MetricsHistoryService
if not MetricsHistoryService.is_running():
MetricsHistoryService.start_collection(app)

# Start queue-bus webhook consumer
from app.queue_bus.consumers import start_webhook_consumer
start_webhook_consumer(app)

# Start queue-bus notification consumer (delivers in-app/email/chat)
from app.notifications.consumer import start_notification_consumer
start_notification_consumer(app)

# Start the API analytics flush thread (a 5s buffer flush — a real-time
# stream, deliberately NOT modeled as a job).
from app.middleware.api_analytics import start_analytics_flush_thread
start_analytics_flush_thread(app)

# Start the unified job system: ONE consumer runs every enqueued Job and
# ONE scheduler ticks all periodic work. This supersedes the former set
# of per-domain daemon scheduler threads (auto-sync, snapshot-retention,
# workflow, health-check, wp-update, api-background, pairing-prune,
# registrar-expiry) — they are now ScheduledJob rows backed by the
# built-in handlers in app/jobs/builtin_handlers.py.
from app.jobs import start_job_system
from app.jobs.builtin_handlers import register_builtin_handlers, seed_builtin_schedules
register_builtin_handlers()
# Register event-driven job handlers (deployment installs, workflow runs,
# scheduled backups).
from app.services.deployment_job_service import DeploymentJobService
DeploymentJobService.register_jobs()
# WorkflowEngine.register_jobs() removed in plan 45 Phase 4 (engine retired).
from app.services.backup_service import BackupService
BackupService.register_jobs()
from app.services.backup_policy_service import BackupPolicyService
BackupPolicyService.register_jobs()
from app.services.server_onboarding_service import ServerOnboardingService
ServerOnboardingService.register_jobs()
from app.services.preview_service import PreviewService
PreviewService.register_jobs()
from app.services.metadata_guard_service import MetadataGuardService
MetadataGuardService.register_jobs()
if not app.config.get('TESTING'):
MetadataGuardService.ensure() # converge the metadata egress rule (no-op when unsupported)
from app.services.speed_test_service import SpeedTestService
SpeedTestService.register_jobs()
from app.services import login_link_service
login_link_service.register_jobs()
from app.services.db_admin_sso_service import DbAdminSsoService
DbAdminSsoService.register_jobs()
from app.services.site_import_service import SiteImportService
SiteImportService.register_jobs()
from app.services.drift_service import DriftService
DriftService.register_jobs()
from app.services.doctor_service import DoctorService
DoctorService.register_jobs()
from app.services.file_integrity_service import FileIntegrityService
FileIntegrityService.register_jobs()
from app.services.malware_scan_service import MalwareScanService
MalwareScanService.register_jobs()
from app.services.bandwidth_service import BandwidthService
BandwidthService.register_jobs()
start_job_system(app, seed=seed_builtin_schedules)

# Resume the embedded agent when this panel is linked to a master
# ServerKit panel (ServerKit-to-ServerKit peering).
from app.services.linked_panel_service import LinkedPanelService
LinkedPanelService.start_client_if_linked(app)

# Request body size limit
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB limit
Expand Down
50 changes: 25 additions & 25 deletions backend/app/api/files.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""File Manager API endpoints for browsing, editing, and managing files."""

from flask import Blueprint, request, jsonify, send_file
from flask_jwt_extended import jwt_required
from ..middleware.rbac import permission_required
from ..services.file_service import FileService
from ..services.storage_provider_service import StorageProviderService
import os
Expand All @@ -11,7 +11,7 @@


@files_bp.route('/browse', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def browse_directory():
"""List directory contents."""
path = request.args.get('path', '/home')
Expand All @@ -25,7 +25,7 @@ def browse_directory():


@files_bp.route('/info', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def get_file_info():
"""Get information about a file or directory."""
path = request.args.get('path')
Expand All @@ -46,7 +46,7 @@ def get_file_info():


@files_bp.route('/read', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def read_file():
"""Read file contents."""
path = request.args.get('path')
Expand All @@ -64,7 +64,7 @@ def read_file():


@files_bp.route('/write', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def write_file():
"""Write content to a file."""
data = request.get_json()
Expand Down Expand Up @@ -92,7 +92,7 @@ def write_file():


@files_bp.route('/create', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def create_file():
"""Create a new file."""
data = request.get_json()
Expand All @@ -116,7 +116,7 @@ def create_file():


@files_bp.route('/mkdir', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def create_directory():
"""Create a new directory."""
data = request.get_json()
Expand All @@ -139,7 +139,7 @@ def create_directory():


@files_bp.route('/delete', methods=['DELETE'])
@jwt_required()
@permission_required('files', 'write')
def delete_path():
"""Delete a file or directory."""
path = request.args.get('path')
Expand All @@ -157,7 +157,7 @@ def delete_path():


@files_bp.route('/rename', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def rename_path():
"""Rename a file or directory."""
data = request.get_json()
Expand All @@ -181,7 +181,7 @@ def rename_path():


@files_bp.route('/copy', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def copy_path():
"""Copy a file or directory."""
data = request.get_json()
Expand All @@ -205,7 +205,7 @@ def copy_path():


@files_bp.route('/move', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def move_path():
"""Move a file or directory."""
data = request.get_json()
Expand All @@ -229,7 +229,7 @@ def move_path():


@files_bp.route('/chmod', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def change_permissions():
"""Change file/directory permissions."""
data = request.get_json()
Expand All @@ -253,7 +253,7 @@ def change_permissions():


@files_bp.route('/search', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def search_files():
"""Search for files matching a pattern."""
directory = request.args.get('directory', '/home')
Expand All @@ -273,7 +273,7 @@ def search_files():


@files_bp.route('/disk-usage', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def get_disk_usage():
"""Get disk usage for a path."""
path = request.args.get('path', '/')
Expand All @@ -286,7 +286,7 @@ def get_disk_usage():


@files_bp.route('/disk-mounts', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def get_disk_mounts():
"""Get disk usage for all mount points."""
result = FileService.get_all_disk_mounts()
Expand All @@ -297,7 +297,7 @@ def get_disk_mounts():


@files_bp.route('/analyze', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def analyze_directory():
"""Analyze directory sizes."""
path = request.args.get('path', '/home')
Expand All @@ -314,7 +314,7 @@ def analyze_directory():


@files_bp.route('/type-breakdown', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def get_type_breakdown():
"""Get file type breakdown for a directory."""
path = request.args.get('path', '/home')
Expand All @@ -330,7 +330,7 @@ def get_type_breakdown():


@files_bp.route('/download', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def download_file():
"""Download a file."""
path = request.args.get('path')
Expand Down Expand Up @@ -358,7 +358,7 @@ def download_file():


@files_bp.route('/upload', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def upload_file():
"""Upload a file."""
if 'file' not in request.files:
Expand Down Expand Up @@ -419,7 +419,7 @@ def upload_file():
# ── S3 / object-storage browser (reuses the configured backup storage creds) ──

@files_bp.route('/s3/browse', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def s3_browse():
"""List a bucket prefix in the same entry shape as the local browser."""
path = request.args.get('path', '/')
Expand All @@ -428,7 +428,7 @@ def s3_browse():


@files_bp.route('/s3/read', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def s3_read():
"""Read a text object for in-app editing."""
path = request.args.get('path')
Expand All @@ -439,7 +439,7 @@ def s3_read():


@files_bp.route('/s3/write', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def s3_write():
"""Write (create or overwrite) an object from text content."""
data = request.get_json() or {}
Expand All @@ -454,7 +454,7 @@ def s3_write():


@files_bp.route('/s3/delete', methods=['DELETE'])
@jwt_required()
@permission_required('files', 'write')
def s3_delete():
"""Delete an object (or every object beneath a prefix)."""
path = request.args.get('path')
Expand All @@ -465,7 +465,7 @@ def s3_delete():


@files_bp.route('/s3/download-url', methods=['GET'])
@jwt_required()
@permission_required('files', 'read')
def s3_download_url():
"""Return a short-lived presigned URL the browser can download directly."""
path = request.args.get('path')
Expand All @@ -476,7 +476,7 @@ def s3_download_url():


@files_bp.route('/s3/upload', methods=['POST'])
@jwt_required()
@permission_required('files', 'write')
def s3_upload():
"""Upload a file into the bucket at the given prefix."""
if 'file' not in request.files:
Expand Down
Loading