【Python+Linux】python+paramiko实现Windows与Linux文件同步

本文是之前一篇博文【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:
    # linux给出chmod权限供filerun操作
    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制作

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


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:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        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:
            # EnumValue方法用来枚举键值,EnumKey用来枚举子键
            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. 写在最后

感谢文中所引用部分作者分享的代码,如果没有这些代码作为参考,想要完成这些功能并搭配协作将会很难完成,如果所使用的代码涉及到了侵权,请私信我告知。

猜你喜欢

转载自blog.csdn.net/weixin_42147967/article/details/127589305