Implement /restore API

This commit is contained in:
Iam54r1n4
2025-02-22 10:01:16 +03:30
parent 4dd9215d1b
commit c399551a50
3 changed files with 110 additions and 57 deletions

View File

@ -180,7 +180,8 @@ def backup_hysteria2():
except Exception as ex:
raise
def restore_hysteria2(backup_file_path):
def restore_hysteria2(backup_file_path: str):
'''Restores Hysteria configuration from the given backup file.'''
try:
run_cmd(['bash', Command.RESTORE_HYSTERIA2.value, backup_file_path])

View File

@ -1,7 +1,8 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, UploadFile, File
from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse
from ..schema.response import DetailResponse
from fastapi.responses import FileResponse
import shutil
# from ..schema.config.hysteria import InstallInputBody
import os
import cli_api
@ -150,13 +151,13 @@ async def set_sni_api(sni: str):
@router.get('/backup', response_class=FileResponse, summary='Backup Hysteria2 configuration')
async def backup():
async def backup_api():
"""
Backups the Hysteria2 configuration and sends the backup ZIP file.
"""
try:
cli_api.backup_hysteria2()
backup_dir = "/opt/hysbackup/"
backup_dir = "/opt/hysbackup/" # TODO: get this path from .env
if not os.path.isdir(backup_dir):
raise HTTPException(status_code=500, detail="Backup directory does not exist.")
@ -182,6 +183,24 @@ async def backup():
raise HTTPException(status_code=500, detail=f'Error: {str(e)}')
@router.get('/restore', response_model=DetailResponse, summary='Restore Hysteria2 Configuration')
async def restore_api(f: UploadFile = File(...)):
try:
dst_dir_path = '/opt/hysbackup/' # TODO: get this path from .env
if not os.path.isdir(dst_dir_path): # TODO: the dir path should be exist, so no need to check
os.makedirs(dst_dir_path)
dst_path = os.path.join(dst_dir_path, f.filename) # type: ignore
with open(dst_path, 'wb') as buffer:
shutil.copyfileobj(f.file, buffer)
cli_api.restore_hysteria2(dst_path)
return DetailResponse(detail='Hysteria2 restored successfully.')
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.get('/enable-obfs', response_model=DetailResponse, summary='Enable Hysteria2 obfs')
async def enable_obfs():
"""

View File

@ -19,26 +19,39 @@
<div class='col-lg-12'>
<div class='card card-primary card-outline card-tabs'>
<div class='card-header p-0 pt-1 border-bottom-0'>
<ul class='nav nav-pills' id='custom-tabs-three-tab' role='tablist' style="margin-left: 20px; margin-top: 10px;">
<ul class='nav nav-pills' id='custom-tabs-three-tab' role='tablist'
style="margin-left: 20px; margin-top: 10px;">
<li class='nav-item'>
<a class='nav-link active' id='subs-tab' data-toggle='pill' href='#subs' role='tab' aria-controls='subs' aria-selected='false'><i class="fas fa-link"></i> Subscriptions</a>
<a class='nav-link active' id='subs-tab' data-toggle='pill' href='#subs' role='tab'
aria-controls='subs' aria-selected='false'><i class="fas fa-link"></i>
Subscriptions</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='telegram-tab' data-toggle='pill' href='#telegram' role='tab' aria-controls='telegram' aria-selected='true'><i class="fab fa-telegram"></i> Telegram
<a class='nav-link' id='telegram-tab' data-toggle='pill' href='#telegram' role='tab'
aria-controls='telegram' aria-selected='true'><i class="fab fa-telegram"></i>
Telegram
Bot</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='port-tab' data-toggle='pill' href='#port' role='tab' aria-controls='port' aria-selected='false'><i class="fas fa-server"></i> Change Port</a>
<a class='nav-link' id='port-tab' data-toggle='pill' href='#port' role='tab'
aria-controls='port' aria-selected='false'><i class="fas fa-server"></i> Change
Port</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='sni-tab' data-toggle='pill' href='#sni' role='tab' aria-controls='sni' aria-selected='false'><i class="fas fa-shield-alt"></i> Change SNI</a>
<a class='nav-link' id='sni-tab' data-toggle='pill' href='#sni' role='tab'
aria-controls='sni' aria-selected='false'><i class="fas fa-shield-alt"></i> Change
SNI</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='ip-tab' data-toggle='pill' href='#change_ip' role='tab' aria-controls='change_ip' aria-selected='false'><i class="fas fa-network-wired"></i> Change IP</a>
<a class='nav-link' id='ip-tab' data-toggle='pill' href='#change_ip' role='tab'
aria-controls='change_ip' aria-selected='false'><i class="fas fa-network-wired"></i>
Change IP</a>
</li>
<!-- New Backup Tab -->
<li class='nav-item'>
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab' aria-controls='backup' aria-selected='false'><i class="fas fa-download"></i> Backup</a>
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab'
aria-controls='backup' aria-selected='false'><i class="fas fa-download"></i>
Backup</a>
</li>
</ul>
</div>
@ -49,35 +62,41 @@
<div class='tab-pane fade show active' id='subs' role='tabpanel' aria-labelledby='subs-tab'>
<ul class='nav nav-tabs' id='subs-tabs' role='tablist'>
<li class='nav-item'>
<a class='nav-link active' id='singbox-tab' data-toggle='tab' href='#singbox' role='tab' aria-controls='singbox'
<a class='nav-link active' id='singbox-tab' data-toggle='tab' href='#singbox'
role='tab' aria-controls='singbox'
aria-selected='true'><strong>SingBox</strong></a>
</li>
<li class='nav-item'>
<a class='nav-link' id='normal-tab' data-toggle='tab' href='#normal' role='tab' aria-controls='normal'
aria-selected='false'><strong>Normal</strong></a>
<a class='nav-link' id='normal-tab' data-toggle='tab' href='#normal' role='tab'
aria-controls='normal' aria-selected='false'><strong>Normal</strong></a>
</li>
</ul>
<div class='tab-content' id='subs-tabs-content'>
<br>
<!-- SingBox Sub Tab -->
<div class='tab-pane fade show active' id='singbox' role='tabpanel' aria-labelledby='singbox-tab'>
<div class='tab-pane fade show active' id='singbox' role='tabpanel'
aria-labelledby='singbox-tab'>
<form>
<div class='form-group'>
<label for='singbox_domain'>Domain:</label>
<input type='text' class='form-control' id='singbox_domain' placeholder='Enter Domain'>
<input type='text' class='form-control' id='singbox_domain'
placeholder='Enter Domain'>
<div class="invalid-feedback">
Please enter a valid domain (without http:// or https://).
</div>
</div>
<div class='form-group'>
<label for='singbox_port'>Port:</label>
<input type='text' class='form-control' id='singbox_port' placeholder='Enter Port'>
<input type='text' class='form-control' id='singbox_port'
placeholder='Enter Port'>
<div class="invalid-feedback">
Please enter a valid port number.
</div>
</div>
<button id="singbox_start" type='button' class='btn btn-success'>Start</button>
<button id="singbox_stop" type='button' class='btn btn-danger' style="display: none;">Stop</button>
<button id="singbox_start" type='button'
class='btn btn-success'>Start</button>
<button id="singbox_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button>
</form>
</div>
@ -87,20 +106,24 @@
<form>
<div class='form-group'>
<label for='normal_domain'>Domain:</label>
<input type='text' class='form-control' id='normal_domain' placeholder='Enter Domain'>
<input type='text' class='form-control' id='normal_domain'
placeholder='Enter Domain'>
<div class="invalid-feedback">
Please enter a valid domain (without http:// or https://).
</div>
</div>
<div class='form-group'>
<label for='normal_port'>Port:</label>
<input type='text' class='form-control' id='normal_port' placeholder='Enter Port'>
<input type='text' class='form-control' id='normal_port'
placeholder='Enter Port'>
<div class="invalid-feedback">
Please enter a valid port number.
</div>
</div>
<button id="normal_start" type='button' class='btn btn-success'>Start</button>
<button id="normal_stop" type='button' class='btn btn-danger' style="display: none;">Stop</button>
<button id="normal_start" type='button'
class='btn btn-success'>Start</button>
<button id="normal_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button>
</form>
</div>
@ -113,20 +136,23 @@
<form>
<div class='form-group'>
<label for='telegram_api_token'>API Token:</label>
<input type='text' class='form-control' id='telegram_api_token' placeholder='Enter API Token'>
<input type='text' class='form-control' id='telegram_api_token'
placeholder='Enter API Token'>
<div class="invalid-feedback">
Please enter a valid API Token.
</div>
</div>
<div class='form-group'>
<label for='telegram_admin_id'>Admin ID:</label>
<input type='text' class='form-control' id='telegram_admin_id' placeholder='Enter Admin ID'>
<input type='text' class='form-control' id='telegram_admin_id'
placeholder='Enter Admin ID'>
<div class="invalid-feedback">
Please enter a valid Admin ID.
</div>
</div>
<button id="telegram_start" type='button' class='btn btn-success'>Start</button>
<button id="telegram_stop" type='button' class='btn btn-danger' style="display: none;">Stop</button>
<button id="telegram_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button>
</form>
</div>
@ -137,7 +163,8 @@
<form>
<div class='form-group'>
<label for='hysteria_port'>Port:</label>
<input type='text' class='form-control' id='hysteria_port' placeholder='Enter Port'>
<input type='text' class='form-control' id='hysteria_port'
placeholder='Enter Port'>
<div class="invalid-feedback">
Please enter a valid port number.
</div>
@ -152,7 +179,8 @@
<form>
<div class='form-group'>
<label for='sni_domain'>Domain:</label>
<input type='text' class='form-control' id='sni_domain' placeholder='Enter Domain'>
<input type='text' class='form-control' id='sni_domain'
placeholder='Enter Domain'>
<div class="invalid-feedback">
Please enter a valid domain (without http:// or https://).
</div>
@ -166,14 +194,16 @@
<form>
<div class='form-group'>
<label for='ipv4'>IPv4:</label>
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4' value="{{ ipv4 or '' }}">
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4'
value="{{ ipv4 or '' }}">
<div class="invalid-feedback">
Please enter a valid IPv4 address.
</div>
</div>
<div class='form-group'>
<label for='ipv6'>IPv6:</label>
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6' value="{{ ipv6 or '' }}">
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6'
value="{{ ipv6 or '' }}">
<div class="invalid-feedback">
Please enter a valid IPv6 address.
</div>
@ -183,7 +213,8 @@
</div>
<!-- Backup Tab (New) -->
<div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'>
<button id="download_backup" type='button' class='btn btn-primary'>Download Backup</button>
<button id="download_backup" type='button' class='btn btn-primary'>Download
Backup</button>
</div>
</div>
</div>
@ -200,7 +231,9 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Font Awesome -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js" integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3apsIOPr7/l+jCMq9Dn9x5qyuAGqgpr4/NBZ95p8yrl/sLhJvoazg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"
integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3apsIOPr7/l+jCMq9Dn9x5qyuAGqgpr4/NBZ95p8yrl/sLhJvoazg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
@ -356,7 +389,7 @@
function validateForm(formId) {
let isValid = true;
$(`#${formId} .form-control`).each(function() {
$(`#${formId} .form-control`).each(function () {
const input = $(this);
const id = input.attr('id');
@ -521,7 +554,7 @@
// Download Backup (New)
function downloadBackup() {
// No confirmation needed for a download
window.location.href = "{{ url_for('backup') }}"; // Initiate the download
window.location.href = "{{ url_for('backup_api') }}"; // Initiate the download
}
@ -538,7 +571,7 @@
$("#download_backup").on("click", downloadBackup); // New backup button
// Input event listeners for real-time validation
$('#singbox_domain, #normal_domain, #sni_domain').on('input', function() {
$('#singbox_domain, #normal_domain, #sni_domain').on('input', function () {
if (isValidDomain($(this).val())) {
$(this).removeClass('is-invalid');
} else {
@ -546,29 +579,29 @@
}
});
$('#singbox_port, #normal_port, #hysteria_port').on('input', function() {
$('#singbox_port, #normal_port, #hysteria_port').on('input', function () {
if (isValidPort($(this).val())) {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
});
$('#ipv4').on('input', function() {
if (isValidIP($(this).val(),4)) {
$('#ipv4').on('input', function () {
if (isValidIP($(this).val(), 4)) {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
});
$('#ipv6').on('input', function() {
if (isValidIP($(this).val(),6)) {
$('#ipv6').on('input', function () {
if (isValidIP($(this).val(), 6)) {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
});
$('#telegram_api_token, #telegram_admin_id').on('input', function() {
$('#telegram_api_token, #telegram_admin_id').on('input', function () {
if ($(this).val().trim() !== "") { // Basic check for non-empty
$(this).removeClass('is-invalid');
} else {