Fix and clean up this ugly ass AI code.
This commit is contained in:
parent
133238dfca
commit
66f4752409
8 changed files with 684 additions and 11 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,3 +4,6 @@ pluginConfigs/*
|
|||
temp/*
|
||||
*.pyc
|
||||
.env
|
||||
build/*
|
||||
dist/*
|
||||
.venv/*
|
||||
116
README_UI.md
Normal file
116
README_UI.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# FFXIV Backup Restore UI
|
||||
|
||||
A Windows-native PyQt6 GUI for the FFXIV backup restore script.
|
||||
|
||||
## Features
|
||||
|
||||
- **Settings Tab**: Configure Nextcloud credentials and local paths
|
||||
- **Restore Tab**: Select backup and monitor restore progress in real-time
|
||||
- **Single Executable**: Packaged with PyInstaller for easy distribution
|
||||
- **Threading**: Non-blocking UI during restore operations
|
||||
- **Status Output**: Real-time console output display
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- Windows (tested on Windows 11)
|
||||
- 7z command-line tool (for extraction)
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Python Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Verify 7z is installed
|
||||
|
||||
The script requires 7z for extraction. Install it or ensure it's in your PATH.
|
||||
|
||||
## Usage
|
||||
|
||||
### Development (Running from source)
|
||||
|
||||
```bash
|
||||
python restore_ui.py
|
||||
```
|
||||
|
||||
### Building Executable
|
||||
|
||||
Run the build script:
|
||||
|
||||
```bash
|
||||
build.bat
|
||||
```
|
||||
|
||||
Or manually with PyInstaller:
|
||||
|
||||
```bash
|
||||
pyinstaller restore_ui.spec
|
||||
```
|
||||
|
||||
The executable will be created in the `dist\` directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is loaded from `.env` file in the same directory as the script. The file contains:
|
||||
|
||||
- **NC_URL**: Nextcloud server URL
|
||||
- **NC_USER**: Nextcloud username
|
||||
- **NC_PASS**: Nextcloud password
|
||||
- **NC_TEMP_DOWNLOAD**: Path to store downloaded .7z file
|
||||
- **TEMP_EXTRACT_PATH**: Path to extract backup contents
|
||||
- **TEMP_BACKUP_PATH**: Directory to store local backups
|
||||
- **PLUGIN_CONFIGS_PATH**: XIVLauncher plugin config directory
|
||||
- **FFXIV_CONFIGS_PATH**: FFXIV game config directory
|
||||
|
||||
## Usage Steps
|
||||
|
||||
1. **Configure Settings** (if needed):
|
||||
- Open "Settings" tab
|
||||
- Verify Nextcloud credentials and paths
|
||||
- Click outside tab to save (settings are read from .env on startup)
|
||||
|
||||
2. **Restore Backup**:
|
||||
- Open "Restore" tab
|
||||
- Enter remote archive path (e.g., `Backups/FFXIV/Desktop/backup_file.7z`)
|
||||
- Click "Start Restore"
|
||||
- Monitor progress in output window
|
||||
|
||||
3. **View Results**:
|
||||
- Success/error message appears upon completion
|
||||
- Output can be cleared with "Clear Output" button
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing Dependencies
|
||||
If you get import errors, run: `pip install -r requirements.txt`
|
||||
|
||||
### 7z not found
|
||||
Ensure 7z is installed and in your system PATH
|
||||
|
||||
### Nextcloud Connection Failed
|
||||
- Verify NC_URL is correct (including https://)
|
||||
- Check username and password are correct
|
||||
- Ensure network connection to Nextcloud server
|
||||
|
||||
### PyInstaller Build Issues
|
||||
If the .spec file fails:
|
||||
- Delete `build/` and `dist/` directories
|
||||
- Rebuild: `pyinstaller restore_ui.spec`
|
||||
|
||||
## Distributing the Executable
|
||||
|
||||
Once built, the executable (`dist\FFXIV-Backup-Restore.exe`) can be:
|
||||
- Copied to any Windows system with .NET Framework (included in Windows)
|
||||
- Distributed as a single file
|
||||
- No Python installation required on target system
|
||||
- Requires `.env` file in same directory as executable
|
||||
|
||||
## Notes
|
||||
|
||||
- The UI runs the original `restore_backup.py` script in a background thread
|
||||
- All output is captured and displayed in the UI
|
||||
- Settings tab is for informational purposes (values are read from `.env` at startup)
|
||||
- To update settings, edit `.env` file directly and restart the application
|
||||
13
build.bat
Normal file
13
build.bat
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@echo off
|
||||
REM Install dependencies
|
||||
echo Installing dependencies...
|
||||
pip install -r requirements.txt
|
||||
|
||||
REM Build executable using PyInstaller
|
||||
echo.
|
||||
echo Building executable...
|
||||
pyinstaller restore_ui.spec
|
||||
|
||||
echo.
|
||||
echo Build complete! Executable located at: dist\FFXIV-Backup-Restore.exe
|
||||
pause
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
PyQt6==6.7.1
|
||||
nextcloud-api-wrapper==0.2.3
|
||||
python-dotenv==1.2.2
|
||||
python-dotenv==1.0.1
|
||||
pyinstaller==6.9.0
|
||||
|
|
|
|||
23
restore_backup.py
Executable file → Normal file
23
restore_backup.py
Executable file → Normal file
|
|
@ -6,16 +6,17 @@ import argparse
|
|||
import subprocess
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from nextcloud import NextCloud
|
||||
from nextcloud import NextCloud # type: ignore
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
load_dotenv(os.curdir + "\\.env")
|
||||
|
||||
|
||||
# --- CONFIGURATION FROM ENVIRONMENT ---
|
||||
NC_URL = os.getenv("NC_URL")
|
||||
NC_USER = os.getenv("NC_USER")
|
||||
NC_PASS = os.getenv("NC_PASS")
|
||||
TEMP_DOWNLOAD_NAME = os.getenv("NC_TEMP_DOWNLOAD", "/tmp/temp_download.7z")
|
||||
TEMP_DOWNLOAD_NAME = os.getenv("NC_TEMP_DOWNLOAD", "\tmp\temp_download.7z")
|
||||
TEMP_EXTRACT_PATH = os.getenv("TEMP_EXTRACT_PATH")
|
||||
TEMP_BACKUP_PATH = os.getenv("TEMP_BACKUP_PATH")
|
||||
PLUGIN_CONFIG_PATH = os.getenv("PLUGIN_CONFIGS_PATH")
|
||||
|
|
@ -51,7 +52,7 @@ Download a .7z archive from Nextcloud, backup local files, and replace them.""")
|
|||
"-r",
|
||||
"--remote",
|
||||
required=True,
|
||||
help="Path to the .7z archive file inside Nextcloud (e.g., 'backups/data.7z')",
|
||||
help="Path to the .7z archive file inside Nextcloud (e.g., 'backups\\data.7z')",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
|
@ -106,12 +107,16 @@ def backup_and_restore(
|
|||
# Create backup of FFXIV config directory if it exists and has content
|
||||
if os.path.exists(restore_config_path) and os.listdir(restore_config_path):
|
||||
print(
|
||||
"Creating safety backup of " + "ffxiv"
|
||||
if is_ffxiv_config
|
||||
else "plugins" + f" configs at:\n -> {backup_config_path}/game"
|
||||
"Creating safety backup of "
|
||||
+ f"{'ffxiv' if is_ffxiv_config else 'plugins'} configs at:"
|
||||
+ f"-> {backup_config_path}\\{'game' if is_ffxiv_config else 'plugins'}"
|
||||
)
|
||||
try:
|
||||
shutil.copytree(restore_config_path, f"{backup_config_path}/game")
|
||||
shutil.copytree(
|
||||
restore_config_path,
|
||||
f"{backup_config_path}\\{'game' if is_ffxiv_config else 'plugins'}",
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
shutil.rmtree(restore_config_path)
|
||||
except Exception as e:
|
||||
print(f"Failed during ffxiv config backup creation phase: {e}")
|
||||
|
|
@ -121,7 +126,7 @@ def backup_and_restore(
|
|||
try:
|
||||
print("Attempting to restore ffxiv config backups")
|
||||
shutil.copytree(
|
||||
f"{TEMP_EXTRACT_PATH}/{"game" if is_ffxiv_config else "plugins"}/",
|
||||
f"{TEMP_EXTRACT_PATH}\\{"game" if is_ffxiv_config else "plugins"}\\",
|
||||
restore_config_path,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
|
|
|
|||
38
restore_backup.spec
Normal file
38
restore_backup.spec
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['restore_backup.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='restore_backup',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
451
restore_ui.py
Normal file
451
restore_ui.py
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from dotenv import load_dotenv
|
||||
from datetime import datetime
|
||||
from nextcloud import NextCloud
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication,
|
||||
QMainWindow,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QTabWidget,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QTextEdit,
|
||||
QFileDialog,
|
||||
QMessageBox,
|
||||
QProgressBar,
|
||||
QGroupBox,
|
||||
QFormLayout,
|
||||
QDialog,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QFont
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
class WebDAVBrowser(QDialog):
|
||||
def __init__(self, parent, nc_url, nc_user, nc_pass):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Browse Nextcloud")
|
||||
self.setGeometry(100, 100, 600, 400)
|
||||
self.nc_url = nc_url
|
||||
self.nc_user = nc_user
|
||||
self.nc_pass = nc_pass
|
||||
self.selected_path = None
|
||||
self.current_path = "/"
|
||||
|
||||
try:
|
||||
self.nc = NextCloud(nc_url, user=nc_user, password=nc_pass)
|
||||
self.nc.login()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self, "Connection Error", f"Failed to connect to Nextcloud: {str(e)}"
|
||||
)
|
||||
self.reject()
|
||||
return
|
||||
|
||||
self.init_ui()
|
||||
self.load_directory(self.current_path)
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Path display and navigation
|
||||
nav_layout = QHBoxLayout()
|
||||
self.path_label = QLabel(self.current_path)
|
||||
nav_layout.addWidget(QLabel("Current Path:"), 0)
|
||||
nav_layout.addWidget(self.path_label, 1)
|
||||
|
||||
up_btn = QPushButton("Up")
|
||||
up_btn.clicked.connect(self.go_up)
|
||||
nav_layout.addWidget(up_btn)
|
||||
|
||||
layout.addLayout(nav_layout)
|
||||
|
||||
# File list
|
||||
self.file_list = QListWidget()
|
||||
self.file_list.itemDoubleClicked.connect(self.item_double_clicked)
|
||||
layout.addWidget(self.file_list)
|
||||
|
||||
# Buttons
|
||||
btn_layout = QHBoxLayout()
|
||||
select_btn = QPushButton("Select")
|
||||
select_btn.clicked.connect(self.select_file)
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
btn_layout.addStretch()
|
||||
btn_layout.addWidget(select_btn)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def load_directory(self, path):
|
||||
try:
|
||||
self.file_list.clear()
|
||||
self.current_path = path
|
||||
self.path_label.setText(path)
|
||||
|
||||
response = self.nc.list_folders(path=path) # type: ignore
|
||||
pprint(response.json_data)
|
||||
|
||||
# Try to extract data from webDavResponse
|
||||
file_list = []
|
||||
if hasattr(response, "__iter__") and not isinstance(response, (str, bytes)):
|
||||
try:
|
||||
file_list = list(response)
|
||||
except TypeError:
|
||||
# If direct iteration fails, try accessing as dict keys
|
||||
if isinstance(response, dict):
|
||||
file_list = list(response.keys())
|
||||
elif hasattr(response, "data"):
|
||||
file_list = (
|
||||
response.data
|
||||
if isinstance(response.data, list)
|
||||
else list(response.data)
|
||||
)
|
||||
|
||||
for item in sorted(file_list):
|
||||
name = str(item).rstrip("/")
|
||||
if name:
|
||||
is_dir = str(item).endswith("/")
|
||||
display_name = f"[DIR] {name}" if is_dir else name
|
||||
list_item = QListWidgetItem(display_name)
|
||||
list_item.setData(Qt.ItemDataRole.UserRole, (name, is_dir))
|
||||
self.file_list.addItem(list_item)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to load directory: {str(e)}")
|
||||
|
||||
def item_double_clicked(self, item):
|
||||
name, is_dir = item.data(Qt.ItemDataRole.UserRole)
|
||||
if is_dir:
|
||||
new_path = self.current_path.rstrip("/") + "/" + name
|
||||
self.load_directory(new_path)
|
||||
|
||||
def go_up(self):
|
||||
if self.current_path != "/":
|
||||
parent = "/".join(self.current_path.rstrip("/").split("/")[:-1]) or "/"
|
||||
self.load_directory(parent)
|
||||
|
||||
def select_file(self):
|
||||
current_item = self.file_list.currentItem()
|
||||
if current_item:
|
||||
name, is_dir = current_item.data(Qt.ItemDataRole.UserRole)
|
||||
if not is_dir:
|
||||
self.selected_path = self.current_path.rstrip("/") + "/" + name
|
||||
self.accept()
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self, "Invalid Selection", "Please select a file, not a directory"
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a file")
|
||||
|
||||
|
||||
class RestoreWorker(QThread):
|
||||
output_signal = pyqtSignal(str)
|
||||
finished_signal = pyqtSignal(bool, str)
|
||||
|
||||
def __init__(self, remote_path):
|
||||
super().__init__()
|
||||
self.remote_path = remote_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# Run the restore script with the remote path
|
||||
cmd = [sys.executable, "restore_backup.py", "-r", self.remote_path]
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
self.output_signal.emit(line.rstrip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
self.finished_signal.emit(True, "Restore completed successfully!")
|
||||
else:
|
||||
self.finished_signal.emit(
|
||||
False, f"Restore failed with return code {process.returncode}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.finished_signal.emit(False, f"Error: {str(e)}")
|
||||
|
||||
|
||||
class RestoreUI(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("FFXIV Backup Restore")
|
||||
self.setGeometry(100, 100, 900, 700)
|
||||
|
||||
load_dotenv()
|
||||
self.restore_worker = None
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QVBoxLayout()
|
||||
|
||||
tabs = QTabWidget()
|
||||
tabs.addTab(self.create_settings_tab(), "Settings")
|
||||
tabs.addTab(self.create_restore_tab(), "Restore")
|
||||
|
||||
layout.addWidget(tabs)
|
||||
central_widget.setLayout(layout)
|
||||
|
||||
def create_settings_tab(self):
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Nextcloud Section
|
||||
nc_group = QGroupBox("Nextcloud Configuration")
|
||||
nc_layout = QFormLayout()
|
||||
|
||||
self.nc_url_input = QLineEdit(os.getenv("NC_URL", ""))
|
||||
self.nc_user_input = QLineEdit(os.getenv("NC_USER", ""))
|
||||
self.nc_pass_input = QLineEdit(os.getenv("NC_PASS", ""))
|
||||
self.nc_pass_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
|
||||
nc_layout.addRow("Nextcloud URL:", self.nc_url_input)
|
||||
nc_layout.addRow("Username:", self.nc_user_input)
|
||||
nc_layout.addRow("Password:", self.nc_pass_input)
|
||||
nc_group.setLayout(nc_layout)
|
||||
|
||||
# Paths Section
|
||||
paths_group = QGroupBox("Local Paths")
|
||||
paths_layout = QVBoxLayout()
|
||||
|
||||
# Temp directory (with browse)
|
||||
self.temp_dir_input = QLineEdit(
|
||||
os.path.dirname(os.getenv("NC_TEMP_DOWNLOAD", ""))
|
||||
)
|
||||
paths_layout.addLayout(
|
||||
self._create_path_row(
|
||||
"Temp Directory:", self.temp_dir_input, self.browse_temp_directory
|
||||
)
|
||||
)
|
||||
|
||||
# Temp filenames
|
||||
self.temp_download_name_input = QLineEdit(
|
||||
os.path.basename(os.getenv("NC_TEMP_DOWNLOAD", "temp_download.7z"))
|
||||
)
|
||||
temp_download_layout = QHBoxLayout()
|
||||
temp_download_layout.addWidget(QLabel("Temp Download Filename:"), 1)
|
||||
temp_download_layout.addWidget(self.temp_download_name_input, 3)
|
||||
paths_layout.addLayout(temp_download_layout)
|
||||
|
||||
self.temp_extract_name_input = QLineEdit(
|
||||
os.path.basename(os.getenv("TEMP_EXTRACT_PATH", "temp_extract"))
|
||||
)
|
||||
temp_extract_layout = QHBoxLayout()
|
||||
temp_extract_layout.addWidget(QLabel("Temp Extract Subdirectory:"), 1)
|
||||
temp_extract_layout.addWidget(self.temp_extract_name_input, 3)
|
||||
paths_layout.addLayout(temp_extract_layout)
|
||||
|
||||
# Other paths (with browse)
|
||||
self.temp_backup_input = QLineEdit(os.getenv("TEMP_BACKUP_PATH", ""))
|
||||
self.plugin_configs_input = QLineEdit(os.getenv("PLUGIN_CONFIGS_PATH", ""))
|
||||
self.ffxiv_configs_input = QLineEdit(os.getenv("FFXIV_CONFIGS_PATH", ""))
|
||||
|
||||
paths_layout.addLayout(
|
||||
self._create_path_row(
|
||||
"Temp Backup:", self.temp_backup_input, self.browse_temp_backup
|
||||
)
|
||||
)
|
||||
paths_layout.addLayout(
|
||||
self._create_path_row(
|
||||
"Plugin Configs:", self.plugin_configs_input, self.browse_plugin_configs
|
||||
)
|
||||
)
|
||||
paths_layout.addLayout(
|
||||
self._create_path_row(
|
||||
"FFXIV Configs:", self.ffxiv_configs_input, self.browse_ffxiv_configs
|
||||
)
|
||||
)
|
||||
|
||||
paths_group.setLayout(paths_layout)
|
||||
|
||||
layout.addWidget(nc_group)
|
||||
layout.addWidget(paths_group)
|
||||
layout.addStretch()
|
||||
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def _create_path_row(self, label, input_field, browse_callback):
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.addWidget(QLabel(label), 1)
|
||||
row_layout.addWidget(input_field, 3)
|
||||
browse_btn = QPushButton("Browse...")
|
||||
browse_btn.clicked.connect(browse_callback)
|
||||
row_layout.addWidget(browse_btn)
|
||||
return row_layout
|
||||
|
||||
def browse_temp_directory(self):
|
||||
path = QFileDialog.getExistingDirectory(self, "Select Temp Directory")
|
||||
if path:
|
||||
self.temp_dir_input.setText(path)
|
||||
|
||||
def browse_temp_backup(self):
|
||||
path = QFileDialog.getExistingDirectory(self, "Select Temp Backup Directory")
|
||||
if path:
|
||||
self.temp_backup_input.setText(path)
|
||||
|
||||
def browse_plugin_configs(self):
|
||||
path = QFileDialog.getExistingDirectory(self, "Select Plugin Configs Directory")
|
||||
if path:
|
||||
self.plugin_configs_input.setText(path)
|
||||
|
||||
def browse_ffxiv_configs(self):
|
||||
path = QFileDialog.getExistingDirectory(self, "Select FFXIV Configs Directory")
|
||||
if path:
|
||||
self.ffxiv_configs_input.setText(path)
|
||||
|
||||
def create_restore_tab(self):
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Remote path input
|
||||
remote_group = QGroupBox("Select Backup to Restore")
|
||||
remote_layout = QHBoxLayout()
|
||||
|
||||
QLabel("Remote Archive Path:").setFont(QFont("Arial", 10))
|
||||
self.remote_path_input = QLineEdit()
|
||||
self.remote_path_input.setPlaceholderText(
|
||||
"e.g., Backups/FFXIV/Desktop/backup_file.7z"
|
||||
)
|
||||
|
||||
remote_layout.addWidget(QLabel("Remote Archive Path:"))
|
||||
remote_layout.addWidget(self.remote_path_input)
|
||||
|
||||
browse_webdav_btn = QPushButton("Browse WebDAV")
|
||||
browse_webdav_btn.clicked.connect(self.browse_webdav)
|
||||
remote_layout.addWidget(browse_webdav_btn)
|
||||
|
||||
remote_group.setLayout(remote_layout)
|
||||
layout.addWidget(remote_group)
|
||||
|
||||
# Output display
|
||||
output_group = QGroupBox("Operation Progress")
|
||||
output_layout = QVBoxLayout()
|
||||
|
||||
self.output_text = QTextEdit()
|
||||
self.output_text.setReadOnly(True)
|
||||
self.output_text.setFont(QFont("Courier New", 9))
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
output_layout.addWidget(self.output_text)
|
||||
output_layout.addWidget(self.progress_bar)
|
||||
|
||||
output_group.setLayout(output_layout)
|
||||
layout.addWidget(output_group)
|
||||
|
||||
# Control buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.restore_button = QPushButton("Start Restore")
|
||||
self.restore_button.clicked.connect(self.start_restore)
|
||||
|
||||
self.clear_button = QPushButton("Clear Output")
|
||||
self.clear_button.clicked.connect(self.output_text.clear)
|
||||
|
||||
button_layout.addWidget(self.restore_button)
|
||||
button_layout.addWidget(self.clear_button)
|
||||
button_layout.addStretch()
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def browse_webdav(self):
|
||||
nc_url = self.nc_url_input.text().strip()
|
||||
nc_user = self.nc_user_input.text().strip()
|
||||
nc_pass = self.nc_pass_input.text().strip()
|
||||
|
||||
if not all([nc_url, nc_user, nc_pass]):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Missing Credentials",
|
||||
"Please configure Nextcloud credentials in Settings tab first",
|
||||
)
|
||||
return
|
||||
|
||||
browser = WebDAVBrowser(self, nc_url, nc_user, nc_pass)
|
||||
if browser.exec() == QDialog.DialogCode.Accepted and browser.selected_path:
|
||||
self.remote_path_input.setText(browser.selected_path)
|
||||
|
||||
def start_restore(self):
|
||||
remote_path = self.remote_path_input.text().strip()
|
||||
|
||||
if not remote_path:
|
||||
QMessageBox.warning(
|
||||
self, "Input Required", "Please enter the remote archive path"
|
||||
)
|
||||
return
|
||||
|
||||
self.output_text.clear()
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setMaximum(0)
|
||||
self.restore_button.setEnabled(False)
|
||||
|
||||
self.output_text.append(
|
||||
f"[{datetime.now().strftime('%H:%M:%S')}] Starting restore process..."
|
||||
)
|
||||
self.output_text.append(f"Remote path: {remote_path}\n")
|
||||
|
||||
self.restore_worker = RestoreWorker(remote_path)
|
||||
self.restore_worker.output_signal.connect(self.append_output)
|
||||
self.restore_worker.finished_signal.connect(self.restore_finished)
|
||||
self.restore_worker.start()
|
||||
|
||||
def append_output(self, text):
|
||||
self.output_text.append(text)
|
||||
self.output_text.verticalScrollBar().setValue(
|
||||
self.output_text.verticalScrollBar().maximum()
|
||||
)
|
||||
|
||||
def restore_finished(self, success, message):
|
||||
self.progress_bar.setVisible(False)
|
||||
self.restore_button.setEnabled(True)
|
||||
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.output_text.append(f"\n[{timestamp}] {message}")
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, "Success", message)
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", message)
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
window = RestoreUI()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
45
restore_ui.spec
Normal file
45
restore_ui.spec
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['restore_ui.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('.env', '.')], # Include .env file
|
||||
hiddenimports=['PyQt6.QtCore', 'PyQt6.QtGui', 'PyQt6.QtWidgets'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludedimports=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='FFXIV-Backup-Restore',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='icon.ico' if os.path.exists('icon.ico') else None,
|
||||
)
|
||||
|
||||
import os
|
||||
Loading…
Reference in a new issue