292 lines
10 KiB
Python
292 lines
10 KiB
Python
import sys
|
|
import os
|
|
import socket
|
|
import configparser
|
|
import logging
|
|
from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, QPushButton,
|
|
QVBoxLayout, QHBoxLayout, QLineEdit, QTextEdit,
|
|
QFormLayout, QSpinBox, QFileDialog, QSystemTrayIcon, QMenu, QAction, QCheckBox)
|
|
from PyQt5.QtCore import QThread, pyqtSignal, Qt
|
|
from PyQt5.QtGui import QIcon
|
|
from pyftpdlib.authorizers import DummyAuthorizer
|
|
from pyftpdlib.handlers import FTPHandler
|
|
from pyftpdlib.servers import FTPServer
|
|
# requirements:
|
|
# python.exe -m pip install pyftpdlib PyQt5
|
|
# compiling and dependancies:
|
|
# python.exe -m pip install pyinstaller Pillow
|
|
# pyinstaller --onefile --windowed --add-data "ftp.png;." --icon=ftp.png ftp_gui.py
|
|
|
|
# this file will be created where the code is run from or in folder where .exe is
|
|
# it gives options to save settings
|
|
CONFIG_FILE = "ftp-config.cfg"
|
|
|
|
class FTPServerThread(QThread):
|
|
log_signal = pyqtSignal(str)
|
|
|
|
def __init__(self, port, username, password, ftp_directory, log_file, parent=None):
|
|
super(FTPServerThread, self).__init__(parent)
|
|
self.port = port
|
|
self.username = username
|
|
self.password = password
|
|
self.ftp_directory = ftp_directory
|
|
self.log_file = log_file
|
|
self.server = None
|
|
self.running = False
|
|
self.setup_logger()
|
|
|
|
def setup_logger(self):
|
|
"""Setup logger to log both to file and emit to GUI."""
|
|
self.logger = logging.getLogger("FTPServer")
|
|
self.logger.setLevel(logging.INFO)
|
|
|
|
# Create file handler
|
|
file_handler = logging.FileHandler(self.log_file)
|
|
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
|
|
self.logger.addHandler(file_handler)
|
|
|
|
def run(self):
|
|
if not os.path.exists(self.ftp_directory):
|
|
os.makedirs(self.ftp_directory)
|
|
|
|
# Set up authorizer with user authentication
|
|
authorizer = DummyAuthorizer()
|
|
authorizer.add_user(self.username, self.password, self.ftp_directory, perm="elradfmw")
|
|
|
|
# Set up FTP handler
|
|
handler = FTPHandler
|
|
handler.authorizer = authorizer
|
|
|
|
# Override handler log method to connect to the log signal and file logger
|
|
handler.log = self.log
|
|
|
|
# Create FTP server
|
|
self.server = FTPServer(("0.0.0.0", self.port), handler)
|
|
self.running = True
|
|
self.server.serve_forever()
|
|
|
|
def log(self, message, logfun=None):
|
|
"""Custom log method to emit logs to the GUI and log to the file."""
|
|
self.log_signal.emit(message)
|
|
self.logger.info(message)
|
|
|
|
def stop(self):
|
|
if self.server:
|
|
self.server.close_all()
|
|
self.running = False
|
|
|
|
|
|
class FTPGuiApp(QWidget):
|
|
def __init__(self, config):
|
|
super().__init__()
|
|
self.setWindowTitle("Simple FTP Server")
|
|
self.server_thread = None
|
|
self.ftp_directory = config['FTP']['ftp_directory']
|
|
self.run_as_daemon = config.getboolean('FTP', 'run_as_daemon')
|
|
self.log_file = config['FTP'].get('log_file', 'ftp_log.txt')
|
|
self.username = config['FTP']['username']
|
|
self.password = config['FTP']['password']
|
|
self.port = config.getint('FTP', 'port')
|
|
|
|
self.tray_icon = None
|
|
self.init_ui()
|
|
|
|
# Start in daemon mode if configured
|
|
if self.run_as_daemon:
|
|
self.start_server()
|
|
self.hide_to_tray()
|
|
|
|
def init_ui(self):
|
|
layout = QVBoxLayout()
|
|
|
|
# Form for user, password, port, and directory
|
|
form_layout = QFormLayout()
|
|
|
|
self.username_input = QLineEdit()
|
|
self.username_input.setText(self.username)
|
|
self.password_input = QLineEdit()
|
|
self.password_input.setEchoMode(QLineEdit.Password)
|
|
self.password_input.setText(self.password)
|
|
self.port_input = QSpinBox()
|
|
self.port_input.setRange(1, 65535)
|
|
self.port_input.setValue(self.port)
|
|
|
|
form_layout.addRow("Username:", self.username_input)
|
|
form_layout.addRow("Password:", self.password_input)
|
|
form_layout.addRow("Port:", self.port_input)
|
|
|
|
# FTP directory picker
|
|
self.directory_input = QLineEdit()
|
|
self.directory_input.setText(self.ftp_directory)
|
|
self.directory_button = QPushButton("Browse FTP Folder")
|
|
self.directory_button.clicked.connect(self.select_directory)
|
|
|
|
directory_layout = QHBoxLayout()
|
|
directory_layout.addWidget(self.directory_input)
|
|
directory_layout.addWidget(self.directory_button)
|
|
|
|
layout.addLayout(form_layout)
|
|
layout.addLayout(directory_layout)
|
|
|
|
# Log file path picker
|
|
self.log_file_input = QLineEdit()
|
|
self.log_file_input.setText(self.log_file)
|
|
self.log_file_button = QPushButton("Browse Log File")
|
|
self.log_file_button.clicked.connect(self.select_log_file)
|
|
|
|
log_layout = QHBoxLayout()
|
|
log_layout.addWidget(self.log_file_input)
|
|
log_layout.addWidget(self.log_file_button)
|
|
|
|
layout.addLayout(log_layout)
|
|
|
|
# Daemon mode checkbox
|
|
self.daemon_checkbox = QCheckBox("Run as Daemon (Minimize to Tray)")
|
|
self.daemon_checkbox.setChecked(self.run_as_daemon)
|
|
layout.addWidget(self.daemon_checkbox)
|
|
|
|
# Button to start and stop the server
|
|
self.start_button = QPushButton("Start FTP Server")
|
|
self.start_button.clicked.connect(self.start_server)
|
|
self.stop_button = QPushButton("Stop FTP Server")
|
|
self.stop_button.clicked.connect(self.stop_server)
|
|
self.stop_button.setEnabled(False)
|
|
|
|
# Layout for buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addWidget(self.start_button)
|
|
button_layout.addWidget(self.stop_button)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
# Log view to show server activity
|
|
self.log_view = QTextEdit()
|
|
self.log_view.setReadOnly(True)
|
|
layout.addWidget(self.log_view)
|
|
|
|
# Label to show listening IP addresses
|
|
self.ip_label = QLabel("Listening on:")
|
|
layout.addWidget(self.ip_label)
|
|
|
|
self.setLayout(layout)
|
|
|
|
# Set up system tray for daemon mode
|
|
self.setup_tray_icon()
|
|
|
|
def setup_tray_icon(self):
|
|
icon_path = os.path.join(os.path.dirname(__file__), 'ftp.png')
|
|
self.tray_icon = QSystemTrayIcon(self)
|
|
self.tray_icon.setIcon(QIcon(icon_path))
|
|
|
|
tray_menu = QMenu()
|
|
|
|
restore_action = QAction("Restore", self)
|
|
restore_action.triggered.connect(self.show)
|
|
tray_menu.addAction(restore_action)
|
|
|
|
quit_action = QAction("Quit", self)
|
|
quit_action.triggered.connect(self.close)
|
|
tray_menu.addAction(quit_action)
|
|
|
|
self.tray_icon.setContextMenu(tray_menu)
|
|
self.tray_icon.activated.connect(self.on_tray_icon_activated)
|
|
self.tray_icon.show()
|
|
|
|
def on_tray_icon_activated(self, reason):
|
|
if reason == QSystemTrayIcon.Trigger:
|
|
self.show()
|
|
|
|
def hide_to_tray(self):
|
|
self.hide()
|
|
self.tray_icon.showMessage("FTP Server", "Running in background", QSystemTrayIcon.Information, 3000)
|
|
|
|
def select_directory(self):
|
|
folder = QFileDialog.getExistingDirectory(self, "Select FTP Folder", self.ftp_directory)
|
|
if folder:
|
|
self.ftp_directory = folder
|
|
self.directory_input.setText(folder)
|
|
|
|
def select_log_file(self):
|
|
log_file, _ = QFileDialog.getSaveFileName(self, "Select Log File", self.log_file, "Log Files (*.txt)")
|
|
if log_file:
|
|
self.log_file = log_file
|
|
self.log_file_input.setText(log_file)
|
|
|
|
def start_server(self):
|
|
username = self.username_input.text()
|
|
password = self.password_input.text()
|
|
port = self.port_input.value()
|
|
ftp_directory = self.directory_input.text()
|
|
log_file = self.log_file_input.text()
|
|
|
|
self.server_thread = FTPServerThread(port, username, password, ftp_directory, log_file)
|
|
self.server_thread.log_signal.connect(self.log_message)
|
|
self.server_thread.start()
|
|
|
|
self.update_ip_addresses()
|
|
self.start_button.setEnabled(False)
|
|
self.stop_button.setEnabled(True)
|
|
|
|
if self.daemon_checkbox.isChecked():
|
|
self.hide_to_tray()
|
|
|
|
def stop_server(self):
|
|
if self.server_thread:
|
|
self.server_thread.stop()
|
|
self.server_thread.wait()
|
|
|
|
self.start_button.setEnabled(True)
|
|
self.stop_button.setEnabled(False)
|
|
|
|
def log_message(self, message):
|
|
self.log_view.append(message)
|
|
|
|
def update_ip_addresses(self):
|
|
ip_addresses = self.get_ip_addresses()
|
|
self.ip_label.setText(f"Listening on: {', '.join(ip_addresses)}")
|
|
|
|
def get_ip_addresses(self):
|
|
ip_list = []
|
|
for ip in socket.gethostbyname_ex(socket.gethostname())[2]:
|
|
if not ip.startswith("127."):
|
|
ip_list.append(ip)
|
|
return ip_list
|
|
|
|
def closeEvent(self, event):
|
|
if self.daemon_checkbox.isChecked():
|
|
event.ignore()
|
|
self.hide_to_tray()
|
|
else:
|
|
event.accept()
|
|
|
|
|
|
def load_config():
|
|
config = configparser.ConfigParser()
|
|
if not os.path.exists(CONFIG_FILE):
|
|
# Default settings for first run
|
|
config['FTP'] = {
|
|
'username': 'user',
|
|
'password': '123',
|
|
'port': '21',
|
|
'ftp_directory': r'C:\Temp\FTP',
|
|
'run_as_daemon': '0',
|
|
'log_file': 'ftp_log.txt'
|
|
}
|
|
with open(CONFIG_FILE, 'w') as configfile:
|
|
config.write(configfile)
|
|
else:
|
|
config.read(CONFIG_FILE)
|
|
return config
|
|
|
|
|
|
if __name__ == '__main__':
|
|
config = load_config()
|
|
|
|
app = QApplication(sys.argv)
|
|
gui = FTPGuiApp(config)
|
|
|
|
# Show the window if not in daemon mode
|
|
if not config.getboolean('FTP', 'run_as_daemon'):
|
|
gui.show()
|
|
|
|
sys.exit(app.exec_()) |