作業:開發一個支持多用戶在線的FTP程序
要求:
- 用戶加密認證
- 允許同時多用戶登錄
- 每個用戶有自己的家目錄 ,且只能訪問自己的家目錄
- 對用戶進行磁盤配額,每個用戶的可用空間不同
- 允許用戶在ftp server上隨意切換目錄
- 允許用戶查看當前目錄下文件
- 允許上傳和下載文件,保證文件一致性
- 文件傳輸過程中顯示進度條
- 附加功能:支持文件的斷點續傳(僅下載)
程序
1、最最重要的readme:

### 作者介紹: * author:lzl ### 博客地址: * http://www.cnblogs.com/lianzhilei/p/5813986.html ### 功能實現 作業:開發一個支持多用戶在線的FTP程序 要求: 用戶加密認證 允許同時多用戶登錄 每個用戶有自己的家目錄 ,且只能訪問自己的家目錄 對用戶進行磁盤配額,每個用戶的可用空間不同 允許用戶在ftp server上隨意切換目錄 允許用戶查看當前目錄下文件 允許上傳和下載文件,保證文件一致性 文件傳輸過程中顯示進度條 附加功能:支持文件的斷點續傳 ### 目錄結構: FTP │ ├── ftpclient #客戶端程序 │ ├── __init__.py │ └── ftpclient.py #客戶端主程序 └── ftpserver #服務端程序 ├── README.txt ├── ftpserver.py #服務端入口程序 ├── conf #配置文件目錄 │ ├── __init__.py │ └── setting.py ├── modules #程序核心目錄 │ ├── __init__.py │ ├── auth_user.py #用戶認證模塊 │ └── sokect_server.py #sokectserver模塊 ├── database #用戶數據庫 │ ├── alex.db │ ├── lzl.db │ └── eric.db ├── home #用戶宿主目錄 │ ├── alex │ ├── lzl │ └── eric └── log ├── __init__.py └── log #待擴展.... ### 功能實現 1、conf目錄下settings.py模塊記錄可操作用戶信息,根據用戶信息生成用戶字典和宿主目錄,已經生成的不再新建 2、每個用戶的宿主目錄磁盤空間配額默認為10M,可在settings.py模塊里進行修改 3、程序運行在windows系統上,程序要求全部實現,下面是具體命令操作 4、切換目錄:cd .. 返回上一級目錄 cd dirname 進入dirname 用戶登錄后默認進入宿主目錄,只可在宿主目錄下隨意切換 5、創建目錄:mkdir dirname 在當前目錄下創建目錄,如果目錄存在則報錯,不存在創建 6、查看當前路徑: pwd 7、查看當前路徑下的文件名和目錄名: dir 8、下載文件(可續傳):get filename ①、服務端當前目錄存在此文件,客戶端不存在此文件,直接下載 ②、服務端當前目錄存在此文件,客戶端存在此文件名,之前下載中斷,文件可續傳,進行續傳 ③、服務端當前目錄存在此文件,客戶端存在此文件名,大小與服務端一致,不下載 9、上傳文件:put filename 判斷宿主目錄磁盤空間是否夠用,可以,上傳文件;否則,報錯 ### 狀態碼 400 用戶認證失敗 401 命令不正確 402 文件不存在 403 創建文件已經存在 404 磁盤空間不夠 405 不續傳 200 用戶認證成功 201 命令可以執行 202 磁盤空間夠用 203 文件具有一致性 205 續傳 000 系統交互碼
2、程序目錄結構:
3、ftp客戶端

#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian import socket import os,sys import hashlib class Myclient(): '''ftp客戶端''' def __init__(self,ip_port): self.ip_port = ip_port def connect(self): '''連接服務器''' self.client = socket.socket() self.client.connect(self.ip_port) def start(self): '''程序開始''' self.connect() while True: username = input("輸入用戶名:").strip() password = input("輸入密碼:").strip() login_info = ("%s:%s" %(username, password)) self.client.sendall(login_info.encode()) #發送用戶密碼信息 status_code = self.client.recv(1024).decode() #返回狀態碼 if status_code == "400": print("[%s]用戶密碼認證錯誤"%status_code) continue else:print("[%s]用戶密碼認證成功"%status_code) self.interactive() def interactive(self): '''開始交互''' while True: command = input("->>").strip() if not command:continue #if command == "exit":break command_str = command.split()[0] if hasattr(self,command_str): # 執行命令 func = getattr(self,command_str) func(command) else:print("[%s]命令不存在"%401) def get(self,command): '''下載文件''' self.client.sendall(command.encode()) #發送要執行的命令 status_code = self.client.recv(1024).decode() if status_code == "201": #命令可執行 filename = command.split()[1] # 文件名存在,判斷是否續傳 if os.path.isfile(filename): revice_size = os.stat(filename).st_size #文件已接收大小 self.client.sendall("403".encode()) response = self.client.recv(1024) self.client.sendall(str(revice_size).encode()) #發送已接收文件大小 status_code = self.client.recv(1024).decode() # 文件大小不一致,續傳 if status_code == "205": print("繼續上次上傳位置進行續傳") self.client.sendall("000".encode()) # 文件大小一致,不續傳,不下載 elif status_code == "405": print("文件已經存在,大小一致") return # 文件不存在 else: self.client.sendall("402".encode()) revice_size = 0 file_size = self.client.recv(1024).decode() #文件大小 file_size = int(file_size) self.client.sendall("000".encode()) with open(filename,"ab") as file: #開始接收 #file_size 為文件總大小 file_size +=revice_size m = hashlib.md5() while revice_size < file_size: minus_size = file_size - revice_size if minus_size > 1024: size = 1024 else: size = minus_size data = self.client.recv(size) revice_size += len(data) file.write(data) m.update(data) self.__progress(revice_size,file_size,"下載中") #進度條 new_file_md5 = m.hexdigest() #生成新文件的md5值 server_file_md5 = self.client.recv(1024).decode() if new_file_md5 == server_file_md5: #md5值一致 print("\n文件具有一致性") else:print("[%s] Error!"%(status_code)) def put(self,command): '''上傳文件''' if len(command.split()) > 1: filename = command.split()[1] #file_path = self.current_path + r"\%s"%filename if os.path.isfile(filename): #文件是否存在 self.client.sendall(command.encode()) #發送要執行的命令 response = self.client.recv(1024) #收到ack確認 file_size = os.stat(filename).st_size # 文件大小 self.client.sendall(str(file_size).encode()) # 發送文件大小 status_code = self.client.recv(1024).decode() # 等待響應,返回狀態碼 if status_code == "202": with open(filename,"rb") as file: m = hashlib.md5() for line in file: m.update(line) send_size = file.tell() self.client.sendall(line) self.__progress(send_size, file_size, "上傳中") # 進度條 self.client.sendall(m.hexdigest().encode()) #發送文件md5值 status_code = self.client.recv(1024).decode() #返回狀態碼 if status_code == "203": print("\n文件具有一致性") else:print("[%s] Error!"%(status_code)) else: print("[402] Error") else: print("[401] Error") def dir(self,command): '''查看當前目錄下的文件''' self.__universal_method_data(command) pass def pwd(self,command): '''查看當前用戶路徑''' self.__universal_method_data(command) pass def mkdir(self,command): '''創建目錄''' self.__universal_method_none(command) pass def cd(self,command): '''切換目錄''' self.__universal_method_none(command) pass def __progress(self, trans_size, file_size,mode): ''' 顯示進度條 trans_size: 已經傳輸的數據大小 file_size: 文件的總大小 mode: 模式 ''' bar_length = 100 #進度條長度 percent = float(trans_size) / float(file_size) hashes = '=' * int(percent * bar_length) #進度條顯示的數量長度百分比 spaces = ' ' * (bar_length - len(hashes)) #定義空格的數量=總長度-顯示長度 sys.stdout.write( "\r%s:%.2fM/%.2fM %d%% [%s]"%(mode,trans_size/1048576,file_size/1048576,percent*100,hashes+spaces)) sys.stdout.flush() def __universal_method_none(self,command): '''通用方法,無data輸出''' self.client.sendall(command.encode()) # 發送要執行的命令 status_code = self.client.recv(1024).decode() if status_code == "201": # 命令可執行 self.client.sendall("000".encode()) # 系統交互 else: print("[%s] Error!" % (status_code)) def __universal_method_data(self,command): '''通用方法,有data輸出''' self.client.sendall(command.encode()) #發送要執行的命令 status_code = self.client.recv(1024).decode() if status_code == "201": #命令可執行 self.client.sendall("000".encode()) #系統交互 data = self.client.recv(1024).decode() print(data) else:print("[%s] Error!" % (status_code)) if __name__ == "__main__": ip_port =("127.0.0.1",9999) #服務端ip、端口 client = Myclient(ip_port) #創建客戶端實例 client.start() #開始連接
4、ftp服務端

#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian import os,hashlib import json from conf import settings from modules import auth_user from modules import sokect_server def create_db(): '''創建用戶數據庫文件''' user_database={} encryption = auth_user.User_operation() limitsize = settings.LIMIT_SIZE for k,v in settings.USERS_PWD.items(): username = k password = encryption.hash(v) user_db_path = settings.DATABASE + r"\%s.db"%username user_home_path = settings.HOME_PATH + r"\%s"%username user_database["username"] = username user_database["password"] = password user_database["limitsize"] = limitsize user_database["homepath"] = user_home_path if not os.path.isfile(user_db_path): with open(user_db_path,"w") as file: file.write(json.dumps(user_database)) def create_dir(): '''創建用戶屬主目錄''' for username in settings.USERS_PWD: user_home_path = settings.HOME_PATH + r"\%s" %username if not os.path.isdir(user_home_path): os.popen("mkdir %s" %user_home_path) if __name__ == "__main__": '''初始化系統數據並啟動程序''' create_db() #創建數據庫 create_dir() #創建屬主目錄 #啟動ftp服務 server = sokect_server.socketserver.ThreadingTCPServer(settings.IP_PORT, sokect_server.Myserver) server.serve_forever()
5、conf配置文件

#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian import os,sys #程序主目錄文件 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #添加環境變量 sys.path.insert(0,BASE_DIR) #數據庫目錄 DATABASE = os.path.join(BASE_DIR,"database") #用戶屬主目錄 HOME_PATH = os.path.join(BASE_DIR,"home") #用戶字典 USERS_PWD = {"alex":"123456","lzl":"8888","eric":"6666"} #磁盤配額 10M LIMIT_SIZE = 10240000 #ftp服務端口 IP_PORT = ("0.0.0.0",9999)
6、database用戶數據庫(系統初始化自動生成)

{"username": "alex", "password": "e10adc3949ba59abbe56e057f20f883e", "limitsize": 10240000, "homepath": "C:\\Users\\L\\PycharmProjects\\s14\\homework\\Day8\\FTP\\ftpserver\\home\\alex"}

{"username": "eric", "password": "e9510081ac30ffa83f10b68cde1cac07", "limitsize": 10240000, "homepath": "C:\\Users\\L\\PycharmProjects\\s14\\homework\\Day8\\FTP\\ftpserver\\home\\eric"}

{"username": "alex", "password": "e10adc3949ba59abbe56e057f20f883e", "limitsize": 10240000, "homepath": "C:\\Users\\L\\PycharmProjects\\s14\\homework\\Day8\\FTP\\ftpserver\\home\\alex"}
7、modules模塊dd

#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian import json import sys,os import hashlib from conf import settings class User_operation(): '''對登錄信息進行認證,登錄成功返回用戶名,失敗返回None''' def authentication(self,login_info): list = login_info.split(":") #對信息進行分割 login_name = list[0] login_passwd = self.hash(list[1]) DB_FILE = settings.DATABASE + r"\%s.db"%login_name if os.path.isfile(DB_FILE): user_database = self.cat_database(DB_FILE) #用戶數據庫信息 if login_name == user_database["username"]: if login_passwd == user_database["password"]: return user_database def cat_database(self,DB_FILE): #獲取數據庫信息 with open(DB_FILE,"r") as file: data = json.loads(file.read()) return data def hash(self,passwd): '''對密碼進行md5加密''' m = hashlib.md5() m.update(passwd.encode("utf-8")) return m.hexdigest()

#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian import socketserver import sys,os import hashlib from os.path import join, getsize from conf import settings from modules import auth_user class Myserver(socketserver.BaseRequestHandler): '''ftp服務端''' def handle(self): try: self.conn = self.request while True: login_info = self.conn.recv(1024).decode() # 接收客戶端發的的賬號密碼信息 result = self.authenticat(login_info) status_code = result[0] self.conn.sendall(status_code.encode()) if status_code == "400": continue self.user_db = result[1] #當前登錄用戶信息 self.current_path = self.user_db["homepath"] #用戶當前目錄 self.home_path = self.user_db["homepath"] #用戶宿主目錄 while True: command = self.conn.recv(1024).decode() command_str = command.split()[0] if hasattr(self,command_str): func = getattr(self,command_str) func(command) else:self.conn.sendall("401".encode()) except ConnectionResetError as e: self.conn.close() print(e) def authenticat(self,login_info): '''認證用戶''' auth = auth_user.User_operation() # 創建認證實例 result = auth.authentication(login_info) # 認證用戶 if result:return "200",result else:return "400",result def get(self,command): '''下載文件''' if len(command.split()) > 1: filename = command.split()[1] file_path = self.current_path + r"\%s"%filename if os.path.isfile(file_path): #文件是否存在 self.conn.sendall("201".encode()) #命令可執行 file_size = os.stat(file_path).st_size # 文件總大小 status_code = self.conn.recv(1024).decode() # 客戶端存在此文件 if status_code == "403": self.conn.sendall("000".encode()) #系統交互 has_send_size = self.conn.recv(1024).decode() has_send_size = int(has_send_size) # 客戶端文件不完整可續傳 if has_send_size < file_size: self.conn.sendall("205".encode()) file_size -= has_send_size #續傳文件大小 response = self.conn.recv(1024) # 等待響應 # 客戶端文件完整不可續傳、不提供下載 else: self.conn.sendall("405".encode()) return # 客戶端不存在此文件 elif status_code == "402": has_send_size = 0 with open(file_path,"rb") as file: self.conn.sendall(str(file_size).encode()) #發送文件大小 response = self.conn.recv(1024) #等待響應 file.seek(has_send_size) m = hashlib.md5() for line in file: m.update(line) self.conn.sendall(line) self.conn.sendall(m.hexdigest().encode()) #發送文件md5值 else:self.conn.sendall("402".encode()) else:self.conn.sendall("401".encode()) def put(self,command): '''上傳文件''' filename = command.split()[1] file_path = self.current_path + r"\%s" % filename self.conn.sendall("000".encode()) #發送確認 file_size = self.conn.recv(1024).decode() # 文件大小 file_size = int(file_size) limit_size = self.user_db["limitsize"] #磁盤額度 used_size = self.__getdirsize(self.home_path) #已用空間大小 if limit_size >= file_size+used_size: self.conn.sendall("202".encode()) with open(file_path, "wb") as file: # 開始接收 revice_size = 0 m = hashlib.md5() while revice_size < file_size: minus_size = file_size - revice_size if minus_size > 1024: size = 1024 else: size = minus_size data = self.conn.recv(size) revice_size += len(data) file.write(data) m.update(data) new_file_md5 = m.hexdigest() # 生成新文件的md5值 server_file_md5 = self.conn.recv(1024).decode() if new_file_md5 == server_file_md5: # md5值一致 self.conn.sendall("203".encode()) else:self.conn.sendall("404".encode()) def dir(self,command): '''查看當前目錄下的文件''' if len(command.split()) == 1: self.conn.sendall("201".encode()) response = self.conn.recv(1024) send_data = os.popen("dir %s"%self.current_path) self.conn.sendall(send_data.read().encode()) else:self.conn.sendall("401".encode()) def pwd(self,command): '''查看當前用戶路徑''' if len(command.split()) == 1: self.conn.sendall("201".encode()) response = self.conn.recv(1024) send_data = self.current_path self.conn.sendall(send_data.encode()) else:self.conn.sendall("401".encode()) def mkdir(self,command): '''創建目錄''' if len(command.split()) > 1: dir_name = command.split()[1] #目錄名 dir_path = self.current_path + r"\%s"%dir_name #目錄路徑 if not os.path.isdir(dir_path): #目錄不存在 self.conn.sendall("201".encode()) response = self.conn.recv(1024) os.popen("mkdir %s"%dir_path) else:self.conn.sendall("403".encode()) else:self.conn.sendall("401".encode()) def cd(self,command): '''切換目錄''' if len(command.split()) > 1: dir_name = command.split()[1] #目錄名 dir_path = self.current_path + r"\%s" %dir_name #目錄路徑 user_home_path = settings.HOME_PATH + r"\%s"%self.user_db["username"] #宿主目錄 if dir_name == ".." and len(self.current_path)>len(user_home_path): self.conn.sendall("201".encode()) response = self.conn.recv(1024) self.current_path = os.path.dirname(self.current_path) #返回上一級目錄 elif os.path.isdir(dir_path) : self.conn.sendall("201".encode()) response = self.conn.recv(1024) if dir_name != "." and dir_name != "..": self.current_path += r"\%s"%dir_name #切換目錄 else:self.conn.sendall("402".encode()) else:self.conn.sendall("401".encode()) def __getdirsize(self,home_path): '''統計目錄空間大小''' size = 0 for root, dirs, files in os.walk(home_path): size += sum([getsize(join(root, name)) for name in files]) return size
效果圖:
下載:
續傳: