原文链接:http://www.itnose.net/detail/6642756.html
作业:开发一个支持多用户在线的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(" 文件具有一致性") 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(" 文件具有一致性") 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( " %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
效果图:
下载:
续传: