本程序已上传githup,点我
作业:开发一个支持多用户在线的FTP程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server的随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
支持文件的断点续传
目录结构
ftp/
|
|-----bin/
| |-----ftpClient.py # 客户端的入口
| |-----ftpServer.py # 服务端的入口
|
|----conf/
| |-----setting.py # 一些配置信息
|
| -----core/
| |-----auth.py # 用户认证模块
| |-----socket_client.py # socket客户端主程序
| |-----socket_server.py # socket服务端主程序
| |-----upload.py # 传输数据模块
|
|------database/ # 已经存在的用户数据库文件
|-----kai.db
|-----xiaolv.db
|
|-----download/ # 用户下载文件的默认地址文件夹
|
|-----home/ # 已经创建的用户的家目录
| |-----kai/
| | -----xiaolv/
|
|-----README.py
实现过程(主要是下载上传文件过程)
举例: 客户端向服务端下载文件
1.判断客户端是否存在已下载的文件
|
|------- 是 -----》 客户端确认是否需要续传 ------》 是 --------》 服务端开始找资源—》 找到资源 ----》 开始续传
| | |-----》未找到资源----》 退出到下载界面
| |
| | -----》 否 ----》 退出到下载界面
|
|------- 否 -----》服务端开始找资源—》 找到资源 ----》 开始续传
|----》 未找到资源----》退出到下载界面
功能实现:
1.实现用户加密认证登录,目前暂不支持在交互界面添加用户,需要自己手动在database/目录下创建用户的数据库文件及添加相关的用户信息,并在home/目录下添加相关的目录
2.支持文件上传下载,支持断点续传,目前只支持文件传输,不支持目录的上传下载
3.支持目录切换及显示当前账户下的目录
http状态码:
200: 客户请求成功
202:创建的文件已经存在
400:用户名不存在,用户认证失败
403.11:密码错误
401: 命令不存在
402 :文件不存在
413:磁盘空间不够用
000: 系统交互码
----------------我是分割线--------------
接下来直接上代码
bin/
ftp客户端接口
import os,sys
path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(path)
from core import socket_client
if __name__ == "__main__":
host,port = "localhost",9901
myClient = socket_client.MySocketClient(host,port)
myClient.start()
ftp服务端接口
import sys,os
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(path)
from core import socket_server
if __name__ == "__main__":
HOST,PORT = "localhost",9901
server = socket_server.socketserver.ThreadingTCPServer((HOST,PORT),socket_server.MyTCPServer)
server.serve_forever()
conf/
setting.py
import os,sys
BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#用户家目录
HOMEPATH = os.path.join(BASEDIR,"home")
#数据库目录
DATABASE = os.path.join(BASEDIR,"database")
#分给没个用户的磁盘配额
LIMITSIZE = 20480000
#用户从服务端下载文件的默认地址
DEFAULT_PATH = "C:\\Users\\Administrator\\Desktop\\python\\ftp\\download"
core/
auth.py
import hashlib,os,json
from conf import settings
class User_auth(object):
def auth(self,account_info):
"""
#此功能是进行用户的登录信息验证,如果登录成功,那么返回用户对应的http状态码及账户信息,否则只返回http状态码
:param account_info: 用户的账户信息:用户名,密码
:return:
"""
name = account_info.split(":")[0]
pwd = account_info.split(":")[1]
pwd = self.hash(pwd.encode()) # 将用户名的密码转换成hash值
user_db_file = settings.DATABASE + r"\%s.db" %name # 也可以写成 "\\%s.db" or "/%s.db"
if os.path.isfile(user_db_file): # 输入的用户名存在
with open(user_db_file) as fr:
user_db_info = json.loads(fr.read()) # or josn.load(fr)
if pwd == user_db_info['password']:
return "200",user_db_info # 确定,客户请求成功
else:
return "403.11",None # 密码错误
else:
return "400",None # 用户名不存在,用户认证失败
def hash(self,pwd):
"""
用户的密码加密
:param self:
:param pwd: 用户密码
:return:
"""
m = hashlib.md5()
m.update(pwd)
return m.hexdigest()
socket_client.py
import sys,os,time
import socket,hashlib
from core import upload
from conf import settings
class MySocketClient(object):
def __init__(self,host,port):
self.host = host
self.port = port
self.client = socket.socket() # 创建客户端实例
def start(self):
self.client.connect((self.host,self.port)) # 连接服务端,localhost为回环地址,一般用于测试
while True:
"""客户端开始输入账户信息登录"""
name = input("\033[31m 请输入账户名: \033[0m").strip()
pwd = input("\033[31m 请输入密码: \033[0m").strip()
# name = "kai";pwd = 123456
if not name or not pwd :continue # 如果账户或密码为空请重新输入
userInfo = "%s:%s" % (name,pwd)
self.client.send(userInfo.encode()) # 向服务端发送用户信息
status_code = self.client.recv(1024).decode() # 接受来自服务端的http状态码
if status_code == "400":
print("\033[1;32m 用户名不存在,用户认证失败,请重新输入 \033[0m")
continue # 用户名不存在重新输入
if status_code == "403.11":
print("\033[1;32m 密码错误,请重新输入 033[0m")
continue
if status_code == "200":
print("\033[1;32m 登录成功\033[0m")
self.interact()
def interact(self):
"""
客户端与服务端的交互接口
:return:
"""
while True:
cmd = input("""请输入操作:
\033[1;31m
eg:
get filename # 从服务端下载文件
put filename # 从服务端上传文件
ls # 查看当前目录下的文件
cd # 目录切换
pwd # 查看当前目录
mkdir dirname # 创建目录
>>> \033[0m""").strip()
cmd_action = cmd.split()[0]
if hasattr(self,cmd_action):
func = getattr(self,cmd_action)
func(cmd)
else:
print("输入命令不存在")
def put(self,cmd):
"""
客户端上传文件
:param self:
:param cmd: 用户的操作命令 eg: put filename
:return:
"""
lst = cmd.split() # 命令分割,此次只允许一次上传一个文件
if len(lst) == 2:
filename = lst[1]
if os.path.isfile(filename):
self.client.send(cmd.encode()) # 向服务端发送上传文件命令
status_code = self.client.recv(1024).decode()
total_size = os.stat(filename).st_size
self.client.send(str(total_size).encode())
status_code = self.client.recv(1024).decode() # 接收来自服务端的http状态码,判断账户是否空间充足,文件是否存在
if status_code == "202":
while True:
affirm_msg = input("创建的文件已经存在,请确认是否需要续传:1:续传 2:不续传:")
if affirm_msg == "1":
print("开始续传")
self.client.send("000".encode()) # 发送交互码,避免粘包,服务端此时需要2次连续send
has_send_size = self.client.recv(1024).decode() # 已经发送给服务端的文件大小
send_md5 = upload.Breakpoint().transfer(filename, int(has_send_size), total_size, self.client) # 进行文件的传输,返回此次发送数据的md5
recv_md5 = self.client.recv(1024).decode() # 服务端接收到此次发送数据的md5
# print("\nsend",send_md5)
# print("recv",recv_md5)
if send_md5 == recv_md5 :
print("发送数据成功")
break
else:
print("发送数据不成功,请重新发送")
break
elif affirm_msg == "2":
print("不续传")
break
else:
print("输入的命令不存在")
continue
elif status_code == "402":
print("文件不存在") # 文件不存在
send_md5 = upload.Breakpoint().transfer(filename, 0, total_size,self.client) # 进行文件的传输,返回此次发送数据的md5
recv_md5 = self.client.recv(1024).decode() # 服务端接收到此次发送数据的md5
print("\nsend md5", send_md5)
print("recv md5",recv_md5)
if send_md5 == recv_md5 : print("发送数据成功")
else : print("发送数据不成功,请重新发送")
else:
print("用户磁盘空间不够")
else:print("发送的文件不存在")
else: print("401 error,命令不正确,一次只能上传一个文件")
def get(self,cmd):
lst = cmd.split()
if len(lst) == 2:
filename = lst[1]
fileName = filename.split("\\")[-1]
self.default_file = os.path.join(settings.DEFAULT_PATH,fileName)
if os.path.isfile(self.default_file): # 用户下载的文件在默认地址下是否存在
while True:
affirm_msg = input("创建的文件已经存在,请确认是否需要续传:1:续传 2:不续传:")
if affirm_msg == "1":
self.get_file(cmd)
elif affirm_msg == "2":
break
else:
print("命令不正确")
else:
self.get_file(cmd,exist="no")
else: print("401 error,命令不正确,一次只能下载一个文件")
def get_file(self,cmd,exist="yes"):
"""
因为get方法从客户端下载文件,文件存在续传和文件不存在直接下载写法一样,故提取
:param cmd:
:param exist: 判断客户端文件是否存在
:return:
"""
self.client.send(cmd.encode())
status_code = self.client.recv(1024).decode()
if status_code == "206":
print("需要下载的文件在客户端存在,在服务端也存在,开始续传" if exist == "yes" else "需要下载的文件在服务端存在,在客户端不存在,开始下载")
has_recvd_size = os.stat(self.default_file).st_size if exist == "yes" else 0
self.client.send(str(has_recvd_size).encode())
total_size = self.client.recv(1024).decode()
self.client.send("000".encode()) # 系统交互,防止粘包
recvd_size = 0
m = hashlib.md5()
with open(self.default_file, "ab") as fa:
remain_size = int(total_size) - has_recvd_size
while recvd_size != remain_size: # 循环接收来自服务端的文件数据
data = self.client.recv(1024)
m.update(data)
recvd_size += len(data)
fa.write(data)
all_recvd_size = has_recvd_size + recvd_size
upload.Breakpoint().progress_bar(all_recvd_size,int(total_size))
self.client.send("000".encode()) # 粘包
send_md5 = self.client.recv(1024).decode() # 服务端发送数据的md5
if has_recvd_size == int(total_size):
print("文件大小一致,无需下载")
self.interact() # 如果按这种逻辑(先判断客户端文件是否存在,在判断该文件的大小是否一致)写,只能调用interact()选项才能返回
if send_md5 == m.hexdigest():
print("接收文件成功")
else:
print("接收文件不成功")
else:
print("需要下载的文件不存在,无法续传")
def mkdir(self,cmd):
self.client.send(cmd.encode())
status_code = self.client.recv(1024).decode()
if status_code == "403":
print("创建的目录存在")
elif status_code == "401":
print("输入的命令不存在")
def pwd(self,cmd):
self.client.send(cmd.encode())
cur_path = self.client.recv(1024).decode()
print(cur_path)
def cd(self,cmd):
"""
:param cmd: eg cd dirname / cd .. /
:return:
"""
self.client.send(cmd.encode())
status_code = self.client.recv(1024).decode()
if status_code == "402":
print("创建的目录不存在")
elif status_code == "400":
print("命令不正确,格式 cd .. / cd dirname")
elif status_code == "403":
print("没有上层目录了")
else:
pass
def ls(self,cmd):
self.client.send(cmd.encode())
dirs = self.client.recv(1024).decode()
print(dirs)
if __name__ == "__main__":
host,port = "localhost",9998
myClient = MySocketClient(host,port)
myClient.start()
socket_server.py
import socketserver
import hashlib,os
from core import upload
from core import auth
from conf import settings
#socketserver
class MyTCPServer(socketserver.BaseRequestHandler):
def handle(self):
try:
while True:
self.data = self.request.recv(1024).decode() # 接受来自客户端的账号的登录信息
auth_result = auth.User_auth().auth(self.data) # 进行用户验证
status_code = auth_result[0]
if status_code == "400" or status_code == "403.11":
self.request.send(status_code.encode()) # 用户名不存在或密码错误
continue
self.request.send(status_code.encode()) # 用户认证成功
self.user_db_info = auth_result[1] # 获取用户的数据库信息
self.home_path = self.user_db_info['homepath']
self.current_path = self.user_db_info['homepath'] # 登陆成功后立即定义一个“当前路径”变量,供后面创建目录,切换目录使用
while True:
self.cmd = self.request.recv(1024).decode() # 接收客户端的上传或下载命令
self.cmd_action = self.cmd.split()[0]
if hasattr(self,self.cmd_action):
func = getattr(self,self.cmd_action)
func(self.cmd)
else:
self.request.send("401".encode()) # 命令不存在
except ConnectionResetError as e:
self.request.close()
print(e)
"""服务端可以提供的命令"""
def put(self,cmd):
"""
接受客户端的上传文件命令
:param self:
:param cmd: eg: put filename
:return:
"""
filename = cmd.split()[1]
fileName = filename.split("\\")[-1] # 文件的名称
home_file = os.path.join(self.current_path, fileName) # 判断当前路径下是否有上传的文件名
self.request.send("000".encode()) # 系统交互码
total_size = self.request.recv(1024).decode() # 上传文件大小
remain_size = self.accountRemainSize() # 获得账户剩余大小
if remain_size > int(total_size):
if os.path.isfile(home_file):
self.request.send("202".encode()) # 创建的文件已经存在
self.request.recv(1024) # 等待ack指令
has_recvd_size = os.stat(home_file).st_size # 已经接收的大小
self.request.send(str(has_recvd_size).encode())
else :
has_recvd_size = 0
self.request.send("402".encode()) # 创建的文件不存在
###开始接受客户端的数据
recvd_size = 0
m = hashlib.md5()
with open(home_file,"ab") as fa:
while recvd_size != int(total_size) - has_recvd_size: # 循环接受来自的文件数据
data = self.request.recv(1024)
m.update(data)
recvd_size += len(data)
fa.write(data)
self.request.send(m.hexdigest().encode()) # 发送接收到的文件的md5
else:
self.request.send("413".encode()) # 磁盘空间不够用
def get(self,cmd):
"""
接受客户端的上传文件命令
:param self:
:param cmd: eg: get filename
:return:
"""
filename = cmd.split()[1]
fileName = filename.split("\\")[-1] # 文件的名称
home_file = os.path.join(self.home_path, fileName) # 用于判断家目录下是否有下载的文件名
if os.path.isfile(home_file): # 这里不用判断if status_code == "205" or status_code == "402": ,因为如果接收到客户端的状态码,服务端一定要先去找文件是否存在
total_size = os.stat(home_file).st_size
self.request.send("206".encode()) # 服务端有客户需要下载的文件
has_send_size = self.request.recv(1024).decode()
self.request.send(str(total_size).encode())
self.request.recv(1024) # 粘包
send_md5 = upload.Breakpoint().transfer(home_file, int(has_send_size), total_size, self.request)
self.request.recv(1024) # 粘包
self.request.send(send_md5.encode())
else:
self.request.send("402".encode()) # 服务端文件不存在
def accountRemainSize(self):
"""
统计登录用户可用目录大小
:return:
"""
account_path = self.user_db_info["homepath"]
print("homepath:",account_path,)
limitsize = self.user_db_info['limitsize']
used_size = 0
for root,dirs,files in os.walk(account_path):
used_size += sum([ os.path.getsize(os.path.join(root,filename)) for filename in files])
return limitsize - used_size
def mkdir(self,cmd):
"""
创建目录,支持创建级联目录
:param cmd:
:return:
"""
dir = cmd.split()
dir_path = os.path.join(self.current_path,dir[-1])
if len(dir) == 2:
if not os.path.isdir(dir_path):
try:
os.mkdir(dir_path)
except FileNotFoundError as e:
os.makedirs(dir_path) #创建级联目录
self.request.send("200".encode())
else:
self.request.send("403".encode()) # 创建目录存在
else:
self.request.send("400".encode())
def pwd(self,cmd):
self.request.send(self.current_path.encode())
def cd(self,cmd):
dir = cmd.split()
if len(dir) == 2:
if dir[-1] == "..": # 只能让用户在自己目录下操作
if len(self.current_path) > len(self.home_path):
self.current_path = os.path.dirname(self.current_path)
self.request.send("200".encode())
else:
self.request.send("403".encode()) # 请求被拒绝,没有上层目录了
elif os.path.isdir(self.current_path + "\\" + dir[-1]):
self.current_path = self.current_path + "\\" + dir[-1]
self.request.send("200".encode())
else:
self.request.send("402".encode())
else:
self.request.send("400".encode())
def ls(self,cmd):
dirs = os.listdir(self.current_path)
self.request.send(str(dirs).encode())
upload.py
import hashlib,sys,time
class Breakpoint(object):
# 本模块确认用户上传或下载的文件是否存在,如果存在是否需要断点续传
def transfer(self,filename,has_send_size,total_size,conn):
"""
进行续传
:param filename:
:param has_send_size: 已经发送的文件大小
:param total_size: 需要传输文件总大小
:param conn: 客户端和服务端进行数据交换的接口
:return:
"""
with open(filename,'rb') as fr:
fr.seek(has_send_size) # 定位到续传的位置
print("has_send",has_send_size,"total",total_size)
m = hashlib.md5()
if has_send_size == total_size:
self.progress_bar(has_send_size, total_size)
for line in fr:
conn.send(line)
m.update(line)
has_send_size += len(line)
# self.progress_bar(has_send_size,total_size)
return m.hexdigest()
def progress_bar(self,has_send_size,total_size):
bar_width = 50 # 进度条长度
process = has_send_size / total_size
send_bar = int(process * bar_width + 0.5) # 发送的数据占到的进度条长度,四舍五入取整
sys.stdout.write("#" * send_bar + "=" * (bar_width - send_bar) +"\r" ) # 注意点:貌似只能这么写才能达到要求
sys.stdout.write("\r%.2f%%: %s%s"% (process * 100,"#" * send_bar,"=" * (bar_width - send_bar))) # 注意点:在pycharm中要加\r\n,用sublime只要\r否则换行
sys.stdout.flush()
-------------我还是分割线----------------------
最后效果图
转载请说明出处
http://my.csdn.net/