作业:开发一个支持多用户在线的FTP程序
要求:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传(仅下载)
ftp客户端:
1 import socket 2 import os,sys 3 import hashlib 4 5 class Myclient(): 6 '''ftp客户端''' 7 def __init__(self,ip_port): 8 self.ip_port = ip_port 9 10 def connect(self): 11 '''连接服务器''' 12 self.client = socket.socket() 13 self.client.connect(self.ip_port) 14 15 def start(self): 16 '''程序开始''' 17 self.connect() 18 while True: 19 username = input("输入用户名:").strip() 20 password = input("输入密码:").strip() 21 login_info = ("%s:%s" %(username, password)) 22 self.client.sendall(login_info.encode()) #发送用户密码信息 23 status_code = self.client.recv(1024).decode() #返回状态码 24 if status_code == "400": 25 print("[%s]用户密码认证错误"%status_code) 26 continue 27 else:print("[%s]用户密码认证成功"%status_code) 28 self.interactive() 29 30 def interactive(self): 31 '''开始交互''' 32 while True: 33 command = input("->>").strip() 34 if not command:continue 35 #if command == "exit":break 36 command_str = command.split()[0] 37 if hasattr(self,command_str): # 执行命令 38 func = getattr(self,command_str) 39 func(command) 40 else:print("[%s]命令不存在"%401) 41 42 def get(self,command): 43 '''下载文件''' 44 self.client.sendall(command.encode()) #发送要执行的命令 45 status_code = self.client.recv(1024).decode() 46 if status_code == "201": #命令可执行 47 filename = command.split()[1] 48 49 # 文件名存在,判断是否续传 50 if os.path.isfile(filename): 51 revice_size = os.stat(filename).st_size #文件已接收大小 52 self.client.sendall("403".encode()) 53 response = self.client.recv(1024) 54 self.client.sendall(str(revice_size).encode()) #发送已接收文件大小 55 status_code = self.client.recv(1024).decode() 56 57 # 文件大小不一致,续传 58 if status_code == "205": 59 print("继续上次上传位置进行续传") 60 self.client.sendall("000".encode()) 61 62 # 文件大小一致,不续传,不下载 63 elif status_code == "405": 64 print("文件已经存在,大小一致") 65 return 66 67 # 文件不存在 68 else: 69 self.client.sendall("402".encode()) 70 revice_size = 0 71 72 file_size = self.client.recv(1024).decode() #文件大小 73 file_size = int(file_size) 74 self.client.sendall("000".encode()) 75 76 with open(filename,"ab") as file: #开始接收 77 #file_size 为文件总大小 78 file_size +=revice_size 79 m = hashlib.md5() 80 while revice_size < file_size: 81 minus_size = file_size - revice_size 82 if minus_size > 1024: 83 size = 1024 84 else: 85 size = minus_size 86 data = self.client.recv(size) 87 revice_size += len(data) 88 file.write(data) 89 m.update(data) 90 self.__progress(revice_size,file_size,"下载中") #进度条 91 new_file_md5 = m.hexdigest() #生成新文件的md5值 92 server_file_md5 = self.client.recv(1024).decode() 93 if new_file_md5 == server_file_md5: #md5值一致 94 print("\n文件具有一致性") 95 else:print("[%s] Error!"%(status_code)) 96 97 def put(self,command): 98 '''上传文件''' 99 if len(command.split()) > 1: 100 filename = command.split()[1] 101 #file_path = self.current_path + r"\%s"%filename 102 if os.path.isfile(filename): #文件是否存在 103 self.client.sendall(command.encode()) #发送要执行的命令 104 response = self.client.recv(1024) #收到ack确认 105 106 file_size = os.stat(filename).st_size # 文件大小 107 self.client.sendall(str(file_size).encode()) # 发送文件大小 108 status_code = self.client.recv(1024).decode() # 等待响应,返回状态码 109 if status_code == "202": 110 with open(filename,"rb") as file: 111 m = hashlib.md5() 112 for line in file: 113 m.update(line) 114 send_size = file.tell() 115 self.client.sendall(line) 116 self.__progress(send_size, file_size, "上传中") # 进度条 117 self.client.sendall(m.hexdigest().encode()) #发送文件md5值 118 status_code = self.client.recv(1024).decode() #返回状态码 119 if status_code == "203": 120 print("\n文件具有一致性") 121 else:print("[%s] Error!"%(status_code)) 122 else: 123 print("[402] Error") 124 else: print("[401] Error") 125 126 def dir(self,command): 127 '''查看当前目录下的文件''' 128 self.__universal_method_data(command) 129 pass 130 131 def pwd(self,command): 132 '''查看当前用户路径''' 133 self.__universal_method_data(command) 134 pass 135 136 def mkdir(self,command): 137 '''创建目录''' 138 self.__universal_method_none(command) 139 pass 140 141 def cd(self,command): 142 '''切换目录''' 143 self.__universal_method_none(command) 144 pass 145 146 def __progress(self, trans_size, file_size,mode): 147 ''' 148 显示进度条 149 trans_size: 已经传输的数据大小 150 file_size: 文件的总大小 151 mode: 模式 152 ''' 153 bar_length = 100 #进度条长度 154 percent = float(trans_size) / float(file_size) 155 hashes = '=' * int(percent * bar_length) #进度条显示的数量长度百分比 156 spaces = ' ' * (bar_length - len(hashes)) #定义空格的数量=总长度-显示长度 157 sys.stdout.write( 158 "\r%s:%.2fM/%.2fM %d%% [%s]"%(mode,trans_size/1048576,file_size/1048576,percent*100,hashes+spaces)) 159 sys.stdout.flush() 160 161 def __universal_method_none(self,command): 162 '''通用方法,无data输出''' 163 self.client.sendall(command.encode()) # 发送要执行的命令 164 status_code = self.client.recv(1024).decode() 165 if status_code == "201": # 命令可执行 166 self.client.sendall("000".encode()) # 系统交互 167 else: 168 print("[%s] Error!" % (status_code)) 169 170 def __universal_method_data(self,command): 171 '''通用方法,有data输出''' 172 self.client.sendall(command.encode()) #发送要执行的命令 173 status_code = self.client.recv(1024).decode() 174 if status_code == "201": #命令可执行 175 self.client.sendall("000".encode()) #系统交互 176 data = self.client.recv(1024).decode() 177 print(data) 178 else:print("[%s] Error!" % (status_code)) 179 180 if __name__ == "__main__": 181 ip_port =("127.0.0.1",9999) #服务端ip、端口 182 client = Myclient(ip_port) #创建客户端实例 183 client.start() #开始连接
ftp 服务端:
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()
conf配置文件
1 import os,sys 2 3 #程序主目录文件 4 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 #添加环境变量 6 sys.path.insert(0,BASE_DIR) 7 8 #数据库目录 9 DATABASE = os.path.join(BASE_DIR,"database") 10 11 #用户属主目录 12 HOME_PATH = os.path.join(BASE_DIR,"home") 13 14 #用户字典 15 USERS_PWD = {"alex":"123456","lzl":"8888","eric":"6666"} 16 17 #磁盘配额 10M 18 LIMIT_SIZE = 10240000 19 20 #ftp服务端口 21 IP_PORT = ("0.0.0.0",9999)
database用户数据库(系统初始化自动生成)
modules模块dd
1 import json 2 import sys,os 3 import hashlib 4 from conf import settings 5 6 class User_operation(): 7 '''对登录信息进行认证,登录成功返回用户名,失败返回None''' 8 def authentication(self,login_info): 9 list = login_info.split(":") #对信息进行分割 10 login_name = list[0] 11 login_passwd = self.hash(list[1]) 12 DB_FILE = settings.DATABASE + r"\%s.db"%login_name 13 if os.path.isfile(DB_FILE): 14 user_database = self.cat_database(DB_FILE) #用户数据库信息 15 if login_name == user_database["username"]: 16 if login_passwd == user_database["password"]: 17 return user_database 18 19 def cat_database(self,DB_FILE): 20 #获取数据库信息 21 with open(DB_FILE,"r") as file: 22 data = json.loads(file.read()) 23 return data 24 25 def hash(self,passwd): 26 '''对密码进行md5加密''' 27 m = hashlib.md5() 28 m.update(passwd.encode("utf-8")) 29 return m.hexdigest()
1 import socketserver 2 import sys,os 3 import hashlib 4 from os.path import join, getsize 5 from conf import settings 6 from modules import auth_user 7 8 9 class Myserver(socketserver.BaseRequestHandler): 10 '''ftp服务端''' 11 def handle(self): 12 try: 13 self.conn = self.request 14 while True: 15 login_info = self.conn.recv(1024).decode() # 接收客户端发的的账号密码信息 16 result = self.authenticat(login_info) 17 status_code = result[0] 18 self.conn.sendall(status_code.encode()) 19 if status_code == "400": 20 continue 21 self.user_db = result[1] #当前登录用户信息 22 self.current_path = self.user_db["homepath"] #用户当前目录 23 self.home_path = self.user_db["homepath"] #用户宿主目录 24 25 while True: 26 command = self.conn.recv(1024).decode() 27 command_str = command.split()[0] 28 if hasattr(self,command_str): 29 func = getattr(self,command_str) 30 func(command) 31 else:self.conn.sendall("401".encode()) 32 except ConnectionResetError as e: 33 self.conn.close() 34 print(e) 35 36 def authenticat(self,login_info): 37 '''认证用户''' 38 auth = auth_user.User_operation() # 创建认证实例 39 result = auth.authentication(login_info) # 认证用户 40 if result:return "200",result 41 else:return "400",result 42 43 def get(self,command): 44 '''下载文件''' 45 if len(command.split()) > 1: 46 filename = command.split()[1] 47 file_path = self.current_path + r"\%s"%filename 48 if os.path.isfile(file_path): #文件是否存在 49 self.conn.sendall("201".encode()) #命令可执行 50 file_size = os.stat(file_path).st_size # 文件总大小 51 status_code = self.conn.recv(1024).decode() 52 53 # 客户端存在此文件 54 if status_code == "403": 55 self.conn.sendall("000".encode()) #系统交互 56 has_send_size = self.conn.recv(1024).decode() 57 has_send_size = int(has_send_size) 58 # 客户端文件不完整可续传 59 if has_send_size < file_size: 60 self.conn.sendall("205".encode()) 61 file_size -= has_send_size #续传文件大小 62 response = self.conn.recv(1024) # 等待响应 63 64 # 客户端文件完整不可续传、不提供下载 65 else: 66 self.conn.sendall("405".encode()) 67 return 68 # 客户端不存在此文件 69 elif status_code == "402": 70 has_send_size = 0 71 72 with open(file_path,"rb") as file: 73 self.conn.sendall(str(file_size).encode()) #发送文件大小 74 response = self.conn.recv(1024) #等待响应 75 file.seek(has_send_size) 76 m = hashlib.md5() 77 for line in file: 78 m.update(line) 79 self.conn.sendall(line) 80 self.conn.sendall(m.hexdigest().encode()) #发送文件md5值 81 else:self.conn.sendall("402".encode()) 82 else:self.conn.sendall("401".encode()) 83 84 def put(self,command): 85 '''上传文件''' 86 filename = command.split()[1] 87 file_path = self.current_path + r"\%s" % filename 88 self.conn.sendall("000".encode()) #发送确认 89 file_size = self.conn.recv(1024).decode() # 文件大小 90 file_size = int(file_size) 91 limit_size = self.user_db["limitsize"] #磁盘额度 92 used_size = self.__getdirsize(self.home_path) #已用空间大小 93 if limit_size >= file_size+used_size: 94 self.conn.sendall("202".encode()) 95 with open(file_path, "wb") as file: # 开始接收 96 revice_size = 0 97 m = hashlib.md5() 98 while revice_size < file_size: 99 minus_size = file_size - revice_size 100 if minus_size > 1024: 101 size = 1024 102 else: 103 size = minus_size 104 data = self.conn.recv(size) 105 revice_size += len(data) 106 file.write(data) 107 m.update(data) 108 new_file_md5 = m.hexdigest() # 生成新文件的md5值 109 server_file_md5 = self.conn.recv(1024).decode() 110 if new_file_md5 == server_file_md5: # md5值一致 111 self.conn.sendall("203".encode()) 112 else:self.conn.sendall("404".encode()) 113 114 115 def dir(self,command): 116 '''查看当前目录下的文件''' 117 if len(command.split()) == 1: 118 self.conn.sendall("201".encode()) 119 response = self.conn.recv(1024) 120 send_data = os.popen("dir %s"%self.current_path) 121 self.conn.sendall(send_data.read().encode()) 122 else:self.conn.sendall("401".encode()) 123 124 def pwd(self,command): 125 '''查看当前用户路径''' 126 if len(command.split()) == 1: 127 self.conn.sendall("201".encode()) 128 response = self.conn.recv(1024) 129 send_data = self.current_path 130 self.conn.sendall(send_data.encode()) 131 else:self.conn.sendall("401".encode()) 132 133 def mkdir(self,command): 134 '''创建目录''' 135 if len(command.split()) > 1: 136 dir_name = command.split()[1] #目录名 137 dir_path = self.current_path + r"\%s"%dir_name #目录路径 138 if not os.path.isdir(dir_path): #目录不存在 139 self.conn.sendall("201".encode()) 140 response = self.conn.recv(1024) 141 os.popen("mkdir %s"%dir_path) 142 else:self.conn.sendall("403".encode()) 143 else:self.conn.sendall("401".encode()) 144 145 def cd(self,command): 146 '''切换目录''' 147 if len(command.split()) > 1: 148 dir_name = command.split()[1] #目录名 149 dir_path = self.current_path + r"\%s" %dir_name #目录路径 150 user_home_path = settings.HOME_PATH + r"\%s"%self.user_db["username"] #宿主目录 151 if dir_name == ".." and len(self.current_path)>len(user_home_path): 152 self.conn.sendall("201".encode()) 153 response = self.conn.recv(1024) 154 self.current_path = os.path.dirname(self.current_path) #返回上一级目录 155 elif os.path.isdir(dir_path) : 156 self.conn.sendall("201".encode()) 157 response = self.conn.recv(1024) 158 if dir_name != "." and dir_name != "..": 159 self.current_path += r"\%s"%dir_name #切换目录 160 161 else:self.conn.sendall("402".encode()) 162 else:self.conn.sendall("401".encode()) 163 164 def __getdirsize(self,home_path): 165 '''统计目录空间大小''' 166 size = 0 167 for root, dirs, files in os.walk(home_path): 168 size += sum([getsize(join(root, name)) for name in files]) 169 return size