python项目开发:ftp server开发
两年后再看之前写的代码才知道写的有多难看(虽然几乎是照着别人做的),因此特地重写了一下代码,新版代码地址:https://github.com/night-cruise/ftp_app
程序要求:
1.用户加密认证 (对用户名密码进行MD5验证)
2.允许同时多用户登陆 (使用socket server方法,为每个用户都创建一个信息文件)
3.每个用户有自己的家目录,且只能访问自己的家目录(每个用户都建立一个单独的目录)
4.对用户进行磁盘配额,每个用户的可用空间不同(用户信息字典中添加磁盘配额这一参数)
5.允许用户在ftp server 上随意切换目录
6.允许用户查看当前目录下的文件
7.允许上传和下载文件,保证文件的一致性(MD5验证)
8.文件传输过程中显示进度条
9.附加功能:支持文件的断点续传
分析:
数据存储:为每个用户都建立一个文件存储用户名、密码(MD5)、磁盘配额大小,为每一个用户都建立一个家目录
业务逻辑功能:可切换目录、创建目录、可查看目录下的文件、可上传、下载文件(支持断点续传、显示进度条)
开发过程中遇到的问题:
客户端和服务端需要互相传递消息,这样编写一端的程序的时候需要知道另一端程序的结构,
于是要先建立好两端之间大致传递的消息,编写好一端后,再编写另一端
程序目录框架:
程序代码:
README:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
程序框架: |--ftp |--ftpclinet |--bin |--start #程序入口 |--core |--ftpclient #程序主要逻辑 |--ftpserver |--bin |--start #程序入口 |--core |--main #程序主要逻辑 |--conf |--settings #定义database、homes的路径 |--homes |--database |--modules |--authendencate #登陆模块 |--socket_server #socket通信模块 |--README ## 状态码 400 用户认证失败 401 命令不正确 402 文件不存在 403 创建文件已经存在 404 磁盘空间不够 405 不续传 200 用户认证成功 201 命令可以执行 202 磁盘空间够用 203 文件具有一致性 205 续传 000 系统交互码 切换目录:cd .. 返回上一级目录 cd dirname 进入dirname 注:用户登录后默认进入家目录,只可在家目录下随意切换 创建目录:mkdir dirname 查看当前路径: pwd 查看当前路径下的文件名和目录名: dir 初始化程序是自动创建用户: {"东云博士":"111111","马孔多":"","江南":"333333","白早":"444444"}
ftpclient:
bin目录下的start:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# -*- coding:utf-8 -*- #!/user/bin/env.python #Author:Mr Wu import os,sys BASE_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_dir) from core import ftpclient if __name__ == '__main__': obj = ftpclient.ftp_client(("127.0.0.1",9999)) obj.start()
core目录下的ftpclient:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# -*- coding:utf-8 -*- #!/user/bin/env.python #Author:Mr Wu import os,hashlib,socket,json,sys,time class ftp_client(object): '''ftp client客户端''' 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: user_name = input("input your name>>>:") user_password = input("input your password>>>:") user_msg = "%s:%s"%(user_name,user_password) self.client.send(user_msg.encode("utf-8")) status_code = self.client.recv(1024).decode() if status_code == "400": print("[400]认证失败!") continue elif status_code == "200": print("[200]认证成功!") #self.user_name = user_name self.interaction() def interaction(self): '''开始交互''' while True: command = input("input command>>>:") if not command: continue get_command = command.split()[0] if hasattr(self,get_command): func = getattr(self,get_command) func(command) else: print("[401]命令错误!") def get(self,command): '''下载文件''' self.client.send(command.encode()) status_code = self.client.recv(1024).decode() if status_code == "401": print("[401]命令错误") elif status_code == "201": filename = command.split()[1] if os.path.isfile(filename): self.client.send("403".encode()) self.client.recv(1024) received_size = os.stat(filename).st_size self.client.send(str(received_size).encode()) status_code = self.client.recv(1024).decode() if status_code == "205": print("[205]可续传!") self.client.send("000".encode()) elif status_code == "405": print("[405]文件完整,不可续传!") return else: self.client.send("402".encode()) received_size = 0 print("[205]可下载!") m = hashlib.md5() f = open(filename,"wb") file_total_size = int(self.client.recv(1024).decode()) self.client.send("000".encode()) f.seek(received_size) while received_size < file_total_size: last_size = file_total_size - received_size if last_size < 1024: size = last_size else: size = 1024 data = self.client.recv(size) m.update(data) f.write(data) self.__progress_bar(received_size,file_total_size,"下载中") received_size += len(data) f.close() received_md5 = self.client.recv(1024).decode() md5 = m.hexdigest() if received_md5 == md5: print("[203]文件具有一致性!") else: print("[403]文件不一致!") def put(self,command): '''上传文件''' self.client.send(command.encode("utf-8")) status_code = self.client.recv(1024).decode() if status_code == "201": filename = command.split()[1] if os.path.isfile(filename): file_total_size = os.stat(filename).st_size self.client.send(str(file_total_size).encode()) status_code = self.client.recv(1024).decode() if status_code == "205": self.client.send("000".encode()) status_code = self.client.recv(1024).decode() if status_code == "202": print("[205]可上传") elif status_code == "404": print("[404]目录空间不足~") return elif status_code == "405": print("[405]文件完整,不续传~") return else: print("[402]文件不存在!") return self.client.send("000".encode()) received_size = int(self.client.recv(1024).decode()) m = hashlib.md5() f = open(filename,"rb") f.seek(received_size) for line in f: self.client.send(line) m.update(line) received_size += len(line) self.__progress_bar(received_size,file_total_size,"上传中") f.close() self.client.send(m.hexdigest().encode()) status_code = self.client.recv(1024).decode() if status_code == "203": print("[203]文件具有一致性") else: print("[403]传输文件不一致!") elif status_code == "401": print("[401]命令错误!") def cd(self,command): '''切换目录''' self.client.send(command.encode("utf-8")) status_code = self.client.recv(1024).decode() if status_code == "201": print("[201]命令执行成功") elif status_code == "401": print("[401]命令错误") def dir(self,command): '''查看当前目录下的文件''' self.client.send(command.encode("utf-8")) status_code = self.client.recv(1024).decode() if status_code == "201": self.client.send("000".encode()) data = self.client.recv(1024).decode() print(data) elif status_code == "401": print("[401]命令错误!") def mkdir(self,command): '''创建目录''' self.client.send(command.encode()) status_code = self.client.recv(1024).decode() if status_code == "401": print("[401]命令错误") elif status_code == "403": print("[403]创建目录已存在!") elif status_code == "201": print("[201]创建目录成功!") def pwd(self,command): '''查看当前用户路径''' self.client.send(command.encode()) status_code = self.client.recv(1024).decode() if status_code == "201": self.client.send("000".encode()) data = self.client.recv(1024).decode() print(data) elif status_code == "401": print("[401]命令错误") def __progress_bar(self,received_size,file_total_size,mode): width = 50 #进度条长度 percent = received_size/file_total_size #进度条百分比 use_num = int(percent*width) #已使用的进度条长度 space_num = int(width - use_num) #未使用的进度条长度 percent = percent*100 sys.stdout.write("%s[%s%s]%d%%\r"%(mode,use_num*"#",space_num*" ",percent)) sys.stdout.flush() return
ftpserver:
bin目录下的start:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_dir) from core import ftpserver if __name__ == '__main__': ftpserver.My_ftp_server()
core目录下的ftpserver:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# -*- coding:utf-8 -*- #!/user/bin/env.python #Author:Mr Wu import sys,os,json,hashlib,socketserver from conf import settings from modules import socket_server class My_ftp_server(object): '''ftp初始化''' def __init__(self): self.interaction() def interaction(self): self.create_user() self.create_dir() server = socketserver.ThreadingTCPServer(settings.Ip_port,socket_server.ftp_server) server.serve_forever() def create_user(self): '''创建用户信息文件''' for key in settings.users_dict: user_database = {} name = key password = settings.users_dict[name] password = self.hash(password) user_database["name"] = name user_database["password"] = password user_database["limit_size"] = settings.Disk_quota #默认磁盘配额 user_database["home_dir"] = settings.homes_dir + r"\%s"%name #用户家目录 user_db = settings.database_dir + r"\%s.db"%name if not os.path.isfile(user_db): f = open(user_db,"w") json.dump(user_database,f) f.close() def create_dir(self): '''创建用户家目录''' for key in settings.users_dict: name = key user_dir = settings.homes_dir + r"\%s"%name if not os.path.isdir(user_dir): os.popen("mkdir %s"%user_dir) def hash(self,password): '''MD5加密''' m = hashlib.md5() m.update(password.encode()) return m.hexdigest()
modules目录下的authendencate:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# -*- coding:utf-8 -*- #!/user/bin/env.python #Author:Mr Wu import sys,os,json,hashlib from conf import settings class authendencate(object): '''用户登录模块''' def __init__(self,user_msg): self.user_msg = user_msg def auth_user(self): '''验证用户登陆''' msg_list = self.user_msg.split(":") user_name = msg_list[0] user_password = self.hash(msg_list[1]) user_database = self.get_user_msg(user_name) if user_database: if user_name == user_database["name"] and user_password == user_database["password"]: return user_database def get_user_msg(self,user_name): '''获取用户信息''' user_db = settings.database_dir + r"\%s.db"%user_name if os.path.isfile(user_db): f = open(user_db,"r") user_database = json.load(f) f.close() return user_database def hash(self,passwd): '''md5加密''' m = hashlib.md5() m.update(passwd.encode()) return m.hexdigest()
modules目录下的socket_server:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# -*- coding:utf-8 -*- #!/user/bin/env.python #Author:Mr Wu import sys,os,socketserver,hashlib from conf import settings from modules import authendencate class ftp_server(socketserver.BaseRequestHandler): '''ftp server端''' def handle(self): try: self.conn = self.request while True: user_msg = self.conn.recv(1024).decode() auth_result = self.auth(user_msg) status_code = auth_result[0] self.conn.send(status_code.encode("utf-8")) if status_code == "400": continue elif status_code == "200": user_database = auth_result[1] self.user_dir = user_database["home_dir"] #用户默认目录 self.home_dir = user_database["home_dir"] #用户家目录 self.limit_size = user_database["limit_size"] #用户磁盘配额 while True: command = self.conn.recv(1024).decode() get_command = command.split()[0] if hasattr(self,get_command): func = getattr(self,get_command) func(command) else: self.conn.send("401".encode("utf-8")) except ConnectionResetError as e: print(e) def get(self,command): '''下载文件''' if len(command.split()) > 1: filename = command.split()[1] file_db = self.user_dir + r"\%s"%filename if os.path.isfile(file_db): file_total_size = os.stat(file_db).st_size self.conn.send("201".encode("utf-8")) status_code = self.conn.recv(1024).decode() if status_code == "402": received_size = 0 elif status_code == "403": self.conn.send("000".encode()) received_size = int(self.conn.recv(1024).decode()) if received_size < file_total_size: self.conn.send("205".encode()) self.conn.recv(1024) else: self.conn.send("405".encode()) return f = open(file_db,"rb") m = hashlib.md5() self.conn.send(str(file_total_size).encode()) self.conn.recv(1024) f.seek(received_size) for line in f: self.conn.send(line) m.update(line) f.close() self.conn.send(m.hexdigest().encode()) else: self.conn.send("402".encode("utf-8")) else: self.conn.send("401".encode("utf-8")) def put(self,command): '''上传文件''' if len(command) > 1: self.conn.send("201".encode()) file_name = command.split()[1] file_db = self.user_dir + r"\%s"%file_name file_total_size = int(self.conn.recv(1024).decode()) if os.path.isfile(file_db): received_size = os.stat(file_db).st_size if received_size < file_total_size: self.conn.send("205".encode()) else: self.conn.send("405".encode()) return else: received_size = 0 self.conn.send("205".encode()) put_size = file_total_size - received_size status_code = self.__get_size(put_size) self.conn.recv(1024) if status_code == "202": self.conn.send("202".encode()) else: self.conn.send("404".encode()) return self.conn.recv(1024) self.conn.send(str(received_size).encode()) m = hashlib.md5() f = open(file_db,"wb") f.seek(received_size) while received_size < file_total_size: last_size = file_total_size - received_size if last_size < 1024: size = last_size else: size = 1024 data = self.conn.recv(1024) received_size += len(data) f.write(data) m.update(data) f.close() md5 = m.hexdigest() new_md5 = self.conn.recv(1024).decode() if md5 == new_md5: self.conn.send("203".encode()) else: self.conn.send("401".encode()) def cd(self,command): '''切换目录''' if len(command) > 1: dir_name = command.split()[1] dir_path = self.user_dir + r"\%s"%dir_name if dir_name == ".." : if len(self.user_dir) > len(self.home_dir): self.conn.send("201".encode()) self.user_dir = os.path.dirname(self.user_dir) else: self.conn.send("401".encode()) elif os.path.isdir(dir_path): self.user_dir = dir_path self.conn.send("201".encode()) else: self.conn.send("401".encode()) else: self.conn.send("401".encode()) def dir(self,command): '''查看当前目录下的文件''' if command == "dir": self.conn.send("201".encode()) self.conn.recv(1024) data = os.popen("dir %s"%self.user_dir) self.conn.send(data.read().encode()) else: print("401".encode()) def mkdir(self,command): '''创建目录''' if len(command) > 1: filename = command.split()[1] dir_path = self.user_dir + r"\%s"%filename if not os.path.isdir(dir_path): self.conn.send("201".encode()) os.popen("mkdir %s"%dir_path) else: self.conn.send("403".encode()) else: self.conn.send("401".encode()) def pwd(self,command): '''查看当前用户路径''' if command == "pwd": self.conn.send("201".encode()) data = self.user_dir self.conn.recv(1024) self.conn.send(data.encode()) else: self.conn.send("401".encode()) def auth(self,user_msg): '''用户登陆''' obj = authendencate.authendencate(user_msg) auth_result = obj.auth_user() if not auth_result: return "400" else: return "200",auth_result def __get_size(self,file_size): '''计算目录空间大小''' dir_size = os.stat(self.home_dir).st_size + file_size if dir_size < self.limit_size: return "202" else: return "404"
conf目录下的settings:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# -*- coding:utf-8 -*- #!/user/bin/env.python #Author:Mr Wu import os,sys BASE_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) '''用户信息的属主目录''' database_dir = os.path.join(BASE_dir,"database") '''用户目录的属主目录''' homes_dir = os.path.join(BASE_dir,"homes") '''用户信息字典''' users_dict = {"东云博士":"111111","马孔多":"","江南":"333333","白早":"444444"} '''默认磁盘配额''' Disk_quota = 102400 '''ip地址和端口''' Ip_port = ("0.0.0.0",9999)
程序运行示例:
断点续传: