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
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -3,4 +3,7 @@ FINAL FANTASY XIV - A Realm Reborn/*
|
||||||
pluginConfigs/*
|
pluginConfigs/*
|
||||||
temp/*
|
temp/*
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.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
|
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
|
import subprocess
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from nextcloud import NextCloud
|
from nextcloud import NextCloud # type: ignore
|
||||||
|
|
||||||
# Load environment variables from .env file
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv(os.curdir + "\\.env")
|
||||||
|
|
||||||
|
|
||||||
# --- CONFIGURATION FROM ENVIRONMENT ---
|
# --- CONFIGURATION FROM ENVIRONMENT ---
|
||||||
NC_URL = os.getenv("NC_URL")
|
NC_URL = os.getenv("NC_URL")
|
||||||
NC_USER = os.getenv("NC_USER")
|
NC_USER = os.getenv("NC_USER")
|
||||||
NC_PASS = os.getenv("NC_PASS")
|
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_EXTRACT_PATH = os.getenv("TEMP_EXTRACT_PATH")
|
||||||
TEMP_BACKUP_PATH = os.getenv("TEMP_BACKUP_PATH")
|
TEMP_BACKUP_PATH = os.getenv("TEMP_BACKUP_PATH")
|
||||||
PLUGIN_CONFIG_PATH = os.getenv("PLUGIN_CONFIGS_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",
|
"-r",
|
||||||
"--remote",
|
"--remote",
|
||||||
required=True,
|
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()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
@ -106,12 +107,16 @@ def backup_and_restore(
|
||||||
# Create backup of FFXIV config directory if it exists and has content
|
# 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):
|
if os.path.exists(restore_config_path) and os.listdir(restore_config_path):
|
||||||
print(
|
print(
|
||||||
"Creating safety backup of " + "ffxiv"
|
"Creating safety backup of "
|
||||||
if is_ffxiv_config
|
+ f"{'ffxiv' if is_ffxiv_config else 'plugins'} configs at:"
|
||||||
else "plugins" + f" configs at:\n -> {backup_config_path}/game"
|
+ f"-> {backup_config_path}\\{'game' if is_ffxiv_config else 'plugins'}"
|
||||||
)
|
)
|
||||||
try:
|
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)
|
shutil.rmtree(restore_config_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed during ffxiv config backup creation phase: {e}")
|
print(f"Failed during ffxiv config backup creation phase: {e}")
|
||||||
|
|
@ -121,7 +126,7 @@ def backup_and_restore(
|
||||||
try:
|
try:
|
||||||
print("Attempting to restore ffxiv config backups")
|
print("Attempting to restore ffxiv config backups")
|
||||||
shutil.copytree(
|
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,
|
restore_config_path,
|
||||||
dirs_exist_ok=True,
|
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