Refactor code structure for improved readability and maintainability
This commit is contained in:
17
routes/__init__.py
Normal file
17
routes/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""PiCopy – register_routes(app): registriert alle Blueprints."""
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
from routes.copy_routes import copy_bp
|
||||
from routes.wifi_routes import wifi_bp
|
||||
from routes.wireguard_routes import wireguard_bp
|
||||
from routes.upload_routes import upload_bp
|
||||
from routes.system_routes import system_bp
|
||||
from routes.browse_routes import browse_bp
|
||||
|
||||
app.register_blueprint(copy_bp)
|
||||
app.register_blueprint(wifi_bp)
|
||||
app.register_blueprint(wireguard_bp)
|
||||
app.register_blueprint(upload_bp)
|
||||
app.register_blueprint(system_bp)
|
||||
app.register_blueprint(browse_bp)
|
||||
147
routes/browse_routes.py
Normal file
147
routes/browse_routes.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""PiCopy – Blueprint: /api/browse, /api/history*, /api/internal-share*."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from picopy.config import load_cfg, HISTORY_FILE, INTERNAL_DEST_DIR, log
|
||||
from picopy.state import load_history
|
||||
from picopy.usb import usb_devices, internal_dest_device
|
||||
from picopy.samba import internal_share_update_state, set_internal_share_enabled
|
||||
|
||||
browse_bp = Blueprint('browse', __name__)
|
||||
|
||||
_browse_mounts = {} # usb_port -> mount_point
|
||||
|
||||
|
||||
def _mp_is_alive(mp):
|
||||
"""Prüft ob ein Mount-Punkt wirklich aktiv und lesbar ist."""
|
||||
try:
|
||||
with open('/proc/mounts') as f:
|
||||
mounted = any(mp in line.split() for line in f)
|
||||
if not mounted:
|
||||
return False
|
||||
os.listdir(mp) # I/O-Test: schlägt fehl wenn Gerät entfernt wurde
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _drop_browse_mount(port):
|
||||
"""Veralteten Mount bereinigen."""
|
||||
mp = _browse_mounts.pop(port, None)
|
||||
if mp:
|
||||
subprocess.run(['umount', '-l', mp], capture_output=True)
|
||||
log.info(f'Browse-Mount bereinigt: {mp}')
|
||||
|
||||
|
||||
def get_browse_mp(dev):
|
||||
if dev.get('internal'):
|
||||
INTERNAL_DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return str(INTERNAL_DEST_DIR)
|
||||
port = dev.get('usb_port', '')
|
||||
|
||||
# Auto-mount vom System bevorzugen
|
||||
if dev.get('mount') and _mp_is_alive(dev['mount']):
|
||||
return dev['mount']
|
||||
|
||||
# Gecachten Mount prüfen
|
||||
mp = _browse_mounts.get(port)
|
||||
if mp:
|
||||
if _mp_is_alive(mp):
|
||||
return mp
|
||||
_drop_browse_mount(port) # veraltet -> aufräumen
|
||||
|
||||
# Frisch mounten
|
||||
mp = f'/mnt/picopy_br_{port}'
|
||||
os.makedirs(mp, exist_ok=True)
|
||||
r = subprocess.run(['mount', dev['device'], mp], capture_output=True)
|
||||
if r.returncode == 0:
|
||||
_browse_mounts[port] = mp
|
||||
return mp
|
||||
return None
|
||||
|
||||
|
||||
@browse_bp.route('/api/browse')
|
||||
def r_browse():
|
||||
port = request.args.get('port', '')
|
||||
rpath = request.args.get('path', '').lstrip('/')
|
||||
|
||||
devs = usb_devices()
|
||||
dev = internal_dest_device(load_cfg()) if port == '__internal__' else None
|
||||
if dev is None:
|
||||
dev = next((d for d in devs if d['usb_port'] == port), None)
|
||||
if not dev:
|
||||
return jsonify(error='Gerät nicht verbunden - bitte neu einstecken'), 404
|
||||
|
||||
mp = get_browse_mp(dev)
|
||||
if not mp:
|
||||
return jsonify(error='Gerät nicht lesbar - bitte neu einstecken'), 500
|
||||
|
||||
try:
|
||||
base = Path(mp).resolve()
|
||||
target = (base / rpath).resolve()
|
||||
|
||||
if not str(target).startswith(str(base)):
|
||||
return jsonify(error='Ungültiger Pfad'), 400
|
||||
if not target.is_dir():
|
||||
return jsonify(error='Kein Verzeichnis'), 400
|
||||
|
||||
entries = []
|
||||
for item in sorted(target.iterdir(),
|
||||
key=lambda x: (x.is_file(), x.name.lower())):
|
||||
try:
|
||||
s = item.stat()
|
||||
entries.append({
|
||||
'name': item.name,
|
||||
'dir': item.is_dir(),
|
||||
'size': s.st_size if item.is_file() else None,
|
||||
'mtime': datetime.fromtimestamp(s.st_mtime).strftime('%d.%m.%y %H:%M'),
|
||||
})
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
rel = str(target.relative_to(base))
|
||||
return jsonify(path='' if rel == '.' else rel, entries=entries)
|
||||
|
||||
except OSError as e:
|
||||
import errno as _errno
|
||||
if e.errno == _errno.EIO:
|
||||
# I/O-Fehler = Gerät abgezogen, Mount bereinigen
|
||||
_drop_browse_mount(port)
|
||||
return jsonify(error='Gerät nicht mehr erreichbar - bitte neu einstecken'), 503
|
||||
return jsonify(error=str(e)), 500
|
||||
except Exception as e:
|
||||
return jsonify(error=str(e)), 500
|
||||
|
||||
|
||||
@browse_bp.route('/api/history')
|
||||
def r_history():
|
||||
return jsonify(load_history())
|
||||
|
||||
|
||||
@browse_bp.route('/api/history', methods=['DELETE'])
|
||||
def r_history_clear():
|
||||
try:
|
||||
HISTORY_FILE.write_text('[]', encoding='utf-8')
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@browse_bp.route('/api/internal-share/status')
|
||||
def r_internal_share_status():
|
||||
return jsonify(internal_share_update_state())
|
||||
|
||||
|
||||
@browse_bp.route('/api/internal-share', methods=['POST'])
|
||||
def r_internal_share_set():
|
||||
data = request.get_json(force=True) or {}
|
||||
enabled = bool(data.get('enabled'))
|
||||
ok, err = set_internal_share_enabled(enabled)
|
||||
if not ok:
|
||||
return jsonify(error=err), 500
|
||||
return jsonify(ok=True, status=internal_share_update_state())
|
||||
153
routes/copy_routes.py
Normal file
153
routes/copy_routes.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""PiCopy – Blueprint: /api/copy/*, /api/devices, /api/storage-info, /api/status, /api/config*."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from picopy.config import load_cfg, save_cfg, _fmt_bytes
|
||||
from picopy.state import copy_state, copy_lock
|
||||
from picopy.usb import usb_devices, ensure_mount, internal_dest_device
|
||||
from picopy.wifi import wifi_state, wifi_lock
|
||||
from picopy.wireguard import wg_state, wg_lock
|
||||
from picopy.samba import internal_share_update_state
|
||||
import picopy.copy_engine as _ce
|
||||
|
||||
copy_bp = Blueprint('copy', __name__)
|
||||
|
||||
|
||||
def _resolve_source_ports(cfg) -> list:
|
||||
ports = cfg.get('source_ports') or []
|
||||
if not ports and cfg.get('source_port'):
|
||||
ports = [{'port': cfg['source_port'], 'label': cfg.get('source_label', '')}]
|
||||
return ports
|
||||
|
||||
|
||||
def _configured_destination(cfg, devs):
|
||||
if cfg.get('dest_type') == 'internal':
|
||||
return internal_dest_device(cfg)
|
||||
return next((d for d in devs if d['usb_port'] == cfg.get('dest_port')), None)
|
||||
|
||||
|
||||
@copy_bp.route('/api/devices')
|
||||
def r_devices():
|
||||
return jsonify(usb_devices())
|
||||
|
||||
|
||||
@copy_bp.route('/api/storage-info')
|
||||
def r_storage_info():
|
||||
cfg = load_cfg()
|
||||
devs = usb_devices()
|
||||
result = []
|
||||
|
||||
src_ports = {sp['port'] for sp in _resolve_source_ports(cfg)}
|
||||
dst_port = cfg.get('dest_port')
|
||||
|
||||
def _du_for_dev(dev):
|
||||
mp, owned = ensure_mount(dev)
|
||||
if not mp:
|
||||
return dict(total=None, used=None, free=None, pct=None)
|
||||
try:
|
||||
du = shutil.disk_usage(mp)
|
||||
return dict(total=du.total, used=du.used, free=du.free,
|
||||
pct=round(du.used / du.total * 100) if du.total else 0)
|
||||
except Exception:
|
||||
return dict(total=None, used=None, free=None, pct=None)
|
||||
finally:
|
||||
if owned:
|
||||
subprocess.run(['umount', mp], capture_output=True)
|
||||
|
||||
for dev in devs:
|
||||
port = dev['usb_port']
|
||||
if port in src_ports:
|
||||
role = 'source'
|
||||
elif port == dst_port:
|
||||
role = 'dest'
|
||||
else:
|
||||
role = 'other'
|
||||
entry = dict(
|
||||
role=role,
|
||||
label=dev.get('label') or dev.get('device') or f'Port {port}',
|
||||
port=port,
|
||||
device=dev.get('device', ''),
|
||||
size_str=dev.get('size', ''),
|
||||
)
|
||||
entry.update(_du_for_dev(dev))
|
||||
result.append(entry)
|
||||
|
||||
if cfg.get('dest_type') == 'internal':
|
||||
entry = dict(role='dest',
|
||||
label=cfg.get('internal_dest_label') or 'Interner Speicher',
|
||||
port='__internal__', device='internal', size_str='')
|
||||
entry.update(_du_for_dev({'internal': True}))
|
||||
result.append(entry)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@copy_bp.route('/api/config', methods=['GET', 'POST'])
|
||||
def r_config():
|
||||
if request.method == 'POST':
|
||||
cfg = load_cfg()
|
||||
cfg.update(request.get_json(force=True))
|
||||
save_cfg(cfg)
|
||||
return jsonify(ok=True)
|
||||
return jsonify(load_cfg())
|
||||
|
||||
|
||||
@copy_bp.route('/api/config/ports/reset', methods=['POST'])
|
||||
def r_ports_reset():
|
||||
cfg = load_cfg()
|
||||
cfg['source_ports'] = []
|
||||
cfg['source_port'] = None
|
||||
cfg['source_label'] = ''
|
||||
cfg['dest_port'] = None
|
||||
cfg['dest_label'] = ''
|
||||
cfg['dest_type'] = 'usb'
|
||||
save_cfg(cfg)
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@copy_bp.route('/api/status')
|
||||
def r_status():
|
||||
with copy_lock:
|
||||
cs = dict(copy_state)
|
||||
with wifi_lock:
|
||||
ws = dict(wifi_state)
|
||||
with wg_lock:
|
||||
wgs = dict(wg_state)
|
||||
share = internal_share_update_state()
|
||||
return jsonify(copy=cs, wifi=ws, vpn=wgs, internal_share=share)
|
||||
|
||||
|
||||
@copy_bp.route('/api/copy/start', methods=['POST'])
|
||||
def r_start():
|
||||
with copy_lock:
|
||||
if copy_state['running']:
|
||||
return jsonify(error='Bereits aktiv'), 400
|
||||
if _ce._copy_thread is not None and _ce._copy_thread.is_alive():
|
||||
return jsonify(error='Abbruch wird noch abgeschlossen - bitte kurz warten und erneut versuchen.'), 400
|
||||
cfg = load_cfg()
|
||||
devs = usb_devices()
|
||||
body = request.get_json(force=True) or {}
|
||||
wanted_ports = body.get('ports') # None = alle konfigurierten Quellen
|
||||
src_ports = _resolve_source_ports(cfg)
|
||||
srcs = [next((d for d in devs if d['usb_port'] == sp['port']), None) for sp in src_ports]
|
||||
srcs = [s for s in srcs if s is not None]
|
||||
if wanted_ports is not None:
|
||||
srcs = [s for s in srcs if s['usb_port'] in wanted_ports]
|
||||
if not srcs: return jsonify(error='Keine Quellgeräte gefunden (Ports nicht verbunden)'), 400
|
||||
dst = _configured_destination(cfg, devs)
|
||||
if not dst: return jsonify(error='Zielgerät nicht gefunden'), 400
|
||||
t = threading.Thread(target=_ce.do_copy, args=(srcs, dst, cfg), daemon=True)
|
||||
_ce._copy_thread = t
|
||||
t.start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@copy_bp.route('/api/copy/cancel', methods=['POST'])
|
||||
def r_cancel():
|
||||
with copy_lock:
|
||||
copy_state['running'] = False
|
||||
return jsonify(ok=True)
|
||||
86
routes/system_routes.py
Normal file
86
routes/system_routes.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""PiCopy – Blueprint: /api/sysinfo, /api/update/*, /api/format/*, /api/system/*."""
|
||||
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from picopy.usb import usb_devices
|
||||
from picopy.state import copy_state
|
||||
from picopy.system import (
|
||||
get_sysinfo, update_state, update_lock,
|
||||
format_state, FORMAT_FILESYSTEMS,
|
||||
check_for_updates, install_update, do_format,
|
||||
)
|
||||
|
||||
system_bp = Blueprint('system', __name__)
|
||||
|
||||
|
||||
@system_bp.route('/api/sysinfo')
|
||||
def r_sysinfo():
|
||||
return jsonify(get_sysinfo())
|
||||
|
||||
|
||||
@system_bp.route('/api/update/status')
|
||||
def r_update_status():
|
||||
with update_lock:
|
||||
return jsonify(dict(update_state))
|
||||
|
||||
|
||||
@system_bp.route('/api/update/check', methods=['POST'])
|
||||
def r_update_check():
|
||||
threading.Thread(target=check_for_updates, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@system_bp.route('/api/update/install', methods=['POST'])
|
||||
def r_update_install():
|
||||
from picopy.config import log
|
||||
try:
|
||||
install_update()
|
||||
return jsonify(ok=True)
|
||||
except SyntaxError as e:
|
||||
return jsonify(error=f'Update-Datei ungültig: {e}'), 500
|
||||
except Exception as e:
|
||||
log.exception('Update fehlgeschlagen')
|
||||
return jsonify(error=str(e)), 500
|
||||
|
||||
|
||||
@system_bp.route('/api/format/status')
|
||||
def r_format_status():
|
||||
return jsonify(dict(format_state))
|
||||
|
||||
|
||||
@system_bp.route('/api/format', methods=['POST'])
|
||||
def r_format():
|
||||
if format_state['running']:
|
||||
return jsonify(error='Formatierung läuft bereits'), 409
|
||||
if copy_state.get('running'):
|
||||
return jsonify(error='Kopiervorgang läuft – bitte warten'), 409
|
||||
|
||||
body = request.get_json(force=True)
|
||||
fs = body.get('fs', '').lower()
|
||||
name = (body.get('name') or 'PICOPY').upper()
|
||||
dev = body.get('device', '')
|
||||
|
||||
if fs not in FORMAT_FILESYSTEMS:
|
||||
return jsonify(error=f'Unbekanntes Dateisystem: {fs}'), 400
|
||||
if not dev.startswith('/dev/'):
|
||||
return jsonify(error='Ungültiges Gerät'), 400
|
||||
|
||||
# Sicherheitscheck: Gerät muss ein bekanntes USB-Gerät sein
|
||||
known = [d['device'] for d in usb_devices()]
|
||||
if dev not in known:
|
||||
return jsonify(error='Gerät nicht als USB-Laufwerk erkannt'), 400
|
||||
|
||||
threading.Thread(target=do_format, args=(fs, name, dev), daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@system_bp.route('/api/system/reboot', methods=['POST'])
|
||||
def r_system_reboot():
|
||||
threading.Thread(target=lambda: (
|
||||
__import__('time').sleep(1),
|
||||
subprocess.Popen(['reboot'])
|
||||
), daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
123
routes/upload_routes.py
Normal file
123
routes/upload_routes.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""PiCopy – Blueprint: /api/upload/*."""
|
||||
|
||||
import subprocess
|
||||
import uuid as _uuid_mod
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from picopy.config import load_cfg, save_cfg
|
||||
from picopy.upload import (
|
||||
upload_state, upload_lock,
|
||||
configure_smb_remote, delete_remote, test_remote,
|
||||
_rclone_obscure, RCLONE_CONF as _RCLONE_CONF,
|
||||
)
|
||||
|
||||
upload_bp = Blueprint('upload', __name__)
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/targets', methods=['GET'])
|
||||
def r_upload_list():
|
||||
return jsonify(load_cfg().get('upload_targets', []))
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/targets', methods=['POST'])
|
||||
def r_upload_add():
|
||||
data = request.get_json(force=True)
|
||||
cfg = load_cfg()
|
||||
tid = data.get('id') or _uuid_mod.uuid4().hex[:8]
|
||||
ctype = data.get('type', 'smb')
|
||||
|
||||
if ctype != 'smb':
|
||||
return jsonify(error='Nur SMB/NAS wird unterstützt'), 400
|
||||
ok, err = configure_smb_remote(
|
||||
tid, data.get('host', ''), data.get('share', ''),
|
||||
data.get('user', ''), data.get('pass', ''))
|
||||
|
||||
if not ok:
|
||||
return jsonify(error=f'rclone: {err}'), 500
|
||||
|
||||
# Credentials direkt im Entry speichern (für Connection-String bei Upload)
|
||||
obscured_pw = _rclone_obscure(data.get('pass', '')) if data.get('pass') else ''
|
||||
entry = {
|
||||
'id': tid, 'type': ctype,
|
||||
'name': data.get('name', ctype),
|
||||
'dest_path': data.get('dest_path', 'PiCopy'),
|
||||
'enabled': True,
|
||||
'smb_host': data.get('host', ''),
|
||||
'smb_share': data.get('share', ''),
|
||||
'smb_user': data.get('user', ''),
|
||||
'smb_pass': obscured_pw,
|
||||
}
|
||||
targets = [t for t in cfg.get('upload_targets', []) if t['id'] != tid]
|
||||
targets.append(entry)
|
||||
cfg['upload_targets'] = targets
|
||||
save_cfg(cfg)
|
||||
return jsonify(ok=True, id=tid)
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/targets/<tid>', methods=['DELETE'])
|
||||
def r_upload_del(tid):
|
||||
cfg = load_cfg()
|
||||
cfg['upload_targets'] = [t for t in cfg.get('upload_targets', []) if t['id'] != tid]
|
||||
save_cfg(cfg)
|
||||
delete_remote(tid)
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/browse', methods=['POST'])
|
||||
def r_upload_browse():
|
||||
"""Listet SMB-Freigaben eines Servers ohne gespeicherte Config (rclone connection string)."""
|
||||
data = request.get_json(force=True)
|
||||
host = data.get('host', '').strip()
|
||||
user = data.get('user', '').strip()
|
||||
pw = data.get('pass', '')
|
||||
if not host:
|
||||
return jsonify(error='Server-Adresse fehlt'), 400
|
||||
conn = f':smb,host={host}'
|
||||
if user:
|
||||
conn += f',user={user}'
|
||||
if pw:
|
||||
try:
|
||||
obscured = _rclone_obscure(pw)
|
||||
conn += f',pass={obscured}'
|
||||
except Exception:
|
||||
pass
|
||||
conn += ':'
|
||||
r = subprocess.run(
|
||||
['rclone', '--config', str(_RCLONE_CONF), 'lsd', conn],
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
if r.returncode != 0:
|
||||
lines = r.stderr.strip().splitlines()
|
||||
err = lines[-1] if lines else 'Verbindung fehlgeschlagen'
|
||||
return jsonify(error=err), 400
|
||||
shares = [line.strip().split()[-1] for line in r.stdout.splitlines() if line.strip()]
|
||||
return jsonify(shares=shares)
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/targets/<tid>/toggle', methods=['POST'])
|
||||
def r_upload_toggle(tid):
|
||||
cfg = load_cfg()
|
||||
for t in cfg.get('upload_targets', []):
|
||||
if t['id'] == tid:
|
||||
t['enabled'] = not t.get('enabled', True)
|
||||
break
|
||||
save_cfg(cfg)
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/targets/<tid>/test', methods=['POST'])
|
||||
def r_upload_test(tid):
|
||||
from picopy.config import log
|
||||
try:
|
||||
ok, err = test_remote(tid)
|
||||
except Exception as e:
|
||||
log.exception('upload test failed')
|
||||
ok, err = False, str(e)
|
||||
return jsonify(ok=ok, error=err)
|
||||
|
||||
|
||||
@upload_bp.route('/api/upload/status')
|
||||
def r_upload_status():
|
||||
with upload_lock:
|
||||
return jsonify(dict(upload_state))
|
||||
83
routes/wifi_routes.py
Normal file
83
routes/wifi_routes.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""PiCopy – Blueprint: /api/wifi/*."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from picopy.config import load_cfg, save_cfg
|
||||
from picopy.wifi import (
|
||||
wifi_state, wifi_lock,
|
||||
is_ap_active, stop_ap, start_ap,
|
||||
connect_client_wifi, update_wifi_state,
|
||||
scan_wifi_networks,
|
||||
)
|
||||
|
||||
wifi_bp = Blueprint('wifi', __name__)
|
||||
|
||||
|
||||
@wifi_bp.route('/api/wifi/scan')
|
||||
def r_wifi_scan():
|
||||
nets = scan_wifi_networks()
|
||||
return jsonify(nets)
|
||||
|
||||
|
||||
@wifi_bp.route('/api/wifi/connect', methods=['POST'])
|
||||
def r_wifi_connect():
|
||||
data = request.get_json(force=True)
|
||||
ssid = data.get('ssid', '').strip()
|
||||
pw = data.get('password', '').strip()
|
||||
if not ssid:
|
||||
return jsonify(error='SSID fehlt'), 400
|
||||
cfg = load_cfg()
|
||||
cfg['wifi_ssid'] = ssid
|
||||
cfg['wifi_password'] = pw
|
||||
save_cfg(cfg)
|
||||
|
||||
def _connect():
|
||||
ap_was_active = is_ap_active()
|
||||
if ap_was_active:
|
||||
stop_ap()
|
||||
time.sleep(2)
|
||||
ok = connect_client_wifi(ssid, pw)
|
||||
if ok:
|
||||
time.sleep(5)
|
||||
update_wifi_state()
|
||||
else:
|
||||
if ap_was_active:
|
||||
start_ap(cfg.get('ap_ssid', 'PiCopy'), cfg.get('ap_password', 'PiCopy,'))
|
||||
update_wifi_state()
|
||||
|
||||
threading.Thread(target=_connect, daemon=True).start()
|
||||
return jsonify(ok=True, msg='Verbindungsversuch gestartet')
|
||||
|
||||
|
||||
@wifi_bp.route('/api/wifi/ap', methods=['POST'])
|
||||
def r_wifi_ap():
|
||||
data = request.get_json(force=True)
|
||||
ssid = data.get('ssid', '').strip()
|
||||
pw = data.get('password', '').strip()
|
||||
if not ssid or len(pw) < 8:
|
||||
return jsonify(error='SSID fehlt oder Passwort zu kurz (min. 8 Zeichen)'), 400
|
||||
cfg = load_cfg()
|
||||
cfg['ap_ssid'] = ssid
|
||||
cfg['ap_password'] = pw
|
||||
save_cfg(cfg)
|
||||
|
||||
def _restart_ap():
|
||||
if is_ap_active():
|
||||
stop_ap()
|
||||
time.sleep(2)
|
||||
start_ap(ssid, pw)
|
||||
time.sleep(3)
|
||||
with wifi_lock:
|
||||
wifi_state.update(mode='ap', ssid=ssid, ip='10.42.0.1')
|
||||
|
||||
threading.Thread(target=_restart_ap, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@wifi_bp.route('/api/wifi/status')
|
||||
def r_wifi_status():
|
||||
with wifi_lock:
|
||||
return jsonify(dict(wifi_state))
|
||||
71
routes/wireguard_routes.py
Normal file
71
routes/wireguard_routes.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""PiCopy – Blueprint: /api/wireguard/*."""
|
||||
|
||||
import re
|
||||
import threading
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from picopy.config import load_cfg, save_cfg
|
||||
from picopy.wireguard import (
|
||||
wg_state, wg_lock, WG_CONF,
|
||||
wg_connect, wg_disconnect,
|
||||
wg_install, wg_uninstall,
|
||||
wg_save_config,
|
||||
)
|
||||
|
||||
wireguard_bp = Blueprint('wireguard', __name__)
|
||||
|
||||
|
||||
@wireguard_bp.route('/api/wireguard/config', methods=['GET', 'POST'])
|
||||
def r_wg_config():
|
||||
if request.method == 'POST':
|
||||
data = request.get_json(force=True)
|
||||
content = data.get('content', '')
|
||||
if not content.strip():
|
||||
return jsonify(error='Konfiguration ist leer'), 400
|
||||
ok, err = wg_save_config(content)
|
||||
if not ok:
|
||||
return jsonify(error=err), 500
|
||||
auto = data.get('auto')
|
||||
if auto is not None:
|
||||
c = load_cfg()
|
||||
c['wireguard_auto'] = bool(auto)
|
||||
save_cfg(c)
|
||||
with wg_lock:
|
||||
wg_state['has_config'] = True
|
||||
return jsonify(ok=True)
|
||||
if WG_CONF.exists():
|
||||
content = WG_CONF.read_text(encoding='utf-8')
|
||||
masked = re.sub(r'(PrivateKey\s*=\s*)(.+)', r'\1****', content)
|
||||
return jsonify(exists=True, config=masked)
|
||||
return jsonify(exists=False, config='')
|
||||
|
||||
|
||||
@wireguard_bp.route('/api/wireguard/connect', methods=['POST'])
|
||||
def r_wg_connect():
|
||||
threading.Thread(target=wg_connect, daemon=True).start()
|
||||
return jsonify(ok=True, msg='Verbindungsversuch gestartet')
|
||||
|
||||
|
||||
@wireguard_bp.route('/api/wireguard/disconnect', methods=['POST'])
|
||||
def r_wg_disconnect():
|
||||
ok = wg_disconnect()
|
||||
return jsonify(ok=ok)
|
||||
|
||||
|
||||
@wireguard_bp.route('/api/wireguard/install', methods=['POST'])
|
||||
def r_wg_install():
|
||||
with wg_lock:
|
||||
if wg_state['pkg_running']:
|
||||
return jsonify(error='Bereits aktiv'), 400
|
||||
threading.Thread(target=wg_install, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@wireguard_bp.route('/api/wireguard/uninstall', methods=['POST'])
|
||||
def r_wg_uninstall():
|
||||
with wg_lock:
|
||||
if wg_state['pkg_running']:
|
||||
return jsonify(error='Bereits aktiv'), 400
|
||||
threading.Thread(target=wg_uninstall, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
Reference in New Issue
Block a user