Bootstrap

FTP编程实验——实现文件上传下载(基于Python3.7和PyQt5)

FTP编程实现文件上传下载(基于Python3.7和PyQt5)

FTP(File Transfer Protocol,文件传输协议) 是 TCP/IP 协议组中的协议之一。FTP协议包括两个组成部分,其一为FTP服务器,其二为FTP客户端。其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。在开发网站的时候,通常利用FTP协议把网页或程序传到Web服务器上。此外,由于FTP传输效率非常高,在网络上传输大的文件时,一般也采用该协议。

默认情况下FTP协议使用TCP端口中的 20和21这两个端口,其中20用于传输数据,21用于传输控制信息。但是,是否使用20作为传输数据的端口与FTP使用的传输模式有关,如果采用主动模式,那么数据传输端口就是20;如果采用被动模式,则具体最终使用哪个端口要服务器端和客户端协商决定。

——来自《百度百科》

老规矩,先上图:
在这里插入图片描述

一、实验目的

  • 了解FTP协议的用途,掌握FTP编程的基本方法,理解其工作流程
  • 巩固PyQt5的使用,扩展学习了新的组件ListView和打开文件选择框
  • 巩固多线程的用法

二、实验内容

编制 FTP 客户端程序,实现如下功能:登陆 FTP 服务器,显 示登录客户目录下的文件和目录名,能从该目录中(包括各级子目录)选择下载 服务器端的文件,也能向服务器上传文件。应用程序基于对话框,程序界面如下图:
在这里插入图片描述

三、实验步骤

实验步骤我主要讲代码,前面的界面设计和代码生成我的前一篇socket网络程序设计实验三文章中讲的比较详细了,不清楚的可以看一下或者百度吧。

(一)服务器端

首先我们得自己简单搞个服务器端出来,服务器端需要用到库:pyftpdlib
没有的需要先pip install一波:

pip install pyftpdlib

在这里插入图片描述
Ok,然后新建个ftp_server.py文件,敲:

from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer

# 获得实例
handler = FTPHandler
# 将用户名,密码,指定目录,权限 添加到里面
handler.authorizer.add_user("mike", "123456", "D:/Work/", perm="elradfmw")
# 开启服务器
server = FTPServer(("127.0.0.1", 21), handler)
server.serve_forever()

Ok,这样服务器就开始运行了,打开浏览器,输入:

ftp://127.0.0.1:21

看到下面这样的界面就说明服务器OK了
在这里插入图片描述

(二)客户端

【1】 界面设计

打开Qt Designer,新建一个Dialog Without Button,然后我们就自己开始设计,注意最大的文本框不是TextBrowser,而是ListView,因为我们需要将文件列表显示在其中,ListView刚好适合我们的需求.设计完后改名保存就好了。

【2】 生成布局代码

在pycharm中找到刚才保存的.ui文件,右键External Tools -> Py UIC,就自动生成了.py文件
在代码的开头加上:

import os
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *
from ftplib import FTP
import logging
import threading

在代码的最后加上:

if __name__ == '__main__':
    app = QApplication(sys.argv)
    MainWindow = QMainWindow()
    ui = Ui_FtpFile()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

至此,运行程序可以显示界面了

【3】 功能实现

首先在dialog类中初始化一些我们需要的变量:

    def __init__(self):
        self.slm = QStringListModel()   # listView的模型
        self.ftp = FTP()        # 实例化FTP
        self.select_file = ""   # listView中选择的文件名
        self.file_list = []     # 存放查询FTP返回的当前目录所有文件列表
1.连接并登录FTP,返回文件列表(查询按钮)

要注意用户名和密码要和我们的服务器端一致

    def connect_button(self):
        host = self.lineEdit.text()     # 获取IP地址框内容
        port = int(self.lineEdit_4.text())  # 获取端口号,注意要转换为int
        usr = self.lineEdit_2.text()        # 获取用户名
        pwd = self.lineEdit_3.text()        # 获取密码
        try:
            self.ftp.connect(host,port,timeout=10)      # 连接FTP
        except:
            logging.warning('network connect time out')     # 打印日志信息
        try:
            self.ftp.login(usr,pwd)     # 登录FTP
        except:
            logging.warning("username or password error")   # 打印日志信息


        self.file_list = self.ftp.nlst() # 查询当前目录的所有文件列表
        self.slm.setStringList(self.file_list)      # 将文件列表存入listView模型

再在retranslateUi下加上:

        self.pushButton.clicked.connect(self.connect_button)
2.单击listView获取选中的item
    def select_item(self, qModelIndex):
        self.select_file = self.file_list[qModelIndex.row()]
        # print(self.select_file)
        if "." in self.select_file:     # 如果是文件,则可下载
            self.pushButton_3.setEnabled(True)
        else:                           # 否则是文件夹,不能下载
            self.pushButton_3.setEnabled(False)

再在retranslateUi下加上:

        self.listView.setModel(self.slm)
        self.listView.clicked.connect(self.select_item)
3.双击listView进入选中的item文件夹
    def cd_button(self):
        if '.' in self.select_file: # 是文件则不能进入
            pass
        else:   # 是文件夹则可以进入
            self.ftp.cwd(self.select_file)
            self.file_list = self.ftp.nlst()  # 刷新当前目录的所有文件列表
            self.slm.setStringList(self.file_list)

再在retranslateUi下加上:

        self.listView.doubleClicked.connect(self.cd_button)
4.返回上一级目录(上一层按钮)
    def back_button(self):
        self.ftp.cwd("..")
        self.file_list = self.ftp.nlst()  # 刷新当前目录的所有文件列表
        self.slm.setStringList(self.file_list)

再在retranslateUi下加上:

        self.pushButton_1.clicked.connect(self.back_button)
5.上传文件(上传按钮)
    def upload_button(self):
        t = threading.Thread(target=self.t,args=())
        t.start()

    def t(self):
        file_path, filer_ = QFileDialog.getOpenFileName()
        print(file_path)
        file_name = os.path.split(file_path)[1]
        print(file_name)
        self.ftp.storbinary('stor '+file_name, open(file_path, 'rb'))

再在retranslateUi下加上:

        self.pushButton_2.clicked.connect(self.upload_button)
6.下载文件(下载按钮)
    def download_button(self):
        self.ftp.retrbinary('retr '+self.select_file, open(self.select_file, 'wb').write)
        print("download succeed")

再在retranslateUi下加上:

        self.pushButton_3.clicked.connect(self.download_button)
7.断开连接(退出按钮)
    def quit_button(self):
        self.ftp.quit()
        self.slm.setStringList([])

再在retranslateUi下加上:

        self.pushButton_4.clicked.connect(self.quit_button)

至此,我们预想的功能都已实现了,也在过程中接入了一些异常捕获来减少bug,但是还不够全面,下面来看看有什么需要改进的地方。

【4】 改进程序

现在程序一打开是这样的:
在这里插入图片描述
我们会发现如果这个时候点“上一层”或者“上传”“退出”,肯定是不行的,因为都还没有连接FTP服务器,所以我们应该先将这三个按钮disable一下
在retranslateUi下加入:

		self.pushButton_1.setEnabled(False)
        self.pushButton_2.setEnabled(False)
        self.pushButton_4.setEnabled(False)

在点击查询按钮连接后再将它们设置为True
在connect_button方法下加上:

		self.pushButton_1.setEnabled(True)
        self.pushButton_2.setEnabled(True)
        self.pushButton_4.setEnabled(True)

还有退出连接后,也要改回来
在quit_button方法下加上:

		self.pushButton_1.setEnabled(False)
        self.pushButton_2.setEnabled(False)
        self.pushButton_4.setEnabled(False)

能想到的别的Bug但是不影响程序使用,例如:

  • 连接后,再点查询(重复连接)是不会出错的,如果不在顶层目录就相当于返回顶层目录
  • 点击上传后没有选择文件夹关闭了,因为我是启用新线程执行这一操作,所以不影响主程序
  • 已经在顶层目录还点“上一层”按钮,也不会出错

服务器端也可以一对多服务的,可以增加多个用户组:

handler.authorizer.add_user("mike", "123456", "D:/Work/", perm="elradfmw")
handler.authorizer.add_user("LiePy", "654321", "D:/Work/", perm="elradfmw")
handler.authorizer.add_user("python", "abcdefg", "D:/Work/", perm="elradfmw")

也可以直接不设用户名密码限制只需要在服务器端中加入:

handler.authorizer.add_anonymous("D:/Work/")

当然,你可以在远程主机上传输文件,只要改一下ip地址即可,不只限于localhost噢

好吧,这篇文章就到这里

源代码已上传至我的github:https://github.com/LiePy/socket_test.git

pass~

;