本文是之前一篇博文【Tkinter + paramiko + threading 实现Windows与Linux文件同步】的改进,将其中的Tkinter界面更换成了PtQt界面(因为PyQt界面自带托盘运行类:QSystemTrayIcon)。
本人手里面有一个安装 FileRun 的 Linux 服务器,其中有一个文件夹存放的内容与本人电脑中一个文件夹相同,每次有文件增添时都要手动上传,FileRun 提供的软件只能在 https 域名上使用,而我的是 http(购买域名的话需要额外花钱且没必要) ,所以闲着没事自己写了个同步的软件。
软件自动记录上次配置信息(写入注册表),可以托盘运行(上传使用的是额外线程,不会阻塞),支持开机自启(使用 os.system 操作 SchTasks)。
完整代码已上传 GitHub ,链接如下
File Sync Su
本文包含了很多模块的相关代码,想要使用某一块的内容直接跳转复制即可,当然点个赞更好了!
1. 主要使用的库
PyQt
paramiko(需要安装pycryptodome)
winreg
thread
pillow
2. 文件上传部分(paramiko)
scp = paramiko.Transport((host_ip, host_port))
scp.connect(username=host_username, password=host_password)
sftp = paramiko.SFTPClient.from_transport(scp)
self.recursiveUpload(sftp, local_path, remote_path)
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host_ip, host_port, host_username, host_password)
client.exec_command('chmod 777 -R ' + remote_path)
client.close()
except:
pass
scp.close()
这部分代码参考了此博客
Python通过paramiko复制远程文件及文件目录到本地——作者:森林番茄
上面代码中,前三行是创建了一个sftp对象,使用该对象进行文件操作。
recursiveUpload 函数用于递归上传,主要作用是将子目录和其中文件也上传到服务器。 接下来的代码是赋予上传的文件 777 权限,因为不给权限的话在 FileRun 中不能操作,try 部分代码如不需要可删除。
recursiveUpload 函数如下
def recursiveUpload(self, sftp, localPath, remotePath):
for root, paths, files in walk(localPath):
remote_files = sftp.listdir(remotePath)
for file in files:
if file not in remote_files:
print('正在上传', remotePath + '/' + file)
sftp.put(join(root, file), remotePath + '/' + file)
for path in paths:
if path not in remote_files:
print('创建文件夹', remotePath + '/' + path)
sftp.mkdir(remotePath + '/' + path)
self.recursiveUpload(sftp, join(localPath, path), remotePath + '/' + path)
break
3. GUI 部分(PyQt)
界面如图
GUI 部分代码如下,通过QtDesigner制作
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(650, 280)
MainWindow.setMinimumSize(QtCore.QSize(650, 280))
MainWindow.setMaximumSize(QtCore.QSize(650, 280))
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
spacerItem = QtWidgets.QSpacerItem(20, 13, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 0, 1, 1, 1)
spacerItem1 = QtWidgets.QSpacerItem(33, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.gridLayout.addItem(spacerItem1, 1, 0, 1, 1)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.label = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.label.setFont(font)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.lineEdit_ip = QtWidgets.QLineEdit(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.lineEdit_ip.setFont(font)
self.lineEdit_ip.setObjectName("lineEdit_ip")
self.horizontalLayout.addWidget(self.lineEdit_ip)
self.horizontalLayout_7.addLayout(self.horizontalLayout)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_7.addItem(spacerItem2)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.label_2 = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.label_2.setFont(font)
self.label_2.setObjectName("label_2")
self.horizontalLayout_2.addWidget(self.label_2)
self.lineEdit_port = QtWidgets.QLineEdit(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.lineEdit_port.setFont(font)
self.lineEdit_port.setObjectName("lineEdit_port")
self.horizontalLayout_2.addWidget(self.lineEdit_port)
self.horizontalLayout_7.addLayout(self.horizontalLayout_2)
self.verticalLayout.addLayout(self.horizontalLayout_7)
spacerItem3 = QtWidgets.QSpacerItem(14, 13, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem3)
self.horizontalLayout_8 = QtWidgets.QHBoxLayout()
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.label_4 = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.label_4.setFont(font)
self.label_4.setObjectName("label_4")
self.horizontalLayout_3.addWidget(self.label_4)
self.lineEdit_username = QtWidgets.QLineEdit(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.lineEdit_username.setFont(font)
self.lineEdit_username.setObjectName("lineEdit_username")
self.horizontalLayout_3.addWidget(self.lineEdit_username)
self.horizontalLayout_8.addLayout(self.horizontalLayout_3)
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_8.addItem(spacerItem4)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.label_3 = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.label_3.setFont(font)
self.label_3.setObjectName("label_3")
self.horizontalLayout_4.addWidget(self.label_3)
self.lineEdit_password = QtWidgets.QLineEdit(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.lineEdit_password.setFont(font)
self.lineEdit_password.setObjectName("lineEdit_password")
self.horizontalLayout_4.addWidget(self.lineEdit_password)
self.horizontalLayout_8.addLayout(self.horizontalLayout_4)
self.verticalLayout.addLayout(self.horizontalLayout_8)
spacerItem5 = QtWidgets.QSpacerItem(14, 13, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem5)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.label_5 = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.label_5.setFont(font)
self.label_5.setObjectName("label_5")
self.horizontalLayout_5.addWidget(self.label_5)
self.lineEdit_localpath = QtWidgets.QLineEdit(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.lineEdit_localpath.setFont(font)
self.lineEdit_localpath.setObjectName("lineEdit_localpath")
self.horizontalLayout_5.addWidget(self.lineEdit_localpath)
self.verticalLayout.addLayout(self.horizontalLayout_5)
spacerItem6 = QtWidgets.QSpacerItem(14, 13, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem6)
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.label_6 = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.label_6.setFont(font)
self.label_6.setObjectName("label_6")
self.horizontalLayout_6.addWidget(self.label_6)
self.lineEdit_remotepath = QtWidgets.QLineEdit(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.lineEdit_remotepath.setFont(font)
self.lineEdit_remotepath.setObjectName("lineEdit_remotepath")
self.horizontalLayout_6.addWidget(self.lineEdit_remotepath)
self.verticalLayout.addLayout(self.horizontalLayout_6)
spacerItem7 = QtWidgets.QSpacerItem(14, 13, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem7)
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
self.horizontalLayout_12.setObjectName("horizontalLayout_12")
self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_10.addItem(spacerItem8)
self.btn_start = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.btn_start.setFont(font)
self.btn_start.setObjectName("btn_start")
self.horizontalLayout_10.addWidget(self.btn_start)
self.horizontalLayout_12.addLayout(self.horizontalLayout_10)
self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
self.horizontalLayout_11.setObjectName("horizontalLayout_11")
spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_11.addItem(spacerItem9)
self.loading = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.loading.setFont(font)
self.loading.setObjectName("loading")
self.horizontalLayout_11.addWidget(self.loading)
spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_11.addItem(spacerItem10)
self.horizontalLayout_12.addLayout(self.horizontalLayout_11)
self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
self.horizontalLayout_9.setObjectName("horizontalLayout_9")
self.btn_stop = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(12)
self.btn_stop.setFont(font)
self.btn_stop.setObjectName("btn_stop")
self.horizontalLayout_9.addWidget(self.btn_stop)
spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_9.addItem(spacerItem11)
self.horizontalLayout_12.addLayout(self.horizontalLayout_9)
self.verticalLayout.addLayout(self.horizontalLayout_12)
self.gridLayout.addLayout(self.verticalLayout, 1, 1, 1, 1)
spacerItem12 = QtWidgets.QSpacerItem(33, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.gridLayout.addItem(spacerItem12, 1, 2, 1, 1)
spacerItem13 = QtWidgets.QSpacerItem(20, 13, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem13, 2, 1, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 650, 23))
self.menubar.setObjectName("menubar")
self.menu = QtWidgets.QMenu(self.menubar)
self.menu.setObjectName("menu")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionabc = QtWidgets.QAction(MainWindow)
self.actionabc.setObjectName("actionabc")
self.actionboot = QtWidgets.QAction(MainWindow)
self.actionboot.setObjectName("actionboot")
self.menu.addAction(self.actionboot)
self.menubar.addAction(self.menu.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "服务器同步软件"))
self.label.setText(_translate("MainWindow", "IP :"))
self.label_2.setText(_translate("MainWindow", "端 口:"))
self.label_4.setText(_translate("MainWindow", "用 户 名:"))
self.label_3.setText(_translate("MainWindow", "密 码:"))
self.label_5.setText(_translate("MainWindow", "本地路径:"))
self.label_6.setText(_translate("MainWindow", "远端路径:"))
self.btn_start.setText(_translate("MainWindow", "开始同步"))
self.loading.setText(_translate("MainWindow", " "))
self.btn_stop.setText(_translate("MainWindow", "停止同步"))
self.menu.setTitle(_translate("MainWindow", "设置"))
self.actionabc.setText(_translate("MainWindow", "boot"))
self.actionboot.setText(_translate("MainWindow", "开机自启"))
其中 btn_start 响应开始同步按钮,主要作用是启动上传文件线程,btn_stop 响应停止同步按钮,主要作用是强制停止上传文件线程。
btn_start连接的函数如下
def startSyncFunction(self):
self.showMessage('File Sync', '开始同步')
ip, port = self.lineEdit_ip.text(), self.lineEdit_port.text()
user, pwd = self.lineEdit_username.text(), self.lineEdit_password.text()
lPath, rPath = self.lineEdit_localpath.text(), self.lineEdit_remotepath.text()
pwd_input = pwd
try:
pwd = pwd.replace('*', '')
if pwd == '':
pwd = self.regDict['pwd']
except KeyError:
pwd = pwd_input
SetValueEx(self.key, 'ip', 0, REG_SZ, ip)
SetValueEx(self.key, 'port', 0, REG_SZ, port)
SetValueEx(self.key, 'user', 0, REG_SZ, user)
SetValueEx(self.key, 'pwd', 0, REG_SZ, pwd)
SetValueEx(self.key, 'lPath', 0, REG_SZ, lPath)
SetValueEx(self.key, 'rPath', 0, REG_SZ, rPath)
self.btn_start.setEnabled(False)
self.btn_stop.setEnabled(True)
self.lineEdit_ip.setEnabled(False)
self.lineEdit_port.setEnabled(False)
self.lineEdit_username.setEnabled(False)
self.lineEdit_password.clear()
self.lineEdit_password.setText('*' * len(pwd_input))
self.lineEdit_password.setEnabled(False)
self.lineEdit_localpath.setEnabled(False)
self.lineEdit_remotepath.setEnabled(False)
self.loading.setVisible(True)
self.gif.start()
self.T_Upload = Thread(target=self.UploadFile, args=(ip, int(port), user, pwd, lPath, rPath))
self.T_Upload.setDaemon(True)
self.T_Upload.start()
btn_stop 代码如下
def stopSyncFunction(self):
self.showMessage('File Sync', '停止同步')
stop_thread(self.T_Upload)
self.gif.stop()
self.loading.setVisible(False)
self.btn_stop.setEnabled(False)
self.btn_start.setEnabled(True)
self.lineEdit_ip.setEnabled(True)
self.lineEdit_port.setEnabled(True)
self.lineEdit_username.setEnabled(True)
self.lineEdit_password.setEnabled(True)
self.lineEdit_localpath.setEnabled(True)
self.lineEdit_remotepath.setEnabled(True)
其中 stop_thread 用于停止线程,代码如下
def _async_raise(tid, exctype):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
try:
_async_raise(thread.ident, SystemExit)
except ValueError:
pass
这部分代码参考了此博客
强行停止python子线程最佳方案——作者:熊彬彬
4. 配置信息写入注册表(winreg)
使用 winreg 读写注册表,每次点击开始同步时,向注册表中写入 ip、port、username、password 等信息,每次启动时自动读取注册表信息,如果信息完全,则直接开始同步。
写入代码如下
SetValueEx(self.key, 'ip', 0, REG_SZ, ip)
SetValueEx(self.key, 'port', 0, REG_SZ, port)
SetValueEx(self.key, 'user', 0, REG_SZ, user)
SetValueEx(self.key, 'pwd', 0, REG_SZ, pwd)
SetValueEx(self.key, 'lPath', 0, REG_SZ, lPath)
SetValueEx(self.key, 'rPath', 0, REG_SZ, rPath)
读取代码如下
self.key = CreateKey(HKEY_LOCAL_MACHINE, r'SOFTWARE\\服务器同步软件')
self.regDict = ReadReg(self.key)
其中 ReadReg 函数为读取 key 中所有的注册表项,并返回一个字典。代码如下
def ReadReg(key):
regDict = {}
try:
i = 0
while 1:
name, value, type = EnumValue(key, i)
regDict[name] = value
i += 1
except WindowsError:
pass
return regDict
5. 程序最小化至托盘(QSystemTrayIcon)
def initTrayIcon(self):
def open():
self.showNormal()
def quit():
QCoreApplication.quit()
def iconActivated(reason):
if reason in (QSystemTrayIcon.DoubleClick,):
open()
startAction = QAction("开始同步", self)
startAction.triggered.connect(self.startSyncFunction)
stopAction = QAction("停止同步", self)
stopAction.triggered.connect(self.stopSyncFunction)
openAction = QAction("打开", self)
openAction.setIcon(QIcon.fromTheme("media-record"))
openAction.triggered.connect(open)
quitAction = QAction("退出", self)
quitAction.setIcon(QIcon.fromTheme("application-exit"))
quitAction.triggered.connect(quit)
menu = QMenu(self)
menu.addAction(startAction)
menu.addAction(stopAction)
menu.addSeparator()
menu.addAction(openAction)
menu.addAction(quitAction)
self.trayIcon = QSystemTrayIcon(self)
self.trayIcon.setIcon(QIcon(self.ico))
self.trayIcon.setToolTip("服务器同步软件")
self.trayIcon.setContextMenu(menu)
self.trayIcon.messageClicked.connect(open)
self.trayIcon.activated.connect(iconActivated)
关闭到托盘和消息弹窗部分代码如下
def closeEvent(self, event):
if self.trayIcon.isVisible():
self.showMessage('File Sync', '程序已托盘运行')
def showMessage(self, title, content):
self.trayIcon.showMessage(title, content, QSystemTrayIcon.Information, 1000)
6. 自启动部分(SchTasks)
点击界面顶端设置中的开机自启动来进行设置,使用 os.system 执行 SchTasks 命令,代码如下
def AutoRun(self):
try:
exePath = realpath(sys.executable)
AutoRunCommand = r'echo y | SCHTASKS /CREATE /TN "FileSync\FileSync" /TR "{}" /SC ONLOGON /DELAY 0000:30 /RL HIGHEST'.format(exePath)
system(AutoRunCommand)
self.showMessage('开机自启动', '设置成功')
except:
self.showMessage('开机自启动', '设置失败,请手动创建任务计划')
如果自启动设置失败,可通过手动创建管理员权限(由于进行了注册表操作)的任务计划,具体方法参考以下链接
如何创建Windows计划任务
7. 生成exe文件(Pyinstaller)
使用 Pyinstaller 进行打包,生成单exe命令,由于代码中使用了图片等数据,为了将这些数据一起打包,首先生成 spec 文件,代码如下
pyi-makespec -F -w --uac-admin --icon img/loading.ico main.py -n 服务器同步软件.exe
然后将 spec 文件中的 data行 修改为
datas=[('img','img')]
img为目前要打包的其他数据所在目录,img为使用时生成临时文件所在目录。
最后生成exe,代码如下
pyinstaller 服务器同步软件.exe.spec
8. 写在最后
感谢文中所引用部分作者分享的代码,如果没有这些代码作为参考,想要完成这些功能并搭配协作将会很难完成,如果所使用的代码涉及到了侵权,请私信我告知。
|