initial commit
This commit is contained in:
292
ftp_gui.py
Normal file
292
ftp_gui.py
Normal file
@@ -0,0 +1,292 @@
|
||||
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_())
|
||||
39
ftp_gui.spec
Normal file
39
ftp_gui.spec
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['ftp_gui.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('ftp.png', '.')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='ftp_gui',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['ftp.png'],
|
||||
)
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
PyQt5
|
||||
pyftpdlib
|
||||
|
||||
# for windows compiling:
|
||||
pyinstaller
|
||||
Pillow
|
||||
Reference in New Issue
Block a user