@@ -1,20 +1,40 @@ |
| 1 | 1 | import os |
| 2 | 2 | import sys |
| 3 | | -import threading |
| 4 | 3 | |
| 5 | 4 | import paramiko |
| 6 | 5 | import qtawesome as qta |
| 7 | 6 | from dotenv import load_dotenv |
| 8 | 7 | |
| 9 | 8 | from PyQt6.QtCore import Qt, QSize |
| 9 | +from PyQt6.QtCore import pyqtSignal, QRunnable, QThreadPool |
| 10 | 10 | from PyQt6.QtWidgets import ( |
| 11 | 11 | QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, |
| 12 | 12 | QLineEdit, QToolButton, QPushButton, QListWidget, QFileDialog, QMessageBox, QFormLayout, |
| 13 | 13 | ) |
| 14 | | - |
| 15 | 14 | from .sftp_backend import SFTPBackend |
| 16 | 15 | |
| 17 | 16 | |
| 17 | +class UploadRunnable(QRunnable): |
| 18 | + """ |
| 19 | + QRunnable for performing SFTP upload in a thread pool. |
| 20 | + Emits finish or error signals on completion. |
| 21 | + """ |
| 22 | + def __init__(self, backend, local_path, remote_path, finish_signal, error_signal): |
| 23 | + super().__init__() |
| 24 | + self.backend = backend |
| 25 | + self.local_path = local_path |
| 26 | + self.remote_path = remote_path |
| 27 | + self.finish_signal = finish_signal |
| 28 | + self.error_signal = error_signal |
| 29 | + |
| 30 | + def run(self): |
| 31 | + try: |
| 32 | + self.backend.upload(self.local_path, self.remote_path) |
| 33 | + self.finish_signal.emit() |
| 34 | + except Exception as e: |
| 35 | + self.error_signal.emit(str(e)) |
| 36 | + |
| 37 | + |
| 18 | 38 | # fetch default host/port from .env |
| 19 | 39 | load_dotenv() |
| 20 | 40 | DEFAULT_HOST = os.getenv('WULFTP_HOST', '') |
@@ -22,12 +42,21 @@ DEFAULT_PORT = int(os.getenv('WULFTP_PORT', '22')) |
| 22 | 42 | |
| 23 | 43 | |
| 24 | 44 | class WulFTPClient(QMainWindow): |
| 45 | + # signals for thread-safe UI updates |
| 46 | + remote_refreshed = pyqtSignal() |
| 47 | + upload_error = pyqtSignal(str) |
| 48 | + |
| 25 | 49 | def __init__(self): |
| 26 | 50 | super().__init__() |
| 27 | 51 | self.backend = SFTPBackend() |
| 28 | 52 | self.sftp = None |
| 29 | 53 | self.init_ui() |
| 30 | 54 | |
| 55 | + # setup thread pool and connect signals |
| 56 | + self.threadpool = QThreadPool() |
| 57 | + self.remote_refreshed.connect(self.refresh_remote) |
| 58 | + self.upload_error.connect(self.show_error) |
| 59 | + |
| 31 | 60 | def init_ui(self): |
| 32 | 61 | """build and arrange ui elements.""" |
| 33 | 62 | self.setWindowTitle('wulFTP POC') |
@@ -152,16 +181,15 @@ class WulFTPClient(QMainWindow): |
| 152 | 181 | local_path = url.toLocalFile() |
| 153 | 182 | filename = os.path.basename(local_path) |
| 154 | 183 | remote_file = os.path.join(self.remote_path, filename) |
| 155 | | - |
| 156 | | - def task(lp=local_path, rf=remote_file): |
| 157 | | - try: |
| 158 | | - self.backend.upload(lp, rf) |
| 159 | | - self.refresh_remote() |
| 160 | | - except Exception as e: |
| 161 | | - self.show_error(str(e)) |
| 162 | | - |
| 163 | | - threading.Thread(target=task, daemon=True).start() |
| 164 | | - |
| 184 | + # schedule upload in thread pool |
| 185 | + runnable = UploadRunnable( |
| 186 | + self.backend, |
| 187 | + local_path, |
| 188 | + remote_file, |
| 189 | + self.remote_refreshed, |
| 190 | + self.upload_error |
| 191 | + ) |
| 192 | + self.threadpool.start(runnable) |
| 165 | 193 | event.acceptProposedAction() |
| 166 | 194 | |
| 167 | 195 | def browse_key(self): |
@@ -210,21 +238,21 @@ class WulFTPClient(QMainWindow): |
| 210 | 238 | |
| 211 | 239 | def browse_and_upload(self): |
| 212 | 240 | """open file dialog and upload the selected file to remote.""" |
| 213 | | - |
| 214 | 241 | path, _ = QFileDialog.getOpenFileName(self, 'select file to upload') |
| 215 | 242 | if not path: |
| 216 | 243 | return |
| 217 | 244 | filename = os.path.basename(path) |
| 218 | 245 | remote_file = os.path.join(self.remote_path, filename) |
| 219 | 246 | |
| 220 | | - def task(): |
| 221 | | - try: |
| 222 | | - self.backend.upload(path, remote_file) |
| 223 | | - self.refresh_remote() |
| 224 | | - except Exception as e: |
| 225 | | - self.show_error(str(e)) |
| 226 | | - |
| 227 | | - threading.Thread(target=task, daemon=True).start() |
| 247 | + # schedule file upload in thread pool |
| 248 | + runnable = UploadRunnable( |
| 249 | + self.backend, |
| 250 | + path, |
| 251 | + remote_file, |
| 252 | + self.remote_refreshed, |
| 253 | + self.upload_error |
| 254 | + ) |
| 255 | + self.threadpool.start(runnable) |
| 228 | 256 | |
| 229 | 257 | |
| 230 | 258 | def main(): |