python之写了3个周末及几个晚上的ftp终于完成了

本程序已上传githup,点我
作业:开发一个支持多用户在线的FTP程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server的随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
支持文件的断点续传

目录结构
ftp/
|
|-----bin/
| |-----ftpClient.py # 客户端的入口
| |-----ftpServer.py # 服务端的入口
|
|----conf/
| |-----setting.py # 一些配置信息
|
| -----core/
| |-----auth.py # 用户认证模块
| |-----socket_client.py # socket客户端主程序
| |-----socket_server.py # socket服务端主程序
| |-----upload.py # 传输数据模块
|
|------database/ # 已经存在的用户数据库文件
|-----kai.db
|-----xiaolv.db
|
|-----download/ # 用户下载文件的默认地址文件夹
|
|-----home/ # 已经创建的用户的家目录
| |-----kai/
| | -----xiaolv/
|
|-----README.py

实现过程(主要是下载上传文件过程)
举例: 客户端向服务端下载文件
1.判断客户端是否存在已下载的文件
|
|------- 是 -----》 客户端确认是否需要续传 ------》 是 --------》 服务端开始找资源—》 找到资源 ----》 开始续传
| | |-----》未找到资源----》 退出到下载界面
| |
| | -----》 否 ----》 退出到下载界面
|
|------- 否 -----》服务端开始找资源—》 找到资源 ----》 开始续传
|----》 未找到资源----》退出到下载界面

功能实现:
1.实现用户加密认证登录,目前暂不支持在交互界面添加用户,需要自己手动在database/目录下创建用户的数据库文件及添加相关的用户信息,并在home/目录下添加相关的目录
2.支持文件上传下载,支持断点续传,目前只支持文件传输,不支持目录的上传下载
3.支持目录切换及显示当前账户下的目录

http状态码:
200: 客户请求成功
202:创建的文件已经存在
400:用户名不存在,用户认证失败
403.11:密码错误
401: 命令不存在
402 :文件不存在
413:磁盘空间不够用
000: 系统交互码

----------------我是分割线--------------
接下来直接上代码

bin/

ftp客户端接口

import os,sys
path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(path)

from core import socket_client

if __name__ == "__main__":
    host,port = "localhost",9901
    myClient = socket_client.MySocketClient(host,port)
    myClient.start()

ftp服务端接口

import sys,os

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

from core import socket_server

if __name__ == "__main__":
    HOST,PORT = "localhost",9901
    server = socket_server.socketserver.ThreadingTCPServer((HOST,PORT),socket_server.MyTCPServer)
    server.serve_forever()

conf/

setting.py

import os,sys

BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

#用户家目录
HOMEPATH = os.path.join(BASEDIR,"home")

#数据库目录
DATABASE = os.path.join(BASEDIR,"database")

#分给没个用户的磁盘配额
LIMITSIZE = 20480000

#用户从服务端下载文件的默认地址
DEFAULT_PATH = "C:\\Users\\Administrator\\Desktop\\python\\ftp\\download"

core/

auth.py

import hashlib,os,json
from conf import settings

class User_auth(object):
    def auth(self,account_info):
        """
        #此功能是进行用户的登录信息验证,如果登录成功,那么返回用户对应的http状态码及账户信息,否则只返回http状态码
        :param account_info: 用户的账户信息:用户名,密码
        :return:
        """
        name = account_info.split(":")[0]
        pwd = account_info.split(":")[1]
        pwd = self.hash(pwd.encode())   # 将用户名的密码转换成hash值
        user_db_file = settings.DATABASE + r"\%s.db" %name  # 也可以写成 "\\%s.db"  or "/%s.db"
        if os.path.isfile(user_db_file):    # 输入的用户名存在
            with open(user_db_file) as fr:
                user_db_info = json.loads(fr.read())     # or josn.load(fr)
                if pwd == user_db_info['password']:
                    return "200",user_db_info      # 确定,客户请求成功
                else:
                    return "403.11",None     # 密码错误
        else:
            return "400",None     # 用户名不存在,用户认证失败


    def hash(self,pwd):
        """
         用户的密码加密
        :param self:
        :param pwd: 用户密码
        :return:
        """
        m = hashlib.md5()
        m.update(pwd)
        return m.hexdigest()

socket_client.py

import sys,os,time
import socket,hashlib
from core import upload
from conf import settings


class MySocketClient(object):
    def __init__(self,host,port):
        self.host = host
        self.port = port
        self.client = socket.socket()  # 创建客户端实例

    def start(self):
        self.client.connect((self.host,self.port))  # 连接服务端,localhost为回环地址,一般用于测试
        while True:
            """客户端开始输入账户信息登录"""
            name = input("\033[31m 请输入账户名: \033[0m").strip()
            pwd = input("\033[31m 请输入密码: \033[0m").strip()
            # name = "kai";pwd = 123456
            if not name or not pwd :continue  # 如果账户或密码为空请重新输入
            userInfo = "%s:%s" % (name,pwd)
            self.client.send(userInfo.encode())  # 向服务端发送用户信息
            status_code = self.client.recv(1024).decode() # 接受来自服务端的http状态码
            if status_code == "400":
                print("\033[1;32m 用户名不存在,用户认证失败,请重新输入 \033[0m")
                continue   # 用户名不存在重新输入
            if status_code == "403.11":
                print("\033[1;32m 密码错误,请重新输入 033[0m")
                continue
            if status_code == "200":
                print("\033[1;32m 登录成功\033[0m")
            self.interact()

    def interact(self):
        """
        客户端与服务端的交互接口
        :return:
        """
        while True:
            cmd = input("""请输入操作: 
            \033[1;31m
            eg:
            get filename  # 从服务端下载文件
            put filename  # 从服务端上传文件
            ls            # 查看当前目录下的文件
            cd            # 目录切换
            pwd           # 查看当前目录
            mkdir dirname # 创建目录
            >>>  \033[0m""").strip()
            cmd_action = cmd.split()[0]
            if hasattr(self,cmd_action):
                func = getattr(self,cmd_action)
                func(cmd)
            else:
                print("输入命令不存在")

    def put(self,cmd):
        """
        客户端上传文件
        :param self:
        :param cmd: 用户的操作命令  eg: put filename
        :return:
        """
        lst = cmd.split()    # 命令分割,此次只允许一次上传一个文件
        if len(lst) == 2:
            filename = lst[1]
            if os.path.isfile(filename):
                self.client.send(cmd.encode())   # 向服务端发送上传文件命令
                status_code = self.client.recv(1024).decode()
                total_size = os.stat(filename).st_size
                self.client.send(str(total_size).encode())
                status_code = self.client.recv(1024).decode()    # 接收来自服务端的http状态码,判断账户是否空间充足,文件是否存在
                if status_code == "202":
                    while True:
                        affirm_msg = input("创建的文件已经存在,请确认是否需要续传:1:续传 2:不续传:")
                        if affirm_msg == "1":
                            print("开始续传")
                            self.client.send("000".encode())    # 发送交互码,避免粘包,服务端此时需要2次连续send
                            has_send_size = self.client.recv(1024).decode()     # 已经发送给服务端的文件大小
                            send_md5 = upload.Breakpoint().transfer(filename, int(has_send_size), total_size, self.client) # 进行文件的传输,返回此次发送数据的md5
                            recv_md5 = self.client.recv(1024).decode()   # 服务端接收到此次发送数据的md5
                            # print("\nsend",send_md5)
                            # print("recv",recv_md5)
                            if send_md5 == recv_md5 :
                                print("发送数据成功")
                                break
                            else:
                                print("发送数据不成功,请重新发送")
                                break
                        elif affirm_msg == "2":
                            print("不续传")
                            break
                        else:
                            print("输入的命令不存在")
                            continue
                elif status_code == "402":
                    print("文件不存在")   # 文件不存在
                    send_md5 = upload.Breakpoint().transfer(filename, 0, total_size,self.client)  # 进行文件的传输,返回此次发送数据的md5
                    recv_md5 = self.client.recv(1024).decode()  # 服务端接收到此次发送数据的md5
                    print("\nsend md5", send_md5)
                    print("recv md5",recv_md5)
                    if send_md5 == recv_md5 : print("发送数据成功")
                    else : print("发送数据不成功,请重新发送")
                else:
                    print("用户磁盘空间不够")
            else:print("发送的文件不存在")
        else: print("401 error,命令不正确,一次只能上传一个文件")

    def get(self,cmd):
        lst = cmd.split()
        if len(lst) == 2:
            filename = lst[1]
            fileName = filename.split("\\")[-1]
            self.default_file = os.path.join(settings.DEFAULT_PATH,fileName)
            if os.path.isfile(self.default_file): # 用户下载的文件在默认地址下是否存在
                while True:
                    affirm_msg = input("创建的文件已经存在,请确认是否需要续传:1:续传 2:不续传:")
                    if affirm_msg == "1":
                        self.get_file(cmd)
                    elif affirm_msg == "2":
                        break
                    else:
                        print("命令不正确")
            else:
                self.get_file(cmd,exist="no")
        else: print("401 error,命令不正确,一次只能下载一个文件")

    def get_file(self,cmd,exist="yes"):
        """
        因为get方法从客户端下载文件,文件存在续传和文件不存在直接下载写法一样,故提取
        :param cmd:
        :param exist: 判断客户端文件是否存在
        :return:
        """
        self.client.send(cmd.encode())
        status_code = self.client.recv(1024).decode()
        if status_code == "206":
            print("需要下载的文件在客户端存在,在服务端也存在,开始续传" if exist == "yes" else "需要下载的文件在服务端存在,在客户端不存在,开始下载")
            has_recvd_size = os.stat(self.default_file).st_size if exist == "yes" else 0
            self.client.send(str(has_recvd_size).encode())
            total_size = self.client.recv(1024).decode()
            self.client.send("000".encode())  # 系统交互,防止粘包
            recvd_size = 0
            m = hashlib.md5()
            with open(self.default_file, "ab") as fa:
                remain_size = int(total_size) - has_recvd_size
                while recvd_size != remain_size:  # 循环接收来自服务端的文件数据
                    data = self.client.recv(1024)
                    m.update(data)
                    recvd_size += len(data)
                    fa.write(data)
                    all_recvd_size = has_recvd_size + recvd_size
                    upload.Breakpoint().progress_bar(all_recvd_size,int(total_size))
                self.client.send("000".encode())  # 粘包
                send_md5 = self.client.recv(1024).decode()  # 服务端发送数据的md5
                if has_recvd_size == int(total_size):
                    print("文件大小一致,无需下载")
                    self.interact()    # 如果按这种逻辑(先判断客户端文件是否存在,在判断该文件的大小是否一致)写,只能调用interact()选项才能返回
                if send_md5 == m.hexdigest():
                    print("接收文件成功")
                else:
                    print("接收文件不成功")
        else:
            print("需要下载的文件不存在,无法续传")

    def mkdir(self,cmd):
        self.client.send(cmd.encode())
        status_code = self.client.recv(1024).decode()
        if status_code == "403":
            print("创建的目录存在")
        elif status_code == "401":
            print("输入的命令不存在")

    def pwd(self,cmd):
        self.client.send(cmd.encode())
        cur_path = self.client.recv(1024).decode()
        print(cur_path)

    def cd(self,cmd):
        """
        :param cmd:  eg cd dirname / cd .. /
        :return:
        """
        self.client.send(cmd.encode())
        status_code = self.client.recv(1024).decode()
        if status_code == "402":
            print("创建的目录不存在")
        elif status_code == "400":
            print("命令不正确,格式 cd .. / cd dirname")
        elif status_code == "403":
            print("没有上层目录了")
        else:
            pass

    def ls(self,cmd):
        self.client.send(cmd.encode())
        dirs = self.client.recv(1024).decode()
        print(dirs)




if __name__ == "__main__":
    host,port = "localhost",9998
    myClient = MySocketClient(host,port)
    myClient.start()

socket_server.py

import socketserver
import hashlib,os
from core import upload
from core import auth
from conf import settings


#socketserver
class MyTCPServer(socketserver.BaseRequestHandler):
    def handle(self):
        try:
            while True:
                self.data = self.request.recv(1024).decode()   # 接受来自客户端的账号的登录信息
                auth_result = auth.User_auth().auth(self.data)   #  进行用户验证
                status_code = auth_result[0]
                if status_code == "400" or status_code == "403.11":
                    self.request.send(status_code.encode())  # 用户名不存在或密码错误
                    continue
                self.request.send(status_code.encode())   # 用户认证成功
                self.user_db_info = auth_result[1]        # 获取用户的数据库信息
                self.home_path = self.user_db_info['homepath']
                self.current_path = self.user_db_info['homepath']  # 登陆成功后立即定义一个“当前路径”变量,供后面创建目录,切换目录使用
                while True:
                    self.cmd = self.request.recv(1024).decode()   # 接收客户端的上传或下载命令
                    self.cmd_action = self.cmd.split()[0]
                    if hasattr(self,self.cmd_action):
                        func = getattr(self,self.cmd_action)
                        func(self.cmd)
                    else:
                        self.request.send("401".encode())  # 命令不存在
        except ConnectionResetError as e:
            self.request.close()
            print(e)


    """服务端可以提供的命令"""
    def put(self,cmd):
        """
        接受客户端的上传文件命令
        :param self:
        :param cmd: eg: put filename
        :return:
        """
        filename = cmd.split()[1]
        fileName = filename.split("\\")[-1]  # 文件的名称
        home_file = os.path.join(self.current_path, fileName)    # 判断当前路径下是否有上传的文件名
        self.request.send("000".encode())  # 系统交互码
        total_size = self.request.recv(1024).decode() # 上传文件大小
        remain_size = self.accountRemainSize()  # 获得账户剩余大小
        if remain_size > int(total_size):
            if os.path.isfile(home_file):
                self.request.send("202".encode())   # 创建的文件已经存在
                self.request.recv(1024)   # 等待ack指令
                has_recvd_size = os.stat(home_file).st_size   # 已经接收的大小
                self.request.send(str(has_recvd_size).encode())
            else :
                has_recvd_size = 0
                self.request.send("402".encode())  # 创建的文件不存在
            ###开始接受客户端的数据
            recvd_size = 0
            m = hashlib.md5()
            with open(home_file,"ab") as fa:
                while recvd_size != int(total_size) - has_recvd_size:    # 循环接受来自的文件数据
                    data = self.request.recv(1024)
                    m.update(data)
                    recvd_size += len(data)
                    fa.write(data)
                self.request.send(m.hexdigest().encode())   # 发送接收到的文件的md5
        else:
            self.request.send("413".encode())  # 磁盘空间不够用

    def get(self,cmd):
        """
        接受客户端的上传文件命令
        :param self:
        :param cmd: eg: get filename
        :return:
        """
        filename = cmd.split()[1]
        fileName = filename.split("\\")[-1]  # 文件的名称
        home_file = os.path.join(self.home_path, fileName)    # 用于判断家目录下是否有下载的文件名
        if os.path.isfile(home_file):    # 这里不用判断if status_code == "205" or status_code == "402": ,因为如果接收到客户端的状态码,服务端一定要先去找文件是否存在
            total_size = os.stat(home_file).st_size
            self.request.send("206".encode())  # 服务端有客户需要下载的文件
            has_send_size = self.request.recv(1024).decode()
            self.request.send(str(total_size).encode())
            self.request.recv(1024)  # 粘包
            send_md5 = upload.Breakpoint().transfer(home_file, int(has_send_size), total_size, self.request)
            self.request.recv(1024)  # 粘包
            self.request.send(send_md5.encode())
        else:
            self.request.send("402".encode())  # 服务端文件不存在


    def accountRemainSize(self):
        """
        统计登录用户可用目录大小
        :return:
        """
        account_path = self.user_db_info["homepath"]
        print("homepath:",account_path,)
        limitsize = self.user_db_info['limitsize']
        used_size = 0
        for root,dirs,files in os.walk(account_path):
            used_size += sum([ os.path.getsize(os.path.join(root,filename))  for filename in files])
        return limitsize - used_size

    def mkdir(self,cmd):
        """
        创建目录,支持创建级联目录
        :param cmd:
        :return:
        """
        dir = cmd.split()
        dir_path = os.path.join(self.current_path,dir[-1])
        if len(dir) == 2:
            if not os.path.isdir(dir_path):
                try:
                    os.mkdir(dir_path)
                except FileNotFoundError as e:
                    os.makedirs(dir_path)   #创建级联目录
                self.request.send("200".encode())
            else:
                self.request.send("403".encode())  # 创建目录存在
        else:
            self.request.send("400".encode())

    def pwd(self,cmd):
        self.request.send(self.current_path.encode())

    def cd(self,cmd):
        dir = cmd.split()
        if len(dir) == 2:
            if dir[-1] == "..":    # 只能让用户在自己目录下操作
                if len(self.current_path) > len(self.home_path):
                    self.current_path = os.path.dirname(self.current_path)
                    self.request.send("200".encode())
                else:
                    self.request.send("403".encode()) # 请求被拒绝,没有上层目录了
            elif os.path.isdir(self.current_path + "\\" + dir[-1]):
                self.current_path = self.current_path + "\\" + dir[-1]
                self.request.send("200".encode())
            else:
                self.request.send("402".encode())
        else:
            self.request.send("400".encode())

    def ls(self,cmd):
        dirs = os.listdir(self.current_path)
        self.request.send(str(dirs).encode())

upload.py

import hashlib,sys,time


class Breakpoint(object):
    # 本模块确认用户上传或下载的文件是否存在,如果存在是否需要断点续传
    def transfer(self,filename,has_send_size,total_size,conn):
        """
        进行续传
        :param filename:
        :param has_send_size: 已经发送的文件大小
        :param total_size: 需要传输文件总大小
        :param conn: 客户端和服务端进行数据交换的接口
        :return:
        """
        with open(filename,'rb') as fr:
            fr.seek(has_send_size)     # 定位到续传的位置
            print("has_send",has_send_size,"total",total_size)
            m = hashlib.md5()
            if has_send_size == total_size:
                self.progress_bar(has_send_size, total_size)
            for line in fr:
                conn.send(line)
                m.update(line)
                has_send_size += len(line)
                # self.progress_bar(has_send_size,total_size)
        return m.hexdigest()

    def progress_bar(self,has_send_size,total_size):
        bar_width = 50     # 进度条长度
        process = has_send_size / total_size
        send_bar = int(process * bar_width + 0.5)     # 发送的数据占到的进度条长度,四舍五入取整
        sys.stdout.write("#" * send_bar  + "=" * (bar_width - send_bar) +"\r" )  # 注意点:貌似只能这么写才能达到要求
        sys.stdout.write("\r%.2f%%: %s%s"% (process * 100,"#" * send_bar,"=" * (bar_width - send_bar)))  # 注意点:在pycharm中要加\r\n,用sublime只要\r否则换行
        sys.stdout.flush()

-------------我还是分割线----------------------
最后效果图
这里写图片描述

这里写图片描述

转载请说明出处
http://my.csdn.net/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值