老男孩Day9作业:高级FTP
一、作业需求
1. 用户加密认证(已完成)
2. 多用户同时登陆(已完成)
3. 每个用户有自己的家目录且只能访问自己的家目录(已完成)
4. 对用户进行磁盘配额、不同用户配额可不同(已完成)
5. 用户可以登陆server后,可切换目录(已完成)
6. 查看当前目录下文件(已完成)
7. 上传下载文件,保证文件一致性(已完成)
8. 传输过程中现实进度条(已完成)
9. 支持断点续传(未完成)
readme:
一、作业需求: 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、断点续传功能有空时可以新增并完善
二、流程图
三、目录结构图
四、代码区
bin目录下程序开始文件
#-*- 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()
#-*- 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()
conf目下的setting.py系统配置文件
#-*- 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
core目录下主程序文件
#-*- 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)
#-*- 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()
#-*- Coding:utf-8 -*- # Author: D.Gray import os,sys,json from conf import setting class Users(object): ''' 用户类 ''' def __init__(self,username): self.username = username self.user_file = setting.USER_FILE + "\\%s.json"%(self.username) #print(self.user_file) self.users_read = self.read_users() def read_users(self): ''' 读取用户文件信息函数 1、判断用户文件是否存在(用户是否存在) 2、遍历用户json文件中内容 3、返回遍历内容 :return: ''' if os.path.exists(self.user_file): with open(self.user_file, 'r') as f: user_read = eval(f.read()) return user_read def get_user(self): ''' 1、判断服务端传过来的用户名参数是否与文件中用户名参数 2、异常处理:用户名与用户文件参数类型不一直 :return: ''' #print('in the User_get_user:',user) try: if self.users_read["username"] == self.username: return self.users_read except TypeError as e: pass def update_status_close(self): ''' 修改用户登录状态函数 0-未登录 1-已登录 无法重复登录 :return: ''' with open(self.user_file,'r') as f: fr = f.read() fd = eval(fr) with open(self.user_file,'w') as fw: res = fr.replace(str(fd['status']),str(1)) fw.write(res) def update_status_open(self): ''' 修改用户登录状态函数 0-未登录 1-已登录 无法重复登录 :return: ''' with open(self.user_file, 'r') as f: fr = f.read() fd = eval(fr) with open(self.user_file, 'w') as fw: res = fr.replace(str(fd['status']), str(0)) fw.write(res) def update_disk_quota(self,new_disk_quota): ''' 更改用户磁盘配额函数 :param new_disk_quota:接收客户端新磁盘配额参数 :return: ''' with open(self.user_file,'r') as f: fr = f.read() fd = eval(fr) with open(self.user_file,'w') as fw: res = fr.replace(str(fd["disk_quota"]),str(new_disk_quota)) fw.write(res)
db/user_info目录下的数据文件
{ "username":"alex", "password":"admin", "status":0, "type":0, "home":"home\\alex", "disk_quota":0.97 }