715 lines
25 KiB
HTML
715 lines
25 KiB
HTML
{% extends "base_admin.html" %}
|
|||
|
|
|
||
|
|
{% block title %}StFV Backup - Neues Backup{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<h2>Neues Backup einrichten</h2>
|
||
|
|
<p>Die Konfiguration erscheint Schritt fuer Schritt, damit du jedes Fenster nacheinander ausfuellen kannst.</p>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.wizard-step {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wizard-step.active {
|
||
|
|
display: block;
|
||
|
|
animation: fade-slide 240ms ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wizard-nav {
|
||
|
|
margin-top: 10px;
|
||
|
|
display: flex;
|
||
|
|
gap: 8px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wizard-pill {
|
||
|
|
border: 1px solid rgba(30, 42, 59, 0.2);
|
||
|
|
padding: 6px 10px;
|
||
|
|
border-radius: 999px;
|
||
|
|
font-size: 12px;
|
||
|
|
color: #5a6e87;
|
||
|
|
background: #fff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wizard-pill.active {
|
||
|
|
border-color: rgba(10, 127, 104, 0.35);
|
||
|
|
background: #e5fff7;
|
||
|
|
color: #0a7f68;
|
||
|
|
font-weight: 700;
|
||
|
|
}
|
||
|
|
|
||
|
|
.step-actions {
|
||
|
|
margin-top: 12px;
|
||
|
|
display: flex;
|
||
|
|
gap: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary {
|
||
|
|
border: 1px solid rgba(30, 42, 59, 0.2);
|
||
|
|
background: #fff;
|
||
|
|
color: #1e2a3b;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree {
|
||
|
|
list-style: none;
|
||
|
|
margin: 8px 0 0;
|
||
|
|
padding: 0 0 0 8px;
|
||
|
|
border-left: 1px solid rgba(30, 42, 59, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree li {
|
||
|
|
margin: 4px 0;
|
||
|
|
padding-left: 10px;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree li::before {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
left: -8px;
|
||
|
|
top: 12px;
|
||
|
|
width: 8px;
|
||
|
|
border-top: 1px solid rgba(30, 42, 59, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree-node {
|
||
|
|
border: 0;
|
||
|
|
background: transparent;
|
||
|
|
color: #1e2a3b;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
padding: 3px 4px;
|
||
|
|
border-radius: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree-node:hover {
|
||
|
|
background: #eef6ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree-node.active {
|
||
|
|
background: #e5fff7;
|
||
|
|
color: #0a7f68;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree-meta {
|
||
|
|
font-size: 13px;
|
||
|
|
color: #5a6e87;
|
||
|
|
margin-top: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tree-empty {
|
||
|
|
color: #5a6e87;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mini-input {
|
||
|
|
margin-top: 8px;
|
||
|
|
display: flex;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mini-input input {
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.inline-help {
|
||
|
|
color: #5a6e87;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes fade-slide {
|
||
|
|
from { opacity: 0; transform: translateY(8px); }
|
||
|
|
to { opacity: 1; transform: translateY(0); }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<form method="post" action="{{ url_for('backup_new') }}" id="backupForm">
|
||
|
|
<input type="hidden" id="source_entry_type" name="source_entry_type" value="directory">
|
||
|
|
<input type="hidden" id="target_kind" name="target_kind" value="folder">
|
||
|
|
|
||
|
|
<div class="wizard-nav" id="wizardNav">
|
||
|
|
<span class="wizard-pill active" data-step-pill="0">Allgemein</span>
|
||
|
|
<span class="wizard-pill" data-step-pill="1">Source</span>
|
||
|
|
<span class="wizard-pill" data-step-pill="2">Destination</span>
|
||
|
|
<span class="wizard-pill" data-step-pill="3">Target</span>
|
||
|
|
<span class="wizard-pill" data-step-pill="4">Zeitplan</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<section class="card wizard-step active" data-step="0">
|
||
|
|
<h3>Allgemein</h3>
|
||
|
|
<label for="name">Backup Name</label>
|
||
|
|
<input id="name" name="name" type="text" placeholder="z. B. unraid-nextcloud-daily" required>
|
||
|
|
<div class="step-actions">
|
||
|
|
<button type="button" class="btn" onclick="nextStep(0)">Weiter</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="card wizard-step" data-step="1">
|
||
|
|
<h3>Source</h3>
|
||
|
|
<label for="source_protocol">Typ</label>
|
||
|
|
<select id="source_protocol" name="source_protocol" required>
|
||
|
|
{% for protocol in target_protocols %}
|
||
|
|
<option value="{{ protocol }}">{{ protocol }}</option>
|
||
|
|
{% endfor %}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<div id="source_remote_fields">
|
||
|
|
<label for="source_host">Host/IP (optional mit :Port)</label>
|
||
|
|
<input id="source_host" name="source_host" type="text" placeholder="192.168.1.20 oder 192.168.1.20:445">
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div>
|
||
|
|
<label for="source_username">Benutzer</label>
|
||
|
|
<input id="source_username" name="source_username" type="text">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label for="source_password">Passwort</label>
|
||
|
|
<input id="source_password" name="source_password" type="password">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="source_share_row">
|
||
|
|
<label for="source_share">Share / Root</label>
|
||
|
|
<input id="source_share" name="source_share" type="text" placeholder="source-share">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<label for="source_subpath">Unterpfad (SMB Browser)</label>
|
||
|
|
<input id="source_subpath" name="source_subpath" type="text" placeholder="/daten/projektA" value="/">
|
||
|
|
<p id="source_test_message"></p>
|
||
|
|
<div id="source_browser" class="card" style="display:none; margin-top:10px;">
|
||
|
|
<div id="source_browser_meta" class="tree-meta"></div>
|
||
|
|
<ul id="source_browser_tree" class="tree"></ul>
|
||
|
|
</div>
|
||
|
|
<p class="inline-help">Ordnerauswahl setzt Source als Ordner, Dateiauswahl setzt Source als Datei.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="source_local_fields" style="display:none;">
|
||
|
|
<label for="source_local_path">Lokaler Pfad</label>
|
||
|
|
<input id="source_local_path" type="text" placeholder="/mnt/storage/quellordner">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="step-actions">
|
||
|
|
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Zurueck</button>
|
||
|
|
<button type="button" class="btn" onclick="nextStep(1)">Weiter</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="card wizard-step" data-step="2">
|
||
|
|
<h3>Destination</h3>
|
||
|
|
<label for="destination_protocol">Typ</label>
|
||
|
|
<select id="destination_protocol" name="destination_protocol" required>
|
||
|
|
{% for protocol in target_protocols %}
|
||
|
|
<option value="{{ protocol }}">{{ protocol }}</option>
|
||
|
|
{% endfor %}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<div id="destination_remote_fields">
|
||
|
|
<label for="destination_host">Host/IP (optional mit :Port)</label>
|
||
|
|
<input id="destination_host" name="destination_host" type="text" placeholder="192.168.1.30 oder 192.168.1.30:445">
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div>
|
||
|
|
<label for="destination_username">Benutzer</label>
|
||
|
|
<input id="destination_username" name="destination_username" type="text">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label for="destination_password">Passwort</label>
|
||
|
|
<input id="destination_password" name="destination_password" type="password">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="destination_share_row">
|
||
|
|
<label for="destination_share">Share / Root</label>
|
||
|
|
<input id="destination_share" name="destination_share" type="text" placeholder="backup-share">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<label for="destination_subpath">Unterpfad (SMB Browser)</label>
|
||
|
|
<input id="destination_subpath" name="destination_subpath" type="text" placeholder="/backups" value="/">
|
||
|
|
<div class="mini-input">
|
||
|
|
<input id="destination_new_folder" type="text" placeholder="Neuer Ordnername">
|
||
|
|
<button class="btn" type="button" onclick="createDestinationFolder()">Ordner erstellen</button>
|
||
|
|
</div>
|
||
|
|
<p id="destination_test_message"></p>
|
||
|
|
<div id="destination_browser" class="card" style="display:none; margin-top:10px;">
|
||
|
|
<div id="destination_browser_meta" class="tree-meta"></div>
|
||
|
|
<ul id="destination_browser_tree" class="tree"></ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="destination_local_fields" style="display:none;">
|
||
|
|
<label for="destination_local_path">Lokaler Pfad</label>
|
||
|
|
<input id="destination_local_path" type="text" placeholder="/mnt/storage/zielordner">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="step-actions">
|
||
|
|
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Zurueck</button>
|
||
|
|
<button type="button" class="btn" onclick="nextStep(2)">Weiter</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="card wizard-step" data-step="3">
|
||
|
|
<h3>Target</h3>
|
||
|
|
<label for="target_pattern" id="target_pattern_label">Zielordner</label>
|
||
|
|
<input id="target_pattern" name="target_pattern" type="text" placeholder="backup_{date}_{time}" required>
|
||
|
|
<p class="inline-help">Wenn in Source eine Datei gewaehlt wurde, ist dies die Zieldatei. Bei Ordnerauswahl ist dies der Zielordner.</p>
|
||
|
|
|
||
|
|
<label for="compression_method">Kompression</label>
|
||
|
|
<select id="compression_method" name="compression_method" required>
|
||
|
|
{% for method in compression_methods %}
|
||
|
|
<option value="{{ method }}">{{ method }}</option>
|
||
|
|
{% endfor %}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<label for="encryption_mode">Verschluesselung</label>
|
||
|
|
<select id="encryption_mode" name="encryption_mode" required>
|
||
|
|
{% for mode in encryption_modes %}
|
||
|
|
<option value="{{ mode }}">{{ mode }}</option>
|
||
|
|
{% endfor %}
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<div id="encryption_password_box">
|
||
|
|
<div class="row">
|
||
|
|
<div>
|
||
|
|
<label for="archive_password">Passwort</label>
|
||
|
|
<input id="archive_password" name="archive_password" type="password">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label for="archive_password_confirm">Passwort bestaetigen</label>
|
||
|
|
<input id="archive_password_confirm" name="archive_password_confirm" type="password">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p class="inline-help">Passwoerter werden verschluesselt gespeichert.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="step-actions">
|
||
|
|
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Zurueck</button>
|
||
|
|
<button type="button" class="btn" onclick="nextStep(3)">Weiter</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="card wizard-step" data-step="4">
|
||
|
|
<h3>Zeitplan</h3>
|
||
|
|
<label for="schedule_mode">Ausfuehrung</label>
|
||
|
|
<select id="schedule_mode" name="schedule_mode" required>
|
||
|
|
<option value="daily">Taeglich</option>
|
||
|
|
<option value="weekly">Woechentlich</option>
|
||
|
|
<option value="monthly">Monatlich</option>
|
||
|
|
<option value="yearly">Jaehrlich</option>
|
||
|
|
<option value="every_n_days">Alle X Tage</option>
|
||
|
|
<option value="every_n_weeks">Alle X Wochen</option>
|
||
|
|
<option value="custom">Benutzerdefiniert (CRON)</option>
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<div id="schedule_time_box">
|
||
|
|
<label for="schedule_time">Uhrzeit</label>
|
||
|
|
<input id="schedule_time" name="schedule_time" type="time" value="02:00">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="schedule_weekday_box" style="display:none;">
|
||
|
|
<label for="schedule_weekday">Wochentag</label>
|
||
|
|
<select id="schedule_weekday" name="schedule_weekday">
|
||
|
|
<option value="1">Montag</option>
|
||
|
|
<option value="2">Dienstag</option>
|
||
|
|
<option value="3">Mittwoch</option>
|
||
|
|
<option value="4">Donnerstag</option>
|
||
|
|
<option value="5">Freitag</option>
|
||
|
|
<option value="6">Samstag</option>
|
||
|
|
<option value="0">Sonntag</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="schedule_dom_box" style="display:none;">
|
||
|
|
<label for="schedule_day_of_month">Tag im Monat</label>
|
||
|
|
<input id="schedule_day_of_month" name="schedule_day_of_month" type="number" min="1" max="31" value="1">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="schedule_interval_box" style="display:none;">
|
||
|
|
<label for="schedule_interval">Intervall (X)</label>
|
||
|
|
<input id="schedule_interval" name="schedule_interval" type="number" min="1" value="1">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="schedule_cron_box" style="display:none;">
|
||
|
|
<label for="schedule_cron">CRON Ausdruck</label>
|
||
|
|
<input id="schedule_cron" name="schedule_cron" type="text" placeholder="0 2 * * *">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="step-actions">
|
||
|
|
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Zurueck</button>
|
||
|
|
<button class="btn" type="submit">Backup speichern</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let currentStep = 0;
|
||
|
|
|
||
|
|
function htmlEscape(value) {
|
||
|
|
return String(value)
|
||
|
|
.replaceAll('&', '&')
|
||
|
|
.replaceAll('<', '<')
|
||
|
|
.replaceAll('>', '>')
|
||
|
|
.replaceAll('"', '"')
|
||
|
|
.replaceAll("'", ''');
|
||
|
|
}
|
||
|
|
|
||
|
|
function showStep(stepIdx) {
|
||
|
|
const steps = document.querySelectorAll('.wizard-step');
|
||
|
|
const pills = document.querySelectorAll('[data-step-pill]');
|
||
|
|
steps.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
|
||
|
|
pills.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
|
||
|
|
currentStep = stepIdx;
|
||
|
|
}
|
||
|
|
|
||
|
|
function requireValue(id, message) {
|
||
|
|
const el = document.getElementById(id);
|
||
|
|
if (!el || !el.value.trim()) {
|
||
|
|
alert(message);
|
||
|
|
el?.focus();
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function validateStep(stepIdx) {
|
||
|
|
if (stepIdx === 0) {
|
||
|
|
return requireValue('name', 'Bitte Backup Name setzen.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (stepIdx === 1) {
|
||
|
|
const protocol = document.getElementById('source_protocol').value;
|
||
|
|
if (protocol === 'LOCAL') {
|
||
|
|
return requireValue('source_local_path', 'Bitte lokalen Source-Pfad angeben.');
|
||
|
|
}
|
||
|
|
if (!requireValue('source_host', 'Bitte Source Host/IP setzen.')) return false;
|
||
|
|
if (!requireValue('source_username', 'Bitte Source Benutzer setzen.')) return false;
|
||
|
|
if (!requireValue('source_password', 'Bitte Source Passwort setzen.')) return false;
|
||
|
|
if (protocol === 'SMB' && !requireValue('source_share', 'Bitte Source Share setzen.')) return false;
|
||
|
|
return requireValue('source_subpath', 'Bitte Source Unterpfad setzen.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (stepIdx === 2) {
|
||
|
|
const protocol = document.getElementById('destination_protocol').value;
|
||
|
|
if (protocol === 'LOCAL') {
|
||
|
|
return requireValue('destination_local_path', 'Bitte lokalen Destination-Pfad angeben.');
|
||
|
|
}
|
||
|
|
if (!requireValue('destination_host', 'Bitte Destination Host/IP setzen.')) return false;
|
||
|
|
if (!requireValue('destination_username', 'Bitte Destination Benutzer setzen.')) return false;
|
||
|
|
if (!requireValue('destination_password', 'Bitte Destination Passwort setzen.')) return false;
|
||
|
|
if (protocol === 'SMB' && !requireValue('destination_share', 'Bitte Destination Share setzen.')) return false;
|
||
|
|
return requireValue('destination_subpath', 'Bitte Destination Unterpfad setzen.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (stepIdx === 3) {
|
||
|
|
if (!requireValue('target_pattern', 'Bitte Zielordner oder Zieldatei setzen.')) return false;
|
||
|
|
const mode = document.getElementById('encryption_mode').value;
|
||
|
|
if (mode === 'password-aes256') {
|
||
|
|
if (!requireValue('archive_password', 'Bitte Verschluesselungs-Passwort setzen.')) return false;
|
||
|
|
if (!requireValue('archive_password_confirm', 'Bitte Passwort bestaetigen.')) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function nextStep(stepIdx) {
|
||
|
|
if (!validateStep(stepIdx)) return;
|
||
|
|
showStep(Math.min(stepIdx + 1, 4));
|
||
|
|
}
|
||
|
|
|
||
|
|
function prevStep(stepIdx) {
|
||
|
|
showStep(Math.max(stepIdx - 1, 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
function setRequired(el, enabled) {
|
||
|
|
if (!el) return;
|
||
|
|
if (enabled) {
|
||
|
|
el.setAttribute('required', 'required');
|
||
|
|
} else {
|
||
|
|
el.removeAttribute('required');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncProtocolFields(prefix) {
|
||
|
|
const protocol = document.getElementById(prefix + '_protocol').value;
|
||
|
|
const remoteFields = document.getElementById(prefix + '_remote_fields');
|
||
|
|
const localFields = document.getElementById(prefix + '_local_fields');
|
||
|
|
const shareRow = document.getElementById(prefix + '_share_row');
|
||
|
|
const host = document.getElementById(prefix + '_host');
|
||
|
|
const share = document.getElementById(prefix + '_share');
|
||
|
|
const subpath = document.getElementById(prefix + '_subpath');
|
||
|
|
const user = document.getElementById(prefix + '_username');
|
||
|
|
const pass = document.getElementById(prefix + '_password');
|
||
|
|
const localPath = document.getElementById(prefix + '_local_path');
|
||
|
|
|
||
|
|
if (protocol === 'LOCAL') {
|
||
|
|
remoteFields.style.display = 'none';
|
||
|
|
localFields.style.display = 'block';
|
||
|
|
shareRow.style.display = 'none';
|
||
|
|
setRequired(host, false);
|
||
|
|
setRequired(share, false);
|
||
|
|
setRequired(user, false);
|
||
|
|
setRequired(pass, false);
|
||
|
|
setRequired(localPath, true);
|
||
|
|
subpath.value = localPath.value || '';
|
||
|
|
share.value = '';
|
||
|
|
user.value = '';
|
||
|
|
pass.value = '';
|
||
|
|
host.value = 'local';
|
||
|
|
if (prefix === 'source') {
|
||
|
|
setSourceEntryType('directory');
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
remoteFields.style.display = 'block';
|
||
|
|
localFields.style.display = 'none';
|
||
|
|
shareRow.style.display = protocol === 'SMB' ? 'block' : 'none';
|
||
|
|
setRequired(host, true);
|
||
|
|
setRequired(share, protocol === 'SMB');
|
||
|
|
setRequired(user, true);
|
||
|
|
setRequired(pass, true);
|
||
|
|
setRequired(localPath, false);
|
||
|
|
|
||
|
|
if (protocol === 'FTP' || protocol === 'SFTP') {
|
||
|
|
if (!share.value) share.value = '/';
|
||
|
|
if (!subpath.value) subpath.value = '/';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectPayload(prefix) {
|
||
|
|
const protocol = document.getElementById(prefix + '_protocol').value;
|
||
|
|
const payload = {
|
||
|
|
prefix: prefix,
|
||
|
|
[prefix + '_protocol']: protocol,
|
||
|
|
[prefix + '_host']: document.getElementById(prefix + '_host').value,
|
||
|
|
[prefix + '_share']: document.getElementById(prefix + '_share').value,
|
||
|
|
[prefix + '_subpath']: document.getElementById(prefix + '_subpath').value,
|
||
|
|
[prefix + '_username']: document.getElementById(prefix + '_username').value,
|
||
|
|
[prefix + '_password']: document.getElementById(prefix + '_password').value
|
||
|
|
};
|
||
|
|
|
||
|
|
if (protocol === 'LOCAL') {
|
||
|
|
payload[prefix + '_host'] = 'local';
|
||
|
|
payload[prefix + '_share'] = '';
|
||
|
|
payload[prefix + '_username'] = '';
|
||
|
|
payload[prefix + '_password'] = '';
|
||
|
|
payload[prefix + '_subpath'] = document.getElementById(prefix + '_local_path').value;
|
||
|
|
document.getElementById(prefix + '_subpath').value = payload[prefix + '_subpath'];
|
||
|
|
}
|
||
|
|
|
||
|
|
return payload;
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearActiveTreeNodes(prefix) {
|
||
|
|
document
|
||
|
|
.querySelectorAll('#' + prefix + '_browser_tree .tree-node')
|
||
|
|
.forEach((node) => node.classList.remove('active'));
|
||
|
|
}
|
||
|
|
|
||
|
|
function setSourceEntryType(entryType) {
|
||
|
|
const t = entryType === 'file' ? 'file' : 'directory';
|
||
|
|
document.getElementById('source_entry_type').value = t;
|
||
|
|
const targetKind = t === 'file' ? 'file' : 'folder';
|
||
|
|
document.getElementById('target_kind').value = targetKind;
|
||
|
|
const label = document.getElementById('target_pattern_label');
|
||
|
|
label.textContent = targetKind === 'file' ? 'Zieldatei' : 'Zielordner';
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeTreeItem(prefix, entry) {
|
||
|
|
const li = document.createElement('li');
|
||
|
|
const button = document.createElement('button');
|
||
|
|
button.type = 'button';
|
||
|
|
button.className = 'tree-node';
|
||
|
|
|
||
|
|
let icon = '📁';
|
||
|
|
if (entry.entry_type === 'up') icon = '↩';
|
||
|
|
if (entry.entry_type === 'file') icon = '📄';
|
||
|
|
button.textContent = icon + ' ' + entry.name;
|
||
|
|
button.dataset.path = entry.path;
|
||
|
|
button.dataset.entryType = entry.entry_type;
|
||
|
|
|
||
|
|
const childList = document.createElement('ul');
|
||
|
|
childList.className = 'tree';
|
||
|
|
childList.style.display = 'none';
|
||
|
|
|
||
|
|
button.addEventListener('click', async () => {
|
||
|
|
clearActiveTreeNodes(prefix);
|
||
|
|
button.classList.add('active');
|
||
|
|
document.getElementById(prefix + '_subpath').value = entry.path;
|
||
|
|
|
||
|
|
if (prefix === 'source') {
|
||
|
|
setSourceEntryType(entry.entry_type === 'file' ? 'file' : 'directory');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.entry_type === 'file') {
|
||
|
|
document.getElementById(prefix + '_test_message').textContent = 'Datei ausgewaehlt: ' + entry.path;
|
||
|
|
document.getElementById(prefix + '_test_message').style.color = '#0a7f68';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await loadTreeLevel(prefix, entry.path, childList);
|
||
|
|
});
|
||
|
|
|
||
|
|
li.appendChild(button);
|
||
|
|
li.appendChild(childList);
|
||
|
|
return li;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadTreeLevel(prefix, path, targetList) {
|
||
|
|
const payload = collectPayload(prefix);
|
||
|
|
payload[prefix + '_subpath'] = path;
|
||
|
|
|
||
|
|
const response = await fetch('{{ url_for("api_target_browse_smb") }}', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(payload)
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
const msg = document.getElementById(prefix + '_test_message');
|
||
|
|
if (!data.ok) {
|
||
|
|
msg.textContent = data.message || 'SMB-Browsing fehlgeschlagen.';
|
||
|
|
msg.style.color = '#b9382f';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
msg.textContent = 'SMB-Verbindung erfolgreich.';
|
||
|
|
msg.style.color = '#0a7f68';
|
||
|
|
document.getElementById(prefix + '_subpath').value = data.path;
|
||
|
|
document.getElementById(prefix + '_browser_meta').textContent = 'Aktueller Pfad: ' + data.path;
|
||
|
|
|
||
|
|
targetList.innerHTML = '';
|
||
|
|
const entries = data.entries || [];
|
||
|
|
if (!entries.length) {
|
||
|
|
const empty = document.createElement('li');
|
||
|
|
empty.className = 'tree-empty';
|
||
|
|
empty.textContent = 'Keine Eintraege';
|
||
|
|
targetList.appendChild(empty);
|
||
|
|
targetList.style.display = 'block';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
entries.forEach((entry) => {
|
||
|
|
targetList.appendChild(makeTreeItem(prefix, entry));
|
||
|
|
});
|
||
|
|
targetList.style.display = 'block';
|
||
|
|
}
|
||
|
|
|
||
|
|
async function renderRootTree(prefix) {
|
||
|
|
const rootList = document.getElementById(prefix + '_browser_tree');
|
||
|
|
rootList.innerHTML = '';
|
||
|
|
await loadTreeLevel(prefix, document.getElementById(prefix + '_subpath').value || '/', rootList);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function browseSmb(prefix) {
|
||
|
|
if (document.getElementById(prefix + '_protocol').value !== 'SMB') {
|
||
|
|
const msg = document.getElementById(prefix + '_test_message');
|
||
|
|
msg.textContent = 'SMB Browser nur bei SMB aktiv.';
|
||
|
|
msg.style.color = '#b9382f';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const container = document.getElementById(prefix + '_browser');
|
||
|
|
container.style.display = 'block';
|
||
|
|
await renderRootTree(prefix);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function createDestinationFolder() {
|
||
|
|
const folderInput = document.getElementById('destination_new_folder');
|
||
|
|
const name = folderInput.value.trim();
|
||
|
|
const msg = document.getElementById('destination_test_message');
|
||
|
|
|
||
|
|
if (!name) {
|
||
|
|
msg.textContent = 'Bitte zuerst einen neuen Ordnernamen eingeben.';
|
||
|
|
msg.style.color = '#b9382f';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const payload = collectPayload('destination');
|
||
|
|
payload.folder_name = name;
|
||
|
|
|
||
|
|
const response = await fetch('{{ url_for("api_target_smb_mkdir") }}', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(payload)
|
||
|
|
});
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.ok) {
|
||
|
|
msg.textContent = data.message || 'Ordner konnte nicht erstellt werden.';
|
||
|
|
msg.style.color = '#b9382f';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
folderInput.value = '';
|
||
|
|
document.getElementById('destination_subpath').value = data.path;
|
||
|
|
msg.textContent = 'Ordner erstellt: ' + data.path;
|
||
|
|
msg.style.color = '#0a7f68';
|
||
|
|
await browseSmb('destination');
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncEncryptionFields() {
|
||
|
|
const mode = document.getElementById('encryption_mode').value;
|
||
|
|
const box = document.getElementById('encryption_password_box');
|
||
|
|
const pwd = document.getElementById('archive_password');
|
||
|
|
const pwd2 = document.getElementById('archive_password_confirm');
|
||
|
|
const visible = mode === 'password-aes256';
|
||
|
|
box.style.display = visible ? 'block' : 'none';
|
||
|
|
setRequired(pwd, visible);
|
||
|
|
setRequired(pwd2, visible);
|
||
|
|
if (!visible) {
|
||
|
|
pwd.value = '';
|
||
|
|
pwd2.value = '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncScheduleFields() {
|
||
|
|
const mode = document.getElementById('schedule_mode').value;
|
||
|
|
document.getElementById('schedule_weekday_box').style.display = mode === 'weekly' || mode === 'every_n_weeks' ? 'block' : 'none';
|
||
|
|
document.getElementById('schedule_dom_box').style.display = mode === 'monthly' || mode === 'yearly' ? 'block' : 'none';
|
||
|
|
document.getElementById('schedule_interval_box').style.display = mode === 'every_n_days' || mode === 'every_n_weeks' ? 'block' : 'none';
|
||
|
|
document.getElementById('schedule_cron_box').style.display = mode === 'custom' ? 'block' : 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
document.getElementById('source_protocol').addEventListener('change', () => syncProtocolFields('source'));
|
||
|
|
document.getElementById('destination_protocol').addEventListener('change', () => syncProtocolFields('destination'));
|
||
|
|
document.getElementById('source_local_path').addEventListener('input', (ev) => {
|
||
|
|
document.getElementById('source_subpath').value = ev.target.value;
|
||
|
|
});
|
||
|
|
document.getElementById('destination_local_path').addEventListener('input', (ev) => {
|
||
|
|
document.getElementById('destination_subpath').value = ev.target.value;
|
||
|
|
});
|
||
|
|
document.getElementById('source_subpath').addEventListener('focus', () => {
|
||
|
|
if (document.getElementById('source_protocol').value === 'SMB') {
|
||
|
|
browseSmb('source');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
document.getElementById('destination_subpath').addEventListener('focus', () => {
|
||
|
|
if (document.getElementById('destination_protocol').value === 'SMB') {
|
||
|
|
browseSmb('destination');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
document.getElementById('source_subpath').addEventListener('click', () => {
|
||
|
|
if (document.getElementById('source_protocol').value === 'SMB') {
|
||
|
|
browseSmb('source');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
document.getElementById('destination_subpath').addEventListener('click', () => {
|
||
|
|
if (document.getElementById('destination_protocol').value === 'SMB') {
|
||
|
|
browseSmb('destination');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
document.getElementById('encryption_mode').addEventListener('change', syncEncryptionFields);
|
||
|
|
document.getElementById('schedule_mode').addEventListener('change', syncScheduleFields);
|
||
|
|
|
||
|
|
syncProtocolFields('source');
|
||
|
|
syncProtocolFields('destination');
|
||
|
|
syncEncryptionFields();
|
||
|
|
syncScheduleFields();
|
||
|
|
setSourceEntryType('directory');
|
||
|
|
showStep(0);
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|