Python网络编程 03 实验:FTP详解

文章目录

一、小实验FTP程序需求

(1)用户加密认证

(2)允许同时多用户登录

(3)每个用户有自己的家目录,且只能访问自己的家目录

(4)对用户进行磁盘配额,每个用户的可用空间不同

(5)允许用户在FTP server上随意切换目录

(6)允许用户查看当前目录下文件

(7)允许上传和下载文件,保证文件一致性

(8)文件传输过程中显示进度条

(9)附加功能:支持文件的断点续传

二、项目文件架构

三、服务端

1、conf/settings.py

python 复制代码
import os
BASE_DIE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

IP = "127.0.0.1"
PORT = 8000

ACCOUNT_PATH = os.path.join(BASE_DIE, "conf", "accounts.cfg")

2、conf/accounts.cgf

python 复制代码
[DEFAULT]

[LuMX]
Password = 123
Quotation = 100

[root]
Password = root
Quotation = 100

3、conf/STATUS_CODE.py

python 复制代码
STATUS_CODE  = {
    250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
    251 : "Invalid cmd ",
    252 : "Invalid auth data",
    253 : "Wrong username or password",
    254 : "Passed authentication",
    255 : "Filename doesn't provided",
    256 : "File doesn't exist on server",
    257 : "ready to send file",
    258 : "md5 verification",

    800 : "the file exist,but not enough ,is continue? ",
    801 : "the file exist !",
    802 : " ready to receive datas",

    900 : "md5 valdate success"
}

4、启动文件 bin/ftp_server.py

python 复制代码
import os, sys

# 获取启动模块所在目录的父级目录路径,
PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 将该目录路径添加到导入模块时要搜索的目录列表
sys.path.append(PATH)

from core import main


if __name__ == "__main__":
    main.ArgvHandler()	# 启动主函数

5、core/main.py

python 复制代码
import optparse
import socketserver
from conf.settings import *
from core import server

class ArgvHandler:
    def __init__(self):
        self.op = optparse.OptionParser()
        options, args = self.op.parse_args()

        self.verify_args(args)

    def verify_args(self,args):
        cmd = args[0]

        if hasattr(self, cmd):
            # 方便扩展,例如可以在终端执行 python ftp_server.py start
            func = getattr(self,cmd)
            func()

    def start(self):
        print("the server is working...")
        s = socketserver.ThreadingTCPServer((IP, PORT), server.ServerHandler)
        s.serve_forever()

6、core/server.py

python 复制代码
import json,configparser
import os,hashlib

from conf.STATUS_CODE import *
from conf import settings
import socketserver
class ServerHandler(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            data = self.request.recv(1024).strip()
            if not data:
                print("the connect is breaked")
                break
            data = json.loads(data.decode("utf-8"))

            if data.get("action"):  # 字典data存在key为action,且不为空,执行下列代码
                func = getattr(self, data.get("action"))
                func(**data)
            else:
                print("Invalid cmd")

    def send_response(self, state_code):
        response = {"status_code": state_code}
        self.request.sendall(json.dumps(response).encode("utf-8"))

    def auth(self,**data):
        username = data["username"]
        password = data["password"]
        user = self.authenticate(username, password)

        if user:
            self.send_response(254)
        else:
            self.send_response(253)

    def authenticate(self,user,pwd):
        cfg = configparser.ConfigParser()
        cfg.read(settings.ACCOUNT_PATH)

        if user in cfg.sections():
            if cfg[user]["Password"]==pwd:
                self.user = user
                self.mainPath = os.path.join(settings.BASE_DIR,"home",self.user)
                print("passed authentication")
                return user

    def put(self,**data):
        print("data",data)
        file_name = data.get("file_name")
        file_size = data.get("file_size")
        target_path = data.get("target_path")

        abs_path = os.path.join(self.mainPath,target_path,file_name)

        has_received=0

        if os.path.exists(abs_path):
            file_has_size=os.stat(abs_path).st_size
            if file_has_size < file_size:
                # 断点续传
                self.request.sendall("800".encode("utf-8"))
                choice = self.request.recv(1024).decode("utf-8")
                if choice=="Y":
                    self.request.sendall(str(file_has_size).encode("utf-8"))
                    has_received += file_has_size
                    f=open(abs_path,"ab")
                else:
                    f=open(abs_path,"wb")
            else:
                # 文件存在,且完整
                self.request.sendall("801".encode("utf-8"))
                return
        else:
            # 文件不存在
            self.request.sendall("802".encode("utf-8"))
            f = open(abs_path,"wb")

        md5_obj = hashlib.md5()
        while has_received < file_size:
            md5_data = self.request.recv(32)
            f_data = self.request.recv(992)

            if self.md5_check(md5_obj,md5_data,f_data):
                self.send_response(900)
                f.write(f_data)
                has_received += len(f_data)
            else:
                print("md5 check error")
                self.send_response(901)
                break

        f.close()

    def md5_check(self,md5_obj,md5_data,f_data):
        md5_obj.update(f_data)

        if md5_obj.hexdigest().encode("utf-8") == md5_data:
            return True

    def ls(self, **data):
        file_list = os.listdir(self.mainPath)
        file_str="\n".join(file_list)

        if not len(file_list):
            file_str="<empty dir>"
        self.request.sendall(file_str.encode("utf-8"))

    def cd(self,**data):
        dirname=data.get("dirname")

        if dirname == "..":
            self.mainPath=os.path.dirname(self.mainPath)
        else:
            self.mainPath=os.path.join(self.mainPath,dirname)
        self.request.sendall(self.mainPath.encode("utf-8"))

    def mkdir(self,**data):
        dirname=data.get("dirname")
        path=os.path.join(self.mainPath,dirname)

        if not os.path.exists(path):
            if "/" in dirname:
                os.makedirs(path)
            else:
                os.mkdir(path)
            self.request.sendall("create success".encode("utf-8"))
        else:
            self.request.sendall("dirname exist".encode("utf-8"))

四、客户端

1、conf/STATUS_CODE.py

python 复制代码
STATUS_CODE  = {
    250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
    251 : "Invalid cmd ",
    252 : "Invalid auth data",
    253 : "Wrong username or password",
    254 : "Passed authentication",
    255 : "Filename doesn't provided",
    256 : "File doesn't exist on server",
    257 : "ready to send file",
    258 : "md5 verification",

    800 : "the file exist,but not enough ,is continue? ",
    801 : "the file exist !",
    802 : " ready to receive datas",

    900 : "md5 valdate success"
}

2、bin/ftp_client.py

python 复制代码
import os, sys, hashlib
import optparse,socket,json

PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PATH)

from conf.STATUS_CODE import STATUS_CODE

class ClientHandler():
    def __init__(self):
        self.op = optparse.OptionParser()
        self.op.add_option("-s", "--server", dest="server")
        self.op.add_option("-P", "--port", dest="port")
        self.op.add_option("-u", "--username", dest="username")
        self.op.add_option("-p", "--password", dest="password")

        self.options, self.args = self.op.parse_args()
        self.verify_args(self.options)
        self.make_connection()
        self.mainPath=os.path.dirname(os.path.abspath(__file__))

    def verify_args(self,options):
        '''
        验证端口号是否合法
        '''
        port = options.port

        if 0 <= int(port) <= 65535:
            return True
        else:
            exit("the port is not in 0-65535")

    def make_connection(self):
        '''
        处理链接
        '''
        self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        self.sock.connect((self.options.server, int(self.options.port)))

    def interactive(self):
        '''
        处理通讯
        '''
        print("begin to interactive...")

        if self.authenticate():
            while True:
                cmd_info = input("[%s]" %self.current_dir).strip()

                cmd_list = cmd_info.split()
                if hasattr(self,cmd_list[0]):
                    func=getattr(self,cmd_list[0])
                    func(*cmd_list)

    def put(self,*cmd_list):
        action,local_path,target_path=cmd_list
        local_path=os.path.join(self.mainPath,local_path)

        file_name=os.path.basename(local_path)
        file_size=os.stat(local_path).st_size

        data = {
            "action": "put",
            "file_name": file_name,
            "file_size": file_size,
            "target_path": target_path
        }

        self.sock.send(json.dumps(data).encode("utf-8"))
        is_exist=self.sock.recv(1024).decode("utf-8")

        has_sent=0

        if is_exist=="800":
            # 文件不完整
            choice=input("the file exist,but not enough,do continue?[Y/N]").strip()
            if choice.upper()=="Y":
                self.sock.sendall("Y".encode("utf-8"))
                continue_position=self.sock.recv(1024).decode("utf-8")
                has_sent += int(continue_position)
            else:
                self.sock.sendall("N".encode("utf-8"))
        elif is_exist=="801":
            # 文件完全存在
            print("the file exist")
            return

        f = open(local_path,"rb")
        f.seek(has_sent)
        md5_obj = hashlib.md5()
        while has_sent < file_size:
            f_data = f.read(992)

            if self.md5_check(md5_obj,f_data):
                has_sent += len(f_data)
                self.show_progress(has_sent,file_size)
            else:
                print("md5 check is error!")
                break

        f.close()

        if has_sent == file_size:
            print("\n",end="")
            print("put success!")

    def md5_check(self,md5_obj,f_data):
        md5_obj.update(f_data)
        md5_data = md5_obj.hexdigest().encode("utf-8")
        data = md5_data + f_data
        self.sock.sendall(data)
        response = self.response()

        if response["status_code"] == 900:
            return True

    def show_progress(self,has,total):
        '''
        显示进度条
        '''
        rate=int(float(has)/float(total)*10000)/10000
        rate_num=int(rate*100)
        sys.stdout.write("{:.2%} {}\r".format(rate,rate_num*"#"))
        # \r 表示光标移动到行首

    def ls(self,*cmd_list):
        data={
            "action": "ls"
        }
        self.sock.sendall(json.dumps(data).encode("utf-8"))
        data=self.sock.recv(1024).decode("utf-8")
        print(data)

    def cd(self,*cmd_list):
        data={
            "action": "cd",
            "dirname": cmd_list[1]
        }
        self.sock.sendall(json.dumps(data).encode("utf-8"))

        data = self.sock.recv(1024).decode("utf-8")
        print(os.path.basename(data))
        self.current_dir=os.path.basename(data)

    def mkdir(self,*cmd_list):
        data={
            "action": "mkdir",
            "dirname": cmd_list[1]
        }
        self.sock.sendall(json.dumps(data).encode("utf-8"))
        data = self.sock.recv(1024).decode("utf-8")
        print(data)

    def authenticate(self):
        if self.options.username is None or self.options.password is None:
            username = input("username:")
            password = input("password:")
            return self.get_auth_result(username,password)

        return self.get_auth_result(self.options.username, self.options.password)

    def response(self):
        data = self.sock.recv(1024).decode("utf-8")
        data = json.loads(data)

        return data
    def get_auth_result(self,user,pwd):
        data = {
            "action":"auth",
            "username":user,
            "password":pwd
        }

        self.sock.send(json.dumps(data).encode("utf-8"))
        response=self.response()
        print("response:",response["status_code"])
        if response["status_code"]==254:
            self.user = user
            self.current_dir=user
            print(STATUS_CODE[254])
            return True
        else:
            print(STATUS_CODE[response["status_code"]])

ch = ClientHandler()
ch.interactive()

五、在终端操作示例

服务端

客户端

相关推荐
数据智能老司机11 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机12 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机12 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机12 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i12 小时前
drf初步梳理
python·django
每日AI新事件12 小时前
python的异步函数
python
这里有鱼汤13 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
databook1 天前
Manim实现脉冲闪烁特效
后端·python·动效
程序设计实验室1 天前
2025年了,在 Django 之外,Python Web 框架还能怎么选?
python
倔强青铜三1 天前
苦练Python第46天:文件写入与上下文管理器
人工智能·python·面试