Release: v2.1.6 - Added VaptVupt 2.40
This commit is contained in:
parent
8e0eb1ee41
commit
572031910c
41 changed files with 4309 additions and 741 deletions
569
gui/src/zupt_gui.py
Normal file
569
gui/src/zupt_gui.py
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Zupt GUI — Cross-Platform Post-Quantum Backup. Requires PySide6."""
|
||||
import sys, os, subprocess, shutil
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QLineEdit, QComboBox, QFileDialog,
|
||||
QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox,
|
||||
QSpinBox, QMessageBox, QStatusBar, QScrollArea
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QObject, QThread
|
||||
from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap
|
||||
|
||||
# ── Find zupt binary ──
|
||||
def _find_zupt():
|
||||
if os.environ.get("ZUPT_BIN") and os.path.isfile(os.environ["ZUPT_BIN"]):
|
||||
return os.environ["ZUPT_BIN"]
|
||||
# Check local project tree FIRST (handles running from zupt-2.1.6/gui/)
|
||||
here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))
|
||||
for c in [here.parent.parent/"zupt", # zupt-2.1.6/gui/src -> zupt-2.1.6/zupt
|
||||
here.parent/"zupt", # zupt-2.1.6/gui -> zupt-2.1.6/zupt (shouldn't happen but safe)
|
||||
here/"zupt", # same dir as script
|
||||
here.parent.parent/"zupt.exe",
|
||||
here.parent/"zupt.exe",
|
||||
here/"zupt.exe"]:
|
||||
if c.is_file() and os.access(str(c), os.X_OK):
|
||||
return str(c.resolve())
|
||||
# Then system PATH
|
||||
found = shutil.which("zupt")
|
||||
if found: return found
|
||||
# Fallback common paths
|
||||
for c in [Path("/usr/local/bin/zupt"), Path("/usr/bin/zupt")]:
|
||||
if c.is_file(): return str(c)
|
||||
return "zupt"
|
||||
|
||||
ZUPT = _find_zupt()
|
||||
|
||||
# ── Query version ONCE at import (cached) ──
|
||||
def _get_version():
|
||||
try:
|
||||
r = subprocess.run([ZUPT, "version"], capture_output=True, text=True, timeout=5)
|
||||
if r.returncode == 0:
|
||||
lines = r.stdout.strip().split("\n")
|
||||
return lines[0], r.stdout.strip()
|
||||
except Exception: pass
|
||||
return "zupt (not found)", ""
|
||||
|
||||
ZUPT_VER_SHORT, ZUPT_VER_FULL = _get_version()
|
||||
|
||||
# ── Find icon file ──
|
||||
def _find_icon():
|
||||
here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))
|
||||
for p in [here/"assets/zupt.ico", here/"assets/zupt-icon.png", here/"assets/zupt.png",
|
||||
here.parent/"assets/zupt.ico", here.parent/"assets/zupt-icon.png",
|
||||
Path("/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"),
|
||||
Path("/usr/share/zupt-gui/assets/zupt-icon.png")]:
|
||||
if p.is_file(): return str(p)
|
||||
return None
|
||||
|
||||
ICON_PATH = _find_icon()
|
||||
|
||||
STYLE = """
|
||||
* { font-family: "Segoe UI", "Cantarell", "Noto Sans", sans-serif; }
|
||||
QMainWindow { background: #0a0a0a; }
|
||||
QTabWidget::pane { background: #0a0a0a; border: none; border-top: 1px solid #1a2a30; }
|
||||
QTabBar { background: #050a0e; }
|
||||
QTabBar::tab {
|
||||
background: #050a0e; color: #5a7a88; padding: 11px 22px;
|
||||
border: none; border-bottom: 2px solid transparent;
|
||||
font-weight: 600; font-size: 12px;
|
||||
}
|
||||
QTabBar::tab:hover { color: #a0c8d8; background: #0a1018; }
|
||||
QTabBar::tab:selected { color: #00dde0; border-bottom-color: #00dde0; background: #0c1218; }
|
||||
QLabel { color: #90acb8; }
|
||||
QLineEdit, QComboBox, QSpinBox {
|
||||
background: #0e1820; color: #c0dce8; border: 1px solid #1a2a30;
|
||||
border-radius: 6px; padding: 9px 12px;
|
||||
font-family: "JetBrains Mono", "Cascadia Code", "SF Mono", monospace; font-size: 13px;
|
||||
selection-background-color: #004858;
|
||||
}
|
||||
QLineEdit:focus, QComboBox:focus, QSpinBox:focus { border-color: #006878; }
|
||||
QComboBox::drop-down { border: none; padding-right: 8px; }
|
||||
QComboBox QAbstractItemView { background: #0e1820; color: #c0dce8; border: 1px solid #1a2a30; selection-background-color: #004858; }
|
||||
QPushButton {
|
||||
background: #0a2028; color: #00dde0; border: 1px solid #1a3a40;
|
||||
border-radius: 6px; padding: 10px 24px; font-weight: 600; font-size: 12px;
|
||||
}
|
||||
QPushButton:hover { background: #0e2830; border-color: #00dde0; }
|
||||
QPushButton:pressed { background: #062020; }
|
||||
QPushButton:disabled { color: #304048; border-color: #151e22; }
|
||||
QPushButton#green { color: #00c868; border-color: #0a3020; background: #061a10; }
|
||||
QPushButton#green:hover { border-color: #00c868; background: #0a2818; }
|
||||
QPushButton#amber { color: #d0a020; border-color: #2a2010; background: #181200; }
|
||||
QPushButton#amber:hover { border-color: #d0a020; background: #201800; }
|
||||
QPushButton#small { padding: 8px 14px; font-size: 11px; }
|
||||
QTextEdit {
|
||||
background: #060c10; color: #608898; border: 1px solid #1a2a30;
|
||||
border-radius: 6px; padding: 10px;
|
||||
font-family: "JetBrains Mono", "Cascadia Code", "SF Mono", monospace; font-size: 11px;
|
||||
}
|
||||
QProgressBar { background: #0e1820; border: none; border-radius: 3px; max-height: 6px; }
|
||||
QProgressBar::chunk { background: qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #00dde0,stop:1 #008090); border-radius: 3px; }
|
||||
QCheckBox { color: #6a8898; spacing: 6px; font-size: 13px; }
|
||||
QCheckBox::indicator { width: 16px; height: 16px; border-radius: 3px; border: 1px solid #1a3a40; background: #0e1820; }
|
||||
QCheckBox::indicator:checked { background: #004858; border-color: #00dde0; }
|
||||
QScrollArea { border: none; background: transparent; }
|
||||
QStatusBar { background: #050a0e; color: #3a5868; font-size: 11px; border-top: 1px solid #1a2a30; }
|
||||
QFrame#sep { background: #1a2a30; max-height: 1px; }
|
||||
"""
|
||||
|
||||
def run_zupt(args, timeout=30):
|
||||
try:
|
||||
r = subprocess.run([ZUPT]+list(args), capture_output=True, text=True, timeout=timeout)
|
||||
return r.returncode, r.stdout, r.stderr
|
||||
except FileNotFoundError: return -1, "", f"zupt not found: {ZUPT}"
|
||||
except subprocess.TimeoutExpired: return -1, "", "Timed out"
|
||||
|
||||
class Worker(QObject):
|
||||
done = Signal(int, str, str)
|
||||
log = Signal(str)
|
||||
def __init__(self, args): super().__init__(); self.args = args
|
||||
def run(self):
|
||||
self.log.emit(f"$ zupt {' '.join(self.args)}")
|
||||
try:
|
||||
proc = subprocess.Popen([ZUPT]+self.args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
err_lines = []
|
||||
for line in proc.stderr:
|
||||
line = line.rstrip('\n')
|
||||
if line: err_lines.append(line); self.log.emit(line)
|
||||
stdout, _ = proc.communicate(timeout=7200)
|
||||
self.done.emit(proc.returncode, stdout or "", "\n".join(err_lines))
|
||||
except FileNotFoundError: self.done.emit(-1, "", f"zupt not found: {ZUPT}")
|
||||
except subprocess.TimeoutExpired: proc.kill(); self.done.emit(-1, "", "Timed out")
|
||||
|
||||
# ── Widgets ──
|
||||
|
||||
def H(text):
|
||||
lbl = QLabel(text.upper())
|
||||
lbl.setStyleSheet("font-size:10px;font-weight:700;color:#3a5868;letter-spacing:1.5px;")
|
||||
return lbl
|
||||
|
||||
def Sep():
|
||||
f = QFrame(); f.setObjectName("sep"); f.setFixedHeight(1); return f
|
||||
|
||||
def Log(h=150):
|
||||
t = QTextEdit(); t.setReadOnly(True); t.setMaximumHeight(h); return t
|
||||
|
||||
def PwField(ph="Optional"):
|
||||
pw = QLineEdit(); pw.setEchoMode(QLineEdit.Password); pw.setPlaceholderText(ph); return pw
|
||||
|
||||
class PathField(QWidget):
|
||||
def __init__(self, ph="", mode="open", filters="All Files (*)"):
|
||||
super().__init__()
|
||||
self.mode, self.filters = mode, filters
|
||||
self.setAcceptDrops(True)
|
||||
lay = QHBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(6)
|
||||
self.edit = QLineEdit(); self.edit.setPlaceholderText(ph)
|
||||
lay.addWidget(self.edit, 1)
|
||||
btn = QPushButton("Browse"); btn.setObjectName("small"); btn.setFixedWidth(70)
|
||||
btn.clicked.connect(self._pick); lay.addWidget(btn)
|
||||
|
||||
def _pick(self):
|
||||
if self.mode == "save": p, _ = QFileDialog.getSaveFileName(self, "Save", "", self.filters)
|
||||
elif self.mode == "dir": p = QFileDialog.getExistingDirectory(self, "Directory")
|
||||
elif self.mode == "multi":
|
||||
ps, _ = QFileDialog.getOpenFileNames(self, "Select", "", self.filters)
|
||||
p = "|".join(ps) if ps else ""
|
||||
else: p, _ = QFileDialog.getOpenFileName(self, "Open", "", self.filters)
|
||||
if p: self.edit.setText(p)
|
||||
|
||||
def path(self): return self.edit.text().strip()
|
||||
def paths(self):
|
||||
t = self.path(); return t.split("|") if "|" in t else [t] if t else []
|
||||
def dragEnterEvent(self, e):
|
||||
if e.mimeData().hasUrls(): e.acceptProposedAction()
|
||||
def dropEvent(self, e):
|
||||
ps = [u.toLocalFile() for u in e.mimeData().urls() if u.toLocalFile()]
|
||||
if ps: self.edit.setText("|".join(ps) if self.mode == "multi" else ps[0])
|
||||
|
||||
def scrollable(w):
|
||||
sa = QScrollArea(); sa.setWidgetResizable(True); sa.setWidget(w); sa.setFrameShape(QFrame.NoFrame); return sa
|
||||
|
||||
def run_async(parent, cmd, btn, log, progress=None):
|
||||
log.clear(); btn.setEnabled(False)
|
||||
if progress: progress.show()
|
||||
t = QThread(); w = Worker(cmd); w.moveToThread(t)
|
||||
w.log.connect(log.append)
|
||||
def finish(code, out, err):
|
||||
btn.setEnabled(True)
|
||||
if progress: progress.hide()
|
||||
log.append("\nDone." if code == 0 else f"\nFailed (exit {code}).")
|
||||
t.quit()
|
||||
w.done.connect(finish)
|
||||
t.started.connect(w.run); t.start()
|
||||
parent._thread, parent._worker = t, w
|
||||
|
||||
# ── Tabs ──
|
||||
|
||||
class KeysTab(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
inner = QWidget()
|
||||
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
|
||||
|
||||
v.addWidget(QLabel("Generate or export ML-KEM-768 + X25519 hybrid keys."))
|
||||
v.addWidget(Sep())
|
||||
|
||||
# Section 1: Generate new keypair
|
||||
v.addWidget(H("Generate new keypair"))
|
||||
v.addWidget(QLabel("Creates both private and public key files."))
|
||||
|
||||
v.addWidget(H("Private key output"))
|
||||
self.gen_priv = PathField("e.g. ~/zupt_private.key", "save", "Key (*.key);;All (*)")
|
||||
v.addWidget(self.gen_priv)
|
||||
|
||||
self.gen_btn = QPushButton("Generate Keypair")
|
||||
self.gen_btn.clicked.connect(self._generate)
|
||||
v.addWidget(self.gen_btn)
|
||||
self.gen_log = Log(120); v.addWidget(self.gen_log)
|
||||
|
||||
v.addWidget(Sep())
|
||||
|
||||
# Section 2: Export public key from existing private key
|
||||
v.addWidget(H("Export public key from private key"))
|
||||
v.addWidget(QLabel("Extract the public key from an existing private key file."))
|
||||
|
||||
v.addWidget(H("Existing private key"))
|
||||
self.exp_priv = PathField("Select private key", "open", "Key (*.key);;All (*)")
|
||||
v.addWidget(self.exp_priv)
|
||||
|
||||
v.addWidget(H("Public key output"))
|
||||
self.exp_pub = PathField("e.g. ~/zupt_public.key", "save", "Key (*.key);;All (*)")
|
||||
v.addWidget(self.exp_pub)
|
||||
|
||||
self.exp_btn = QPushButton("Export Public Key")
|
||||
self.exp_btn.setObjectName("green")
|
||||
self.exp_btn.clicked.connect(self._export)
|
||||
v.addWidget(self.exp_btn)
|
||||
self.exp_log = Log(100); v.addWidget(self.exp_log)
|
||||
|
||||
v.addStretch()
|
||||
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
|
||||
|
||||
def _generate(self):
|
||||
p = self.gen_priv.path() or str(Path.home() / "zupt_private.key")
|
||||
self.gen_priv.edit.setText(p)
|
||||
self.gen_log.clear(); self.gen_btn.setEnabled(False)
|
||||
code, _, err = run_zupt(["keygen", "-o", p])
|
||||
self.gen_log.append(err.strip())
|
||||
if code == 0:
|
||||
pub = p.rsplit(".", 1)[0] + "_public.key" if "." in p else p + ".pub"
|
||||
c2, _, e2 = run_zupt(["keygen", "--pub", "-o", pub, "-k", p])
|
||||
self.gen_log.append(e2.strip())
|
||||
if c2 == 0:
|
||||
self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}")
|
||||
else:
|
||||
self.gen_log.append("\nFailed.")
|
||||
self.gen_btn.setEnabled(True)
|
||||
|
||||
def _export(self):
|
||||
priv = self.exp_priv.path()
|
||||
pub = self.exp_pub.path()
|
||||
if not priv: QMessageBox.warning(self, "Zupt", "Select the private key file."); return
|
||||
if not pub:
|
||||
pub = priv.rsplit(".", 1)[0] + "_public.key" if "." in priv else priv + ".pub"
|
||||
self.exp_pub.edit.setText(pub)
|
||||
self.exp_log.clear(); self.exp_btn.setEnabled(False)
|
||||
code, _, err = run_zupt(["keygen", "--pub", "-o", pub, "-k", priv])
|
||||
self.exp_log.append(err.strip())
|
||||
if code == 0:
|
||||
self.exp_log.append(f"\nPublic key: {pub}")
|
||||
else:
|
||||
self.exp_log.append("\nFailed.")
|
||||
self.exp_btn.setEnabled(True)
|
||||
|
||||
|
||||
class CompressTab(QWidget):
|
||||
def __init__(self, initial=None):
|
||||
super().__init__()
|
||||
self._thread = self._worker = None
|
||||
inner = QWidget()
|
||||
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
|
||||
v.addWidget(QLabel("Compress files into an encrypted .zupt archive."))
|
||||
v.addWidget(Sep())
|
||||
v.addWidget(H("Source files / directory"))
|
||||
self.src = PathField("Drop files here or browse", "multi"); v.addWidget(self.src)
|
||||
v.addWidget(H("Output archive"))
|
||||
self.dst = PathField("e.g. backup.zupt", "save", "Zupt (*.zupt);;All (*)"); v.addWidget(self.dst)
|
||||
row = QHBoxLayout(); row.setSpacing(16)
|
||||
for label, widget in [("Codec", self._mk_codec()), ("Level", self._mk_level())]:
|
||||
c = QVBoxLayout(); c.addWidget(H(label)); c.addWidget(widget); row.addLayout(c)
|
||||
c = QVBoxLayout(); c.addWidget(H("Options"))
|
||||
self.dedup = QCheckBox("Dedup"); self.solid = QCheckBox("Solid")
|
||||
oh = QHBoxLayout(); oh.addWidget(self.dedup); oh.addWidget(self.solid); oh.addStretch()
|
||||
c.addLayout(oh); row.addLayout(c)
|
||||
v.addLayout(row); v.addWidget(Sep())
|
||||
enc = QHBoxLayout(); enc.setSpacing(16)
|
||||
pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField("AES-256"); pw.addWidget(self.pw); enc.addLayout(pw)
|
||||
pq = QVBoxLayout(); pq.addWidget(H("PQ public key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq)
|
||||
v.addLayout(enc)
|
||||
self.btn = QPushButton("Compress"); self.btn.clicked.connect(self._run); v.addWidget(self.btn)
|
||||
self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress)
|
||||
self.log = Log(140); v.addWidget(self.log); v.addStretch()
|
||||
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
|
||||
if initial: self.src.edit.setText("|".join(initial))
|
||||
|
||||
def _mk_codec(self):
|
||||
self.codec = QComboBox(); self.codec.addItems(["AUTO","VaptVupt","LZHP","Store"]); return self.codec
|
||||
def _mk_level(self):
|
||||
self.level = QSpinBox(); self.level.setRange(1,9); self.level.setValue(7); return self.level
|
||||
|
||||
def _run(self):
|
||||
srcs = self.src.paths()
|
||||
if not srcs or not srcs[0]: QMessageBox.warning(self, "Zupt", "Select files."); return
|
||||
dst = self.dst.path() or srcs[0] + ".zupt"; self.dst.edit.setText(dst)
|
||||
cmd = ["compress", "-l", str(self.level.value())]
|
||||
cm = {"AUTO": None, "VaptVupt": "--vv", "LZHP": "--lzhp", "Store": "-s"}
|
||||
if cm.get(self.codec.currentText()): cmd.append(cm[self.codec.currentText()])
|
||||
if self.dedup.isChecked(): cmd.append("--dedup")
|
||||
if self.solid.isChecked(): cmd.append("--solid")
|
||||
if self.pw.text(): cmd += ["-p", self.pw.text()]
|
||||
if self.pq.path(): cmd += ["--pq", self.pq.path()]
|
||||
cmd.append(dst); cmd.extend(srcs)
|
||||
run_async(self, cmd, self.btn, self.log, self.progress)
|
||||
|
||||
|
||||
class ExtractTab(QWidget):
|
||||
def __init__(self, initial=None):
|
||||
super().__init__()
|
||||
self._thread = self._worker = None
|
||||
inner = QWidget()
|
||||
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
|
||||
v.addWidget(QLabel("Extract and decrypt a .zupt archive."))
|
||||
v.addWidget(Sep())
|
||||
v.addWidget(H("Archive")); self.arc = PathField("Drop .zupt here", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.arc)
|
||||
v.addWidget(H("Output directory")); self.out = PathField("Same as archive", "dir"); v.addWidget(self.out)
|
||||
enc = QHBoxLayout(); enc.setSpacing(16)
|
||||
pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField(); pw.addWidget(self.pw); enc.addLayout(pw)
|
||||
pq = QVBoxLayout(); pq.addWidget(H("PQ private key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq)
|
||||
v.addLayout(enc)
|
||||
self.btn = QPushButton("Extract"); self.btn.setObjectName("green"); self.btn.clicked.connect(self._run); v.addWidget(self.btn)
|
||||
self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress)
|
||||
self.log = Log(140); v.addWidget(self.log); v.addStretch()
|
||||
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
|
||||
if initial: self.arc.edit.setText(initial)
|
||||
|
||||
def _run(self):
|
||||
arc = self.arc.path()
|
||||
if not arc: QMessageBox.warning(self, "Zupt", "Select an archive."); return
|
||||
cmd = ["extract"]
|
||||
if self.out.path(): cmd += ["-o", self.out.path()]
|
||||
if self.pw.text(): cmd += ["-p", self.pw.text()]
|
||||
if self.pq.path(): cmd += ["--pq", self.pq.path()]
|
||||
cmd.append(arc)
|
||||
run_async(self, cmd, self.btn, self.log, self.progress)
|
||||
|
||||
|
||||
class VerifyTab(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
inner = QWidget()
|
||||
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
|
||||
v.addWidget(QLabel("Verify checksums or inspect archive metadata."))
|
||||
v.addWidget(Sep())
|
||||
v.addWidget(H("Verify integrity"))
|
||||
self.varc = PathField("Archive to verify", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.varc)
|
||||
v.addWidget(H("Password (if encrypted)"))
|
||||
self.vpw = PwField("Leave empty if not encrypted"); v.addWidget(self.vpw)
|
||||
self.vbtn = QPushButton("Verify"); self.vbtn.setObjectName("amber"); self.vbtn.clicked.connect(self._verify); v.addWidget(self.vbtn)
|
||||
self.vlog = Log(120); v.addWidget(self.vlog)
|
||||
v.addWidget(Sep())
|
||||
v.addWidget(H("Archive info (no password needed)"))
|
||||
self.iarc = PathField("Archive to inspect", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.iarc)
|
||||
self.ibtn = QPushButton("Show Info"); self.ibtn.clicked.connect(self._info); v.addWidget(self.ibtn)
|
||||
self.ilog = Log(140); v.addWidget(self.ilog); v.addStretch()
|
||||
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
|
||||
|
||||
def _verify(self):
|
||||
arc = self.varc.path()
|
||||
if not arc: return
|
||||
cmd = ["test"]
|
||||
if self.vpw.text(): cmd += ["-p", self.vpw.text()]
|
||||
cmd.append(arc); self.vlog.clear()
|
||||
code, out, err = run_zupt(cmd, timeout=600)
|
||||
self.vlog.append((err + "\n" + out).strip())
|
||||
self.vlog.append("\nAll checksums passed." if code == 0 else "\nVerification failed.")
|
||||
|
||||
def _info(self):
|
||||
arc = self.iarc.path()
|
||||
if not arc: return
|
||||
self.ilog.clear()
|
||||
code, out, err = run_zupt(["info", arc])
|
||||
self.ilog.append(out.strip() if out.strip() else err.strip())
|
||||
|
||||
|
||||
class DiskTab(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._thread = self._worker = None
|
||||
inner = QWidget()
|
||||
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
|
||||
v.addWidget(QLabel("Full-disk or partition backup and restore."))
|
||||
v.addWidget(Sep())
|
||||
v.addWidget(H("Backup — source device or image"))
|
||||
self.bsrc = PathField("/dev/sdX or disk.img"); v.addWidget(self.bsrc)
|
||||
v.addWidget(H("Backup — output archive"))
|
||||
self.bout = PathField("backup.zupt", "save", "Zupt (*.zupt);;All (*)"); v.addWidget(self.bout)
|
||||
bopt = QHBoxLayout(); bopt.setSpacing(16)
|
||||
oc = QVBoxLayout(); oc.addWidget(H("Options")); self.bdedup = QCheckBox("Block deduplication"); oc.addWidget(self.bdedup); bopt.addLayout(oc)
|
||||
pc = QVBoxLayout(); pc.addWidget(H("Password")); self.bpw = PwField("Optional — AES-256"); pc.addWidget(self.bpw); bopt.addLayout(pc)
|
||||
v.addLayout(bopt)
|
||||
self.bbtn = QPushButton("Start Backup"); self.bbtn.clicked.connect(self._backup); v.addWidget(self.bbtn)
|
||||
self.blog = Log(100); v.addWidget(self.blog)
|
||||
v.addWidget(Sep())
|
||||
v.addWidget(H("Restore — archive"))
|
||||
self.rarc = PathField("backup.zupt", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.rarc)
|
||||
v.addWidget(H("Restore — target device or file"))
|
||||
self.rtgt = PathField("/dev/sdX or output.img", "save"); v.addWidget(self.rtgt)
|
||||
v.addWidget(H("Restore — password"))
|
||||
self.rpw = PwField("If archive is encrypted"); v.addWidget(self.rpw)
|
||||
self.rbtn = QPushButton("Start Restore"); self.rbtn.setObjectName("green"); self.rbtn.clicked.connect(self._restore); v.addWidget(self.rbtn)
|
||||
self.rlog = Log(100); v.addWidget(self.rlog); v.addStretch()
|
||||
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
|
||||
|
||||
def _backup(self):
|
||||
s, o = self.bsrc.path(), self.bout.path()
|
||||
if not s or not o: QMessageBox.warning(self, "Zupt", "Set source and output."); return
|
||||
cmd = ["disk", "backup"]
|
||||
if self.bdedup.isChecked(): cmd.append("--dedup")
|
||||
if self.bpw.text(): cmd += ["-p", self.bpw.text()]
|
||||
cmd += [o, s]; run_async(self, cmd, self.bbtn, self.blog)
|
||||
|
||||
def _restore(self):
|
||||
a, t = self.rarc.path(), self.rtgt.path()
|
||||
if not a or not t: QMessageBox.warning(self, "Zupt", "Set archive and target."); return
|
||||
if QMessageBox.warning(self, "Confirm", f"OVERWRITE {t}?", QMessageBox.Yes|QMessageBox.Cancel) != QMessageBox.Yes: return
|
||||
cmd = ["disk", "restore"]
|
||||
if self.rpw.text(): cmd += ["-p", self.rpw.text()]
|
||||
cmd += [a, t]; run_async(self, cmd, self.rbtn, self.rlog)
|
||||
|
||||
|
||||
class AboutTab(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
inner = QWidget()
|
||||
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(4)
|
||||
# Extract version number from cached string
|
||||
ver_num = ZUPT_VER_SHORT.replace("zupt ", "").strip() if "zupt " in ZUPT_VER_SHORT else ZUPT_VER_SHORT
|
||||
for text, style in [
|
||||
("ZUPT", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
|
||||
(ver_num, "color:white;font-size:28px;font-weight:800;font-family:monospace;"),
|
||||
("", ""),
|
||||
("Post-quantum backup compression with ML-KEM-768 + X25519", "color:#6a8898;font-size:13px;"),
|
||||
("hybrid encryption, hardware-adaptive codecs, and block dedup.", "color:#6a8898;font-size:13px;"),
|
||||
("", ""),
|
||||
("CRYPTOGRAPHIC STACK", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
|
||||
("ML-KEM-768 FIPS 203 Post-Quantum KEM", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("X25519 RFC 7748 Elliptic Curve DH", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("AES-256-CTR FIPS 197 Symmetric Cipher", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("HMAC-SHA256 RFC 2104 Authentication", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("PBKDF2 RFC 8018 Key Derivation", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("SHA3/SHAKE FIPS 202 Hash / XOF", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("", ""),
|
||||
("CREDITS", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
|
||||
("zupt Cristian Cezar Moises MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("github.com/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
|
||||
("", ""),
|
||||
("libzupt Alessandro de Oliveira Faria MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("github.com/cabelo/libzupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
|
||||
("", ""),
|
||||
("WEBSITE", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
|
||||
("https://zupt.securityops.co", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("zupt@riseup.net", "color:#5a7a88;font-size:12px;font-family:monospace;"),
|
||||
("", ""),
|
||||
(ZUPT_VER_SHORT, "color:#3a5868;font-size:11px;font-family:monospace;"),
|
||||
]:
|
||||
lbl = QLabel(text)
|
||||
if text == "": lbl.setFixedHeight(10)
|
||||
elif style: lbl.setStyleSheet(style)
|
||||
v.addWidget(lbl)
|
||||
v.addStretch()
|
||||
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
|
||||
|
||||
|
||||
# ── Main ──
|
||||
|
||||
class ZuptWindow(QMainWindow):
|
||||
def __init__(self, compress_files=None, extract_file=None):
|
||||
super().__init__()
|
||||
self.setWindowTitle(f"Zupt — {ZUPT_VER_SHORT}")
|
||||
self.setMinimumSize(720, 500)
|
||||
self.resize(880, 640)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
# Window icon
|
||||
if ICON_PATH:
|
||||
self.setWindowIcon(QIcon(ICON_PATH))
|
||||
|
||||
central = QWidget(); self.setCentralWidget(central)
|
||||
layout = QVBoxLayout(central); layout.setContentsMargins(0,0,0,0); layout.setSpacing(0)
|
||||
|
||||
# Header
|
||||
hdr = QFrame(); hdr.setStyleSheet("background:#050a0e;border-bottom:1px solid #1a2a30;")
|
||||
hl = QHBoxLayout(hdr); hl.setContentsMargins(20,10,20,10)
|
||||
title = QLabel("ZUPT"); title.setStyleSheet("color:white;font-size:15px;font-weight:800;letter-spacing:3px;")
|
||||
hl.addWidget(title)
|
||||
sub = QLabel("Post-Quantum Backup"); sub.setStyleSheet("color:#3a5868;font-size:10px;font-weight:600;letter-spacing:1px;margin-left:8px;")
|
||||
hl.addWidget(sub); hl.addStretch()
|
||||
ver_num = ZUPT_VER_SHORT.replace("zupt ", "v").strip()
|
||||
vl = QLabel(ver_num); vl.setStyleSheet("color:#3a5868;font-size:10px;font-family:monospace;background:#0a1018;padding:3px 10px;border-radius:4px;border:1px solid #1a2a30;")
|
||||
hl.addWidget(vl)
|
||||
layout.addWidget(hdr)
|
||||
|
||||
# Tabs
|
||||
self.tabs = QTabWidget()
|
||||
self.compress_tab = CompressTab(initial=compress_files)
|
||||
self.extract_tab = ExtractTab(initial=extract_file)
|
||||
self.tabs.addTab(KeysTab(), "Keys")
|
||||
self.tabs.addTab(self.compress_tab, "Compress")
|
||||
self.tabs.addTab(self.extract_tab, "Extract")
|
||||
self.tabs.addTab(VerifyTab(), "Verify")
|
||||
self.tabs.addTab(DiskTab(), "Disk")
|
||||
self.tabs.addTab(AboutTab(), "About")
|
||||
if compress_files: self.tabs.setCurrentIndex(1)
|
||||
elif extract_file: self.tabs.setCurrentIndex(2)
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
sb = QStatusBar()
|
||||
sb.showMessage(f"{ZUPT_VER_SHORT} | {ZUPT}")
|
||||
self.setStatusBar(sb)
|
||||
|
||||
def dragEnterEvent(self, e):
|
||||
if e.mimeData().hasUrls(): e.acceptProposedAction()
|
||||
def dropEvent(self, e):
|
||||
ps = [u.toLocalFile() for u in e.mimeData().urls() if u.toLocalFile()]
|
||||
if not ps: return
|
||||
if ps[0].endswith(".zupt"):
|
||||
self.extract_tab.arc.edit.setText(ps[0]); self.tabs.setCurrentIndex(2)
|
||||
else:
|
||||
self.compress_tab.src.edit.setText("|".join(ps)); self.tabs.setCurrentIndex(1)
|
||||
|
||||
|
||||
def main():
|
||||
compress_files = extract_file = None
|
||||
args = sys.argv[1:]
|
||||
if args:
|
||||
if args[0] == "--compress" and len(args) > 1: compress_files = args[1:]
|
||||
elif args[0] == "--extract" and len(args) > 1: extract_file = args[1]
|
||||
elif args[0].endswith(".zupt"): extract_file = args[0]
|
||||
else: compress_files = args
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Zupt")
|
||||
if ICON_PATH: app.setWindowIcon(QIcon(ICON_PATH))
|
||||
app.setStyle("Fusion")
|
||||
app.setStyleSheet(STYLE)
|
||||
pal = QPalette()
|
||||
for role, c in [(QPalette.Window,"#0a0a0a"),(QPalette.WindowText,"#90acb8"),
|
||||
(QPalette.Base,"#0e1820"),(QPalette.Text,"#c0dce8"),
|
||||
(QPalette.Button,"#0a2028"),(QPalette.ButtonText,"#00dde0"),
|
||||
(QPalette.Highlight,"#004858"),(QPalette.HighlightedText,"#00dde0")]:
|
||||
pal.setColor(role, QColor(c))
|
||||
app.setPalette(pal)
|
||||
win = ZuptWindow(compress_files=compress_files, extract_file=extract_file)
|
||||
win.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue