老男孩Day9作业:高级FTP

时间:2021-07-17 02:12:54

一、作业需求

1. 用户加密认证(已完成)

2. 多用户同时登陆(已完成)

3. 每个用户有自己的家目录且只能访问自己的家目录(已完成)

4. 对用户进行磁盘配额、不同用户配额可不同(已完成)

5. 用户可以登陆server后,可切换目录(已完成)

6. 查看当前目录下文件(已完成)

7. 上传下载文件,保证文件一致性(已完成)

8. 传输过程中现实进度条(已完成)

9. 支持断点续传(未完成)

readme:

 

老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
一、作业需求:

1. 用户加密认证(已完成)

2. 多用户同时登陆(已完成)

3. 每个用户有自己的家目录且只能访问自己的家目录(已完成)

4. 对用户进行磁盘配额、不同用户配额可不同(已完成)

5. 用户可以登陆server后,可切换目录(已完成)

6. 查看当前目录下文件(已完成)

7. 上传下载文件,保证文件一致性(已完成)

8. 传输过程中现实进度条(已完成)

9. 支持断点续传(未完成)

二、博客地址:http://www.cnblogs.com/catepython/p/8616018.html

三、运行环境

操作系统:Win10

Python:3.6.2rcl

Pycharm:2017.1.14

四、功能实现

1)多用户同时登录,并做了用户不得重复登录判断(现为测试方便此调用方法已注释)

2)区分不同用户不同的文件目录

3)可在当前目录下上传/下载文件并保存

4)上传/下载文件进度显示

5)区分了用户本地/服务端文件目录

6)只能移动到自己家目录下的目录

cd /:移动到根目录下    cd ..:返回上一级目录    cd + 目录名:移动到指定目录下

7)新增pwd查看当前路径操作

8)查看当前目录下文件信息    

新增dir home:查看用户本地目录文件信息    dir server:查看用户服务端目录文件信息

9)每个用户有不同的磁盘配额

10)上传/下载文件后进行加密认证

11)新增mkdir操作:在当前目录下创建新目录文件


五、测试

1)文件名为空判断

2)用户信息判断

3)指令格式化判断

4)用户使用cd指令对其做了isdir()判断

5)用户使用mkdir指令时对其做了当前目录下已有同名目录判断

6)上传/下载到指定路径判断 

例:

1、当前在根目录下:E:.....\user_home

上传/下载文件完成后文件保存至根目录下

2、当前路径:E:.....\user_home\test\test2

上传/下载文件完成后文件保存在test2目录下

7)在当前路径下创建新目录文件

例:

1、当前在根目录下:E:.....\user_home

使用mkdir命令在根目录下创建新目录

2、当前路径:E:.....\user_home\test\test2

使用mkdir命令在E:.....\user_home\test\test2目录下创建新目录

8)上传/下载文件后进行加密认证:对本地文件与服务端文件做了mk5加密认证

9)做了多用户登录上传/下载

10)当用户配额<上传/下载文件时会做“磁盘配额不足无法上传/下载文件”提示



六、备注
1、断点续传功能有空时可以新增并完善
readme

 

 

 

二、流程图

 老男孩Day9作业:高级FTP

三、目录结构图

老男孩Day9作业:高级FTP

 

四、代码区

bin目录下程序开始文件

老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
#-*- Coding:utf-8 -*-
# Author: D.Gray
import os,sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

from core import ftp_client
fc = ftp_client.Ftp_client()
start_client.py
老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
#-*- Coding:utf-8 -*-
# Author: D.Gray
import os,sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

from core import ftp_server
fs = ftp_server.Ftp_server()
start_server.py

conf目下的setting.py系统配置文件

老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
#-*- Coding:utf-8 -*-
# Author: D.Gray
import os,sys,socket
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

#IP和端口信息
IP_PORT = ("localhost",6969)

#用户数据文件
USER_FILE = os.path.join(BASE_DIR,'db\\user_info')
#用户文件目录
USER_HOME = BASE_DIR
setting.py

core目录下主程序文件

老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
#-*- Coding:utf-8 -*-
# Author: D.Gray
import sys,os,socket,hashlib,time,json
from conf import setting
from core import users
class Ftp_client(object):
    '''
    FTP客服端
    '''
    def __init__(self):
        '''
        构造函数
        '''
        self.client = setting.socket.socket()
        self.client.connect(setting.IP_PORT)
        self.help_info = """\033[33;1m
                    请用'put'+'空格'+'文件名'的格式下载文件
                    请用'get'+'空格'+'文件名'的格式上传文件
                    请用'cd'+'空格'+'目录名'的格式进入家目录下的子文件夹
                    请用'cd'+'空格'+'..'的格式返回上级目录
                    请用'mkdir'+'空格'+'目录名'的格式创建家目录的文件夹
                    输入'dir'+'空格'+'home'查看用户家目录
                    输入'dir'+'空格'+'server'查看用户服务端家目录
            \033[0m"""
        if self.auth():
            self.start()

    def auth(self):
        '''
        用户登录认证函数
        1、用户输入账号密码
        2、序列化用户信息字典发送给服务端
        3、接收服务端用户登录认证消息
        4、认证成功返回True给构造函数
        5、用户进入start()函数进行指令操作
        :return:
        '''
        while True:
            username = input("请输入账户名>>>:").strip()
            password = input('请输入用户密码>>>:').strip()
            #auth = 'auth %s %s'%(username,password)
            mesg = {
                "action":'auth',
                "username":username,
                "password":password
            }
            self.client.send(json.dumps(mesg).encode())
            self.user_obj = users.Users(username)
            back_res = self.client.recv(1024).decode()
            if back_res == 'ok':
                print("\033[32;1m认证成功\033[0m")
                user = self.user_obj.get_user()
                self.user_name = user["username"]
                self.user_type = user["type"]
                self.user_path = user['home']
                self.disk_quota = user["disk_quota"]
                self.pwd_path = os.path.join(setting.USER_HOME,self.user_path,"user_home") #定义一个默认路径
                return True
            elif back_res == "302":
                print("\033[31;1m密码错误\033[0m")
            elif back_res == "301":
                print("\033[31;1m该用户已登录\033[0m")
            else:
                print("\033[31;1m用户不存在\033[0m")

    def start(self):
        '''
        用户操作函数
        1、用户输入操作指令
        2、判断操作指令是否有效
        3、反射指令
        :return:
        '''
        while True:
            user_inport = input("%s>>>:"%(self.user_name)).strip()
            if len(user_inport) == 0 :continue
            user_inport = user_inport.split()
            if user_inport[0] == 'q':
                break
            if hasattr(self,user_inport[0]):
                func = getattr(self,user_inport[0])
                func(user_inport)
            else:
                print("\033[31;1m请输入有效指令:\033[0m",self.help_info)
                continue

    def put(self,cmd):
        '''
        下载服务端文件函数
        1、接收服务端回调信息(305 = 服务端文件不存在或下载文件大小)
        2、判断磁盘配额和文件大小
        3、接收服务端回调信息
        4、开始接收文件并打印进度条
        5、加密认证
        6、重新计算磁盘配额  调用Users类中update_disk_quota()方法将最新磁盘配额参数重新写入用户文件中
        :param cmd:
        :return:
        '''
        if len(cmd) < 2:
            print("\033[31;1m请输入有效指令:\033[0m", self.help_info)
        else:
            '''
            下载服务端文件
            '''
            mesg = {
                "action": cmd[0],
                "file_name": cmd[1],
                "disk_quota": self.disk_quota
            }
            self.client.send(json.dumps(mesg).encode())
            server_back = self.client.recv(1024).decode()
            print("\033[32;1m收到服务器回调:\033[0m",server_back)
            if server_back == '305':
                print("\033[31;1m文件不存在\033[0m")
            else:
                file_total_size = int(server_back)
                print("\033[32;1m下载文件总大小:\033[0m", file_total_size)
                print("\033[32;1m磁盘配额还剩:%sM\033[0m" % mesg["disk_quota"])
                if file_total_size >= mesg["disk_quota"] * (2 ** 20):
                    print('\033[31;1m磁盘配额不够无法下载文件\033[0m')
                else:
                    revered_size = 0
                    # file_path = os.path.join(setting.USER_HOME,self.user_path,"user_home",cmd[1])
                    file_path = os.path.join(self.pwd_path,cmd[1])
                    print('in the put_pwd_path:',file_path)
                    self.client.send(b"ok")
                    self.m = hashlib.md5()
                    i = 0
                    with open(file_path,'wb') as f:
                        while revered_size < file_total_size:
                            if file_total_size - revered_size < 1024:
                                size = file_total_size - revered_size
                            else:
                                size = 1024
                            data = self.client.recv(size)
                            revered_size += len(data)
                            '''
                            打印进度条
                            '''
                            str1 = "已接受 %sByte"%revered_size
                            str2 = '%s%s'%(round((revered_size/file_total_size)*100,2),'%')
                            str3 = '[%s%s]'%('*'*i,str2)
                            sys.stdout.write('\033[32;1m\r%s%s\033[0m'%(str1,str3))
                            sys.stdout.flush()
                            i += 2
                            time.sleep(0.3)
                            '''
                            加密认证
                            '''
                            self.m.update(data)
                            f.write(data)
                        self.encryption()
                        '''
                        磁盘配额
                        '''
                        new_disk_quota = round((mesg["disk_quota"] * (2 ** 20) - file_total_size) / (2 ** 20), 2)
                        # mesg["disk_quota"]* (2 ** 20)  将用户文件中磁盘参数转成相应的Bytes数值
                        self.user_obj.update_disk_quota(new_disk_quota)
                        print("\033[32;1m磁盘配额还剩:%sM\033[0m"%new_disk_quota)

    def get(self,cmd):
        '''
        客户端上传文件至服务端函数
        1、判断指令格式是否正确
        2、上传文件或文件路径是否有效和存在
        3、获取文件大小
        4、判断磁盘配额是否大于文件大小
        5、获取服务端上传文件回调请求
        6、发送文件并打印进度条
        7、加密认证
        8、重新计算磁盘配额  调用Users类中update_disk_quota()方法将最新磁盘配额参数重新写入用户文件中
        :param cmd:
        :return:
        '''
        if len(cmd) < 2:
            print("\033[31;1m请输入有效指令:\033[0m", self.help_info)
        else:
            '''
            上传文件
            '''
            file_path = os.path.join(self.pwd_path,cmd[1])
            if os.path.isfile(file_path):
                file_total_size = os.stat(file_path).st_size
                mesg = {
                    "action": cmd[0],
                    "file_name": cmd[1],
                    "disk_quota": self.disk_quota,
                    "file_size" : file_total_size
                }
                print("\033[32;1m磁盘配额还剩:%sM\033[0m" % mesg["disk_quota"])
                if file_total_size >= mesg["disk_quota"]*(2**20):
                    print("\033[31;1m磁盘配额不够无法上传文件\033[0m")
                else:
                    self.client.send(json.dumps(mesg).encode())
                    print("\033[32;1m上传文件总大小:\033[0m", file_total_size)
                    self.client.recv(1024)
                    print("开始发送文件")
                    self.m = hashlib.md5()
                    send_size = 0
                    i = 0
                    with open(file_path,'rb')as f:
                        while send_size < file_total_size:
                            if file_total_size - send_size <1024:
                                size = file_total_size - send_size
                                data = f.read(size)
                                send_size += len(data)
                            else:
                                data = f.read(1024)
                                send_size += len(data)
                            self.client.send(data)
                            '''
                            打印进度条
                            '''
                            str1 = "已上传 %sByte:" %send_size
                            str2 = '%s%s' % (round((send_size / file_total_size) * 100, 2), '%')
                            str3 = '[%s%s]' % ('*'*i, str2)
                            sys.stdout.write('\033[32;1m\r%s%s\033[0m' % (str1, str3))
                            sys.stdout.flush()
                            i += 2
                            time.sleep(0.3)
                            '''
                            文件加密
                            '''
                            self.m.update(data)
                    self.encryption()
                    '''
                    磁盘配额
                    '''
                    new_disk_quota = round((mesg["disk_quota"]*(2**20) - file_total_size)/(2**20),2)
                    self.user_obj.update_disk_quota(new_disk_quota)
                    print("\033[32;1m磁盘配额还剩:%sM\033[0m"%new_disk_quota)
            else:
                print("\033[31;1m文件不存在\033[0m")

    def encryption(self):
        '''
        文件加密函数
        1、判断用户是否需要加密
        2、取消加密发送'401'信息给服务端
        3、确认加密发送'400'信息给服务端
        4、接收服务端文件加密信息
        5、判断客户端和服务端文件加密信息是否一致
        :return:
        '''
        encryption = input("\n文件已接收是否需要加密认证...按q取消加密>>>")
        if encryption != 'q':
            self.client.send(b'400')
            print('\033[32;1m确认加密\033[0m')
            file_md5 = self.m.hexdigest()
            server_back_md5 = self.client.recv(1024).decode()
            print("\033[32;1m本地文件加密:%s\n服务端文件加密:%s\033[0m" % (file_md5, server_back_md5))
            if file_md5 == server_back_md5:
                print("\033[32;1m加密认证成功\033[0m")
            else:
                print("加密认证失败")
        else:
            self.client.send(b'401')
            print("\033[32;1m\n已取消加密.文件接收成功\033[0m")

    def dir(self,cmd):
        '''
        查看根目录下文件信息函数
        1、dir_home 查看用户本地文件内容
        2、dir_server 查看用户服务器文件内容
        3、接收服务端指令文件大小
        4、发送接收目录信息指令
        5、接收目录信息
        :param cmd:
        :return:
        '''
        if len(cmd) < 2:
            print("\033[31;1m请输入有效指令:\033[0m", self.help_info)
        else:
            if cmd[1] == "home" or cmd[1] == 'server':
                mesg = {
                    "action":cmd[0],
                    "object":cmd[1]
                }
                self.client.send(json.dumps(mesg).encode())
                server_back = self.client.recv(1024).decode()
                print('\033[32;1m收到服务端回调指令大小:\033[0m',server_back)
                self.client.send("ok".encode())
                revered_size = 0
                revered_data = b''
                while revered_size < int(server_back):
                    data = self.client.recv(1024)
                    revered_data += data
                    revered_size = len(data)
                    print('\033[32;1m实际收到指令大小:\033[0m',revered_size)
                else:
                    print(revered_data.decode())
            else:
                print("\033[31;1m请输入有效指令:\033[0m", self.help_info)

    def mkdir(self,cmd):
        '''
        添加目录文件函数
        1、判断指令是否正确
        2、先获取当前路径
        3、判断所添加目录是否已存在
        4、使用os.mkdir()函数添加新目录
        5、新目录添加成功
        :param cmd:
        :return:
        '''
        if len(cmd) < 2:
            print("\033[31;1m请输入有效指令:\033[0m", self.help_info)
        else:
            # file_path = os.path.join(setting.USER_HOME,self.user_path,"user_home",cmd[1])
            file_path = os.path.join(self.pwd_path,cmd[1])
            print("当前路径:", file_path)
            if os.path.exists(file_path):
                print("\033[31;1m该目录文件夹已存在\033[0m")
            else:
                os.mkdir(file_path)
                print("该目录文件夹创建成功")

    def cd(self,cmd):
        '''
        CD:移动到指定目录函数
        1、先判断指令是否正确
        2、判断路径是否有效
        3、根据输入做相应操作如:cd ..:移动到上一级目录   cd / :移动到根目录  cd 目录名:移动到指定目录
        4、拆分路径重新拼接新路径
        5、返回self.pwd_path当前所在目录
        :param cmd:
        :return:
        '''
        if len(cmd) < 2:
            print("\033[31;1m请输入有效指令:\033[0m", self.help_info)
        else:
            if cmd[1] == '..':
                list = []
                pwd_path = os.path.join(self.pwd_path)
                for index in pwd_path.split('\\'):  #列表形式拆分当前目录路径以'\\'分隔
                    list.append(index)          #将目录路径参数添加至list列表中
                list[0] = '%s%s'%(list[0],'/')  #将列表第一个元素 E: 字符串拼接成 E:/
                if list[-1] == "user_home":
                    print("已在根目录下")
                else:
                    del list[-1]    #删除列表最后个元素也就是上一级目录路径
                    self.pwd_path = ''
                    for item in list:       #重新拼接成新的路径
                        self.pwd_path = os.path.join(self.pwd_path,item)
                    print("当前路径:",self.pwd_path)
                    #print(os.listdir(self.pwd_path))
            elif cmd[1] == '/':
                self.pwd_path = os.path.join(setting.USER_HOME,self.user_path,"user_home")
                print("已返回根目录:", self.pwd_path)
            else:
                pwd_path = os.path.join(self.pwd_path,cmd[1])   #移动到指定目录  cmd[1]目录名
                if os.path.isdir(pwd_path):
                    #print(os.listdir(pwd_path))
                    self.pwd_path = pwd_path    #返回用户当前路径
                    print("当前路径:", self.pwd_path)
                else:
                    print("\033[31;1m系统找不到指定的路径\033[0m")

    def pwd(self,cmd):
        '''
        显示当前目录路径
        :param cmd:
        :return:
        '''
        print("当前路径:", self.pwd_path)
        print(os.listdir(self.pwd_path))

    def help(self,cmd):
        '''
        帮助文档函数
        :param cmd:
        :return:
        '''
        print(self.help_info)
ftp_client.py
老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
#-*- Coding:utf-8 -*-
# Author: D.Gray
import sys,os,socket,hashlib,socketserver,json,time
from conf import setting
from core import users
class MyServer(socketserver.BaseRequestHandler):
    print('等待链接...')
    '''
    FTP服务端类
    '''
    def auth(self,*args):
        '''
        用户登录认证函数
        1、接收客户端用户字典信息
        2、序列化字典信息
        3、调用Users类中get_user()函数
        4、判断用户是否有效
        5、发送认证信息至客户端
        :param args:
        :return:
        '''
        cmd = args[0]
        self.user_obj = users.Users(cmd['username'])
        auth_user = self.user_obj.get_user()
        if auth_user:
            if auth_user['password'] == cmd["password"]:
                if auth_user['status'] == 0:
                    self.request.send(b"ok")
                    #self.user_obj.update_status_close()
                    self.user_home = auth_user["home"]
                    self.user_type = auth_user["type"]
                else:
                    self.request.send(b"301")
                    print("\033[31;1m该用户已登录\033[0m")
            else:
                self.request.send(b'302')
                print("\033[31;1m密码错误\033[0m")
        else:
            self.request.send(b"300")
            print("\033[31;1m用户名不存在\033[0m")

    def put(self,*args):
        '''
        服务端发送文件给客户端
        1、判断文件是否存在
        2、获取文件总大小
        3、发送文件大小给客户端
        4、接收客户端下载文件请求
        5、开始循环发送文件给客户端
        6、发送完成后调用加密函数
        :param args:
        :return:
        '''
        cmd = args[0]
        file_path = os.path.join(setting.USER_HOME,self.user_home,'server_home',cmd["file_name"])
        if os.path.isfile(file_path):
            file_total_size = os.stat(file_path).st_size
            print("\033[32;1m获取文件大小:\033[0m",file_total_size)
            self.request.send(str(file_total_size).encode())
            self.request.recv(1024)
            print("开始发送文件")
            self.m = hashlib.md5()
            with open(file_path,'rb') as f:
                for line in f:
                    self.m.update(line)
                    self.request.send(line)
                self.encryption()
        else:
            self.request.send(b'305')
            print("文件不存在")

    def get(self,*args):
        '''
        服务端接收客户端发送文件函数
        1、接收客户端发送文件大小
        2、发送接收客户端文件请求
        3、开始接收文件
        4、跟踪文件接收并写入相应目录
        5、接收完成后调用加密函数
        :param args:
        :return:
        '''
        cmd = args[0]
        #print(cmd)
        file_path = os.path.join(setting.USER_HOME,self.user_home,"server_home",cmd["file_name"])
        print("\033[32;1m收到客户端发送文件大小:\033[0m", cmd["file_size"])
        self.request.send(b"ok")
        print("开始接收文件")
        file_total_size = cmd["file_size"]
        rever_size = 0
        self.m = hashlib.md5()
        with open(file_path,'wb') as f:
            while rever_size < file_total_size:
                if file_total_size - rever_size <1024:
                    size = file_total_size - rever_size
                else:
                    size = 1024
                data = self.request.recv(size)
                rever_size += len(data)
                self.m.update(data)
                f.write(data)
            self.encryption()

    def encryption(self):
        '''
        加密认证函数
        1、发送确认加密'400'请求
        2、发送服务端文件加密信息至客户端
        :return:
        '''
        client_back = self.request.recv(1024).decode()
        if client_back == "400":
            print("\033[32;1m确认加密请求\033[0m")
            server_file_md5 = self.m.hexdigest()
            self.request.send(server_file_md5.encode())
            print("\033[32;1m服务端文件加密:\033[0m", server_file_md5)
        else:
            print("\033[32;1m\n已取消加密.客户端文件接收完成\033[0m")

    def dir(self,*args):
        '''
        服务端查看目录文件信息函数
        1、序列化客户端字典信息
        2、popen()获取目录文件信息
        3、判断目录信息长度
        4、发送目录长度信息至客户端
        5、接收客户端回调请求
        6、发送目录信息至客户端
        :param args:
        :return:
        '''
        cmd = args[0]
        if cmd["object"] == 'home':
            file_name = 'user_home'
        else:
            file_name = 'server_home'
        file_path = os.path.join(setting.USER_HOME,self.user_home,file_name)
        res = os.popen("%s %s"%(cmd["action"],file_path)).read()
        print("in the dir:",res)
        if len(res) == 0:
            res = "has not output"
        self.request.send(str(len(res.encode())).encode())
        self.request.recv(1024)
        self.request.send(res.encode())


    def handle(self):
        '''
        与客户端交互函数(解析客户端操作指令)
        1、接收客户端链接信息
        2、接收客户端操作指令(action)需序列化
        3、反射操作指令
        :return:
        '''
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{}已链接".format(self.client_address))
                actin_dict = json.loads(self.data.decode())
                #print(actin_dict)
                print('in the handle',actin_dict["action"])
                if hasattr(self,str(actin_dict["action"])):
                    func = getattr(self,str(actin_dict["action"]))
                    func(actin_dict)
            except ConnectionResetError as e:
                print("%s客户端已断开%s"%(self.client_address,e))
                #self.user_obj.update_status_open()
                break

server = socketserver.ThreadingTCPServer((setting.IP_PORT),MyServer)    #支持多用户操作:ThreadingTCPServer
server.serve_forever()
ftp_server.py

db/user_info目录下的数据文件

老男孩Day9作业:高级FTP老男孩Day9作业:高级FTP
{
  "username":"alex",
  "password":"admin",
  "status":0,
  "type":0,
  "home":"home\\alex",
  "disk_quota":0.97
}
alex.json