mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-30 04:39:33 +00:00
221 lines
7.6 KiB
Python
221 lines
7.6 KiB
Python
import os
|
|
import sqlite3
|
|
import zipfile
|
|
import json
|
|
import threading
|
|
import shutil
|
|
from datetime import datetime, timezone
|
|
from flask import Blueprint, jsonify, send_file, request, after_this_request
|
|
from app.config import config
|
|
from app.api.middleware import admin_required
|
|
|
|
system_bp = Blueprint('system', __name__)
|
|
|
|
@system_bp.route('/backup', methods=['GET'])
|
|
@admin_required
|
|
def backup_system():
|
|
tmp_db_path = '/tmp/backup.db'
|
|
tmp_zip_path = f'/tmp/email_backup_{datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")}.zip'
|
|
|
|
if os.path.exists(tmp_db_path):
|
|
os.remove(tmp_db_path)
|
|
|
|
db_path = config.DB_PATH
|
|
try:
|
|
conn = sqlite3.connect(db_path)
|
|
conn.execute(f"VACUUM INTO '{tmp_db_path}'")
|
|
conn.close()
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
manifest = {
|
|
"schema": 1,
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"files": []
|
|
}
|
|
|
|
try:
|
|
with zipfile.ZipFile(tmp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
zf.write(tmp_db_path, arcname='db/mail.db')
|
|
|
|
att_path = config.ATTACHMENTS_PATH
|
|
if os.path.exists(att_path):
|
|
for root, dirs, files in os.walk(att_path):
|
|
for f in files:
|
|
file_path = os.path.join(root, f)
|
|
arc_name = os.path.relpath(file_path, config.MAIL_DATA_PATH)
|
|
zf.write(file_path, arcname=arc_name)
|
|
|
|
zf.writestr('manifest.json', json.dumps(manifest, indent=2))
|
|
|
|
@after_this_request
|
|
def cleanup(response):
|
|
if os.path.exists(tmp_db_path):
|
|
os.remove(tmp_db_path)
|
|
if os.path.exists(tmp_zip_path):
|
|
os.remove(tmp_zip_path)
|
|
return response
|
|
|
|
return send_file(tmp_zip_path, as_attachment=True, download_name=os.path.basename(tmp_zip_path))
|
|
except Exception as e:
|
|
if os.path.exists(tmp_db_path):
|
|
os.remove(tmp_db_path)
|
|
if os.path.exists(tmp_zip_path):
|
|
os.remove(tmp_zip_path)
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@system_bp.route('/local-domains', methods=['GET'])
|
|
@admin_required
|
|
def local_domains():
|
|
db_path = config.DB_PATH
|
|
try:
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
rows = conn.execute("""
|
|
SELECT m.domain,
|
|
COUNT(DISTINCT m.address) AS mailbox_count,
|
|
COUNT(msg.id) AS message_count
|
|
FROM mailboxes m
|
|
LEFT JOIN messages msg ON msg.mailbox_address = m.address
|
|
GROUP BY m.domain
|
|
""").fetchall()
|
|
conn.close()
|
|
return jsonify([dict(r) for r in rows])
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@system_bp.route('/wipe-domain', methods=['POST'])
|
|
@admin_required
|
|
def wipe_domain():
|
|
data = request.get_json(force=True, silent=True) or {}
|
|
domain = data.get('domain', '').strip()
|
|
if not domain:
|
|
return jsonify({"error": "domain required"}), 400
|
|
config.IN_MAINTENANCE = True
|
|
db_path = config.DB_PATH
|
|
try:
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
rows = conn.execute(
|
|
"SELECT id FROM messages WHERE mailbox_address LIKE ? AND has_attachments=1",
|
|
(f'%@{domain}',)
|
|
).fetchall()
|
|
for row in rows:
|
|
shutil.rmtree(os.path.join(config.ATTACHMENTS_PATH, str(row['id'])), ignore_errors=True)
|
|
conn.execute("DELETE FROM mailboxes WHERE address LIKE ?", (f'%@{domain}',))
|
|
conn.commit()
|
|
conn.close()
|
|
def _vacuum():
|
|
c = sqlite3.connect(db_path)
|
|
c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
c.execute("VACUUM")
|
|
c.close()
|
|
threading.Thread(target=_vacuum, daemon=True).start()
|
|
config.IN_MAINTENANCE = False
|
|
return jsonify({"status": "wiped", "domain": domain})
|
|
except Exception as e:
|
|
config.IN_MAINTENANCE = False
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@system_bp.route('/wipe-all', methods=['POST'])
|
|
@admin_required
|
|
def wipe_all():
|
|
config.IN_MAINTENANCE = True
|
|
db_path = config.DB_PATH
|
|
try:
|
|
att_path = config.ATTACHMENTS_PATH
|
|
if os.path.exists(att_path):
|
|
shutil.rmtree(att_path)
|
|
os.makedirs(att_path, exist_ok=True)
|
|
conn = sqlite3.connect(db_path)
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
conn.execute("DELETE FROM mailboxes")
|
|
conn.commit()
|
|
conn.close()
|
|
def _vacuum():
|
|
c = sqlite3.connect(db_path)
|
|
c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
c.execute("VACUUM")
|
|
c.close()
|
|
threading.Thread(target=_vacuum, daemon=True).start()
|
|
config.IN_MAINTENANCE = False
|
|
return jsonify({"status": "wiped"})
|
|
except Exception as e:
|
|
config.IN_MAINTENANCE = False
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
def _schedule_restart():
|
|
def restart():
|
|
import time
|
|
time.sleep(2)
|
|
os._exit(0)
|
|
threading.Thread(target=restart).start()
|
|
|
|
@system_bp.route('/restore', methods=['POST'])
|
|
@admin_required
|
|
def restore_system():
|
|
config.IN_MAINTENANCE = True
|
|
|
|
if 'file' not in request.files:
|
|
config.IN_MAINTENANCE = False
|
|
return jsonify({"error": "No file uploaded"}), 400
|
|
|
|
file = request.files['file']
|
|
tmp_upload_path = '/tmp/restore_upload.zip'
|
|
staging_path = '/data/restore_staging'
|
|
old_data_path = '/data/old_data'
|
|
|
|
file.save(tmp_upload_path)
|
|
|
|
try:
|
|
with zipfile.ZipFile(tmp_upload_path, 'r') as zf:
|
|
if 'manifest.json' not in zf.namelist():
|
|
raise ValueError("Invalid backup archive: missing manifest.json")
|
|
if os.path.exists(staging_path):
|
|
shutil.rmtree(staging_path)
|
|
os.makedirs(staging_path)
|
|
zf.extractall(staging_path)
|
|
|
|
if os.path.exists(old_data_path):
|
|
shutil.rmtree(old_data_path)
|
|
os.makedirs(old_data_path)
|
|
|
|
db_dir = os.path.dirname(config.DB_PATH)
|
|
att_dir = config.ATTACHMENTS_PATH
|
|
|
|
if os.path.exists(db_dir):
|
|
shutil.move(db_dir, os.path.join(old_data_path, 'db'))
|
|
if os.path.exists(att_dir):
|
|
shutil.move(att_dir, os.path.join(old_data_path, 'attachments'))
|
|
|
|
staged_db = os.path.join(staging_path, 'db')
|
|
staged_att = os.path.join(staging_path, 'attachments')
|
|
|
|
if os.path.exists(staged_db):
|
|
shutil.move(staged_db, db_dir)
|
|
if os.path.exists(staged_att):
|
|
shutil.move(staged_att, att_dir)
|
|
|
|
shutil.rmtree(staging_path)
|
|
os.remove(tmp_upload_path)
|
|
|
|
_schedule_restart()
|
|
return jsonify({"status": "success"})
|
|
|
|
except Exception as e:
|
|
config.IN_MAINTENANCE = False
|
|
if os.path.exists(tmp_upload_path):
|
|
os.remove(tmp_upload_path)
|
|
if os.path.exists(staging_path):
|
|
shutil.rmtree(staging_path)
|
|
if os.path.exists(os.path.join(old_data_path, 'db')):
|
|
if os.path.exists(db_dir):
|
|
shutil.rmtree(db_dir)
|
|
shutil.move(os.path.join(old_data_path, 'db'), db_dir)
|
|
if os.path.exists(os.path.join(old_data_path, 'attachments')):
|
|
if os.path.exists(att_dir):
|
|
shutil.rmtree(att_dir)
|
|
shutil.move(os.path.join(old_data_path, 'attachments'), att_dir)
|
|
|
|
return jsonify({"error": str(e)}), 500
|