一、作業需求
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 }