作业需求
要求:
- 用户加密认证 1
- 允许同时多用户登录 1
- 每个用户有自己的家目录 ,且只能访问自己的家目录 1
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录 1
- 允许用户查看当前目录下文件 1
- 允许上传和下载文件,保证文件一致性 1
- 文件传输过程中显示进度条 1
- 附加功能:支持文件的断点续传---------------------未完成
作业分析
作业结构:
client:
server:
小说明:
在这个作业中呢,用到了很多知识点,比如用户加密认证、加密传送文件都用到了hashlib模块;保存用户信息,传递一些字典用到了json模块;多用户同时在线用到了socketserver模块;还有一些命令的操作,用到了os.popen()函数;还有一些有关路径的函数,什么的。。。诶,好像也没用到好多哈。。。对了还有反射,这个非常重要。其他就没什么的了,具体的分析看下面:
client端代码
import socket,hashlib,json,os ,sys
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(base_dir)
sys.path.append(base_dir) # 添加conf文件的路径\\
from conf import config
from core import put, auth, ls, cd, mkdir
httpCode = """
"200" : "服务器成功返回网页"
"404" : "请求的网页不存在"
"403" : "出现错误"
"503" : "服务不可用"
"""
class FtpClient(object):
def __init__(self):
self.client = socket.socket() # 创建一个socket客户端
self.userMessDirt = {}
def connect(self,host,port):
self.client.connect((host, port)) # 客户端和服务器端进行连接
def help(self):
msg = '''
ls
pwd
cd ../..
get filename
put filename
'''
print(msg)
def interactive(self):
self.userMessDirt = json.loads(self.client.recv(config.buffer).decode())
self.localDir = os.path.basename(self.userMessDirt["homeDir"]) # 初始地址
while True:
cmd_str = input("[%s@user %s] #" %(self.userMessDirt["userName"], self.localDir))
if len(cmd_str) ==0:continue
cmd = cmd_str.split()[0]
if hasattr(self,"cmd_%s" % cmd):
func = getattr(self, "cmd_%s" % cmd)
func(cmd_str)
else:
self.help()
def cmd_ls(self, *args):
'''客户端实现打印出该目录下的所有文件'''
ls.ls(self, *args)
def cmd_pwd(self, *args):
'''客户端实现打印出当前目录功能'''
pass
def cmd_cd(self, *args):
'''客户端实现切换目录功能'''
cd.cd(self, *args)
def cmd_get(self, *args):
'''客户端下载文件'''
pass
def cmd_put(self, *args):
'''客户端上传文件'''
put.cmd_put(self,*args)
def cmd_mkdir(self, *args):
'''创建一个空文件夹'''
mkdir.mkdir(self, *args)
def auth_login(self):
auth.auth_login(self)
def auth_register(self):
auth.auth_register(self)
def run():
info = """
1.登录
2.注册
>>:"""
auth_choice = input(info).strip()
ftp = FtpClient()
ftp.connect("localhost",9090)
if auth_choice == "1":
ftp.auth_login()
else:
ftp.auth_register()
ftp.interactive()
main
import hashlib, os, sys
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
from conf import config
def auth_login(self):
self.client.send("login".encode("utf-8")) # 通知对面我要登录了
loginErrorCount = 3
while True:
userName_input = self.client.recv(config.buffer)
userName = input(userName_input.decode()).strip()
self.client.send(userName.encode("utf-8")) # 注意这里的编码问题
httpCode = self.client.recv(config.buffer).decode()
if httpCode == "403":
print("用户名错误,请重新输入")
elif httpCode == "200":
break
while loginErrorCount:
has = hashlib.md5()
passWord_input = self.client.recv(config.buffer)
passWord = input(passWord_input.decode()).strip()
has.update(passWord.encode("utf-8")) # 使用hashlib编码必须要使用byte类型
self.client.send(has.hexdigest().encode("utf-8"))
# print("has:",has.hexdigest()) # 测试是否生成了md5
# self.client.send(self.passWord.encode("utf-8"))
http_code = self.client.recv(config.buffer).decode()
if http_code == "200":
break
elif http_code == "403":
loginErrorCount -= 1
print("密码错误,剩余\033[31;1m%s\033[0m机会" % loginErrorCount)
if loginErrorCount == 0:
sys.exit(0)
def auth_register(self):
self.client.send("register".encode("utf-8")) # 通知对面我要注册了
has = hashlib.md5()
has_1 = hashlib.md5()
userName_input = self.client.recv(config.buffer)
userName = input(userName_input.decode()).strip()
self.client.send(userName.encode("utf-8"))
while True:
passWord_input = self.client.recv(config.buffer)
passWord = input(passWord_input.decode()).strip()
has.update(passWord.encode("utf-8")) # 对第一次输入的密码加密
self.client.send(has.hexdigest().encode("utf-8"))
passWord_input_again = self.client.recv(config.buffer)
passWord_again = input(passWord_input_again.decode()).strip()
has_1.update(passWord_again.encode("utf-8")) # 对第一次输入的密码加密
self.client.send(has_1.hexdigest().encode("utf-8"))
http_code = self.client.recv(config.buffer).decode()
if http_code == "200":
break
elif http_code == "403":
print("密码错误。。。")
client.auth
import json, os
base_dir = os.path.dirname(os.path.abspath(__file__))
from conf import config
def cd(self, *args):
'''cd 中.. 是返回上层目录,/ '''
cmd_str = args[0].split()
if len(cmd_str) > 1:
cmd_path = cmd_str[1]
cmd_dirt = {
"action" : "cd",
"path" : cmd_path
}
self.client.send(json.dumps(cmd_dirt).encode("utf-8"))
self.localDir = os.path.basename(self.client.recv(config.buffer).decode())
client.cd
ps:在这里说一下,貌似真的是各种命令的实行大多一样,只要做出来了一个,其他的就能做了
import sys
def progress_bar(num_cur, total):
ratio = float(num_cur) / float(total)
percentage = int(ratio * 100)
r = '\r\n[%s%s]%d%%' % (">"*percentage, " "*(100-percentage), percentage )
sys.stdout.write(r)
sys.stdout.flush()
client.progress_bar
import os, json, hashlib, sys
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
from core import progress_bar
from conf import config
def cmd_put(self, *args):
cmd_str = args[0].split() # 提取出args中的数据,并将其按空格的形式转换成列表
if len(cmd_str) > 1:
fileName = cmd_str[1]
if os.path.isfile(fileName): # 判断这是不是个文件
fileSize = os.stat(fileName).st_size # 获得文件的大小
cmd_dirt = {
"action": "put",
"fileName": fileName,
"fileSize": fileSize,
"overridden": True
}
self.client.send(json.dumps(cmd_dirt).encode("utf-8")) # 用json将整个字典传过去
self.client.recv(config.buffer) # 等待服务器端的确认, 不用做任何处理
with open(fileName, "rb") as fr:
has = hashlib.md5()
sum_size = 0
for line in fr:
line_size = len(line)
sum_size += line_size
has.update(line)
self.client.send(has.hexdigest().encode("utf-8")) # line 本身就是字节类型
return_md5 = self.client.recv(config.buffer)
if has.hexdigest() == return_md5.decode():
self.client.send(b'200')
self.client.recv(config.buffer)
self.client.send(line)
progress_bar.progress_bar(sum_size, cmd_dirt["fileSize"])
else:
self.client.send(b'403')
print("传输错误。。。")
break
else:
print("\nfile upload success...")
else:
print(fileName, "不存在。。。")
client.put
# ls
import json, os
base_dir = os.path.dirname(os.path.abspath(__file__))
from conf import config
def ls(self, *args):
cmd_str = args[0].split()
if len(cmd_str) == 1 and cmd_str[0] == "ls":
cmd_dirt = {
"action" : "ls",
}
self.client.send(json.dumps(cmd_dirt).encode("utf-8"))
return_mess = self.client.recv(config.buffer)
print(return_mess.decode())
# mkdir
import json, os
base_dir = os.path.dirname(os.path.abspath(__file__))
from conf import config
def mkdir(self, *args):
cmd_str = args[0].split()
if len(cmd_str) > 1:
fileName = cmd_str[1]
cmd_dirt = {
"action" : "mkdir",
"fileName" : fileName,
}
self.client.send(json.dumps(cmd_dirt).encode("utf-8"))
self.client.recv(config.buffer)
ls & mkdir(pwd、get都没做)
import os,sys,logging
# 最顶端的路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
# 存放日志的目录
LOG_DIR = BASE_DIR + '/log/log.txt'
logging.basicConfig(level=logging.DEBUG, # level=loggin.INFO意思是,把日志纪录级别设置为INFO,也就是说,只有比日志是INFO或比INFO级别更高的日志才会被纪录到文件里
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename=LOG_DIR,
filemode='a+')
# 存放用户信息的目录
USER_DIR = BASE_DIR + '/date/'
# 每次发送文件的大小
buffer = 2048
# 给用户分配的内存大小
memory = 100 * 1024 * 1024
client.config
server代码
import socketserver,os,sys,json
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(base_dir)
sys.path.append(base_dir ) # 添加conf文件的路径
from conf import config
from core import put, auth, ls, cd, mkdir
class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self): # 所有的请求都是在handle 中执行的
try:
self.userMessDirt = {}
state = self.request.recv(config.buffer).decode()
if state == "login":
self.auth_login()
elif state == "register":
self.auth_register()
self.request.send(json.dumps(self.userMessDirt).encode("utf-8"))
self.current_path = os.path.join(self.userMessDirt["homeDir"])
os.chdir(self.current_path) # 将当前的目录改到self.current_path下面去,这里是指每个用户的root目录
while True:
cmd_dirt = json.loads(self.request.recv(config.buffer).decode())
action = cmd_dirt["action"]
if hasattr(self, action):
func = getattr(self, action)
func(cmd_dirt)
except ConnectionResetError as e:
print(self.userMessDirt["userName"], "断开。。。")
def ls(self, *args):
'''查看该目录下的所有文件'''
ls.ls(self, *args)
def pwd(self, *args):
'''打印当前路径'''
pass
def cd(self, *args):
'''切换目录'''
cd.cd(self,*args)
def get(self, *args):
'''服务器端 发送文件给 客户端'''
pass
def mkdir(self, *args):
'''创建一个空的文件夹'''
mkdir.mkdir(self, *args)
def put(self, *args):
'''服务器端 接收 客户端给的文件'''
put.put(self, *args)
def auth_login(self):
auth.auth_login(self)
def auth_register(self):
auth.auth_register(self)
def run():
Host , Port = "localhost" , 9090 # ip地址
server = socketserver.ThreadingTCPServer((Host,Port) , MyTCPHandler) # 每来一个请求,服务端就开启一个新的线程
server.serve_forever()
main
import os,sys,logging
# 最顶端的路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
# 存放日志的目录
LOG_DIR = BASE_DIR + '/log/log.txt'
logging.basicConfig(level=logging.DEBUG, # level=loggin.INFO意思是,把日志纪录级别设置为INFO,也就是说,只有比日志是INFO或比INFO级别更高的日志才会被纪录到文件里
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename=LOG_DIR,
filemode='a+')
# 存放用户信息的目录
USER_DIR = BASE_DIR + '/date/'
# 每次发送文件的大小
buffer = 2048
# 给用户分配的内存大小
memory = 100 * 1024 * 1024
# 家目录的路径
HOME_DIR = BASE_DIR + '\home\\'
server.config
import os, sys, json
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
from conf import config
def dirt(userName, passWord, memory, homeDir):
return {
"userName":userName,
"passWord":passWord,
"memory" : memory,
"homeDir" : homeDir
}
def auth_login(self):
while True:
self.request.send('请输入用户名:'.encode("utf-8"))
userName = self.request.recv(config.buffer)
try:
with open(config.USER_DIR + userName.decode(), "r", encoding="utf-8") as fr:
self.userMessDirt = json.load(fr) # 记住json只能dump&load一次,所以放在循环里的时候注意
self.request.send(b'200')
break
except FileNotFoundError as e:
self.request.send(b'403')
loginErrorCount = 3
while loginErrorCount:
self.request.send('请输入密码:'.encode("utf-8"))
passWord_md5 = self.request.recv(config.buffer)
# print("md5:",passWord_md5.decode())
if self.userMessDirt["passWord"] == passWord_md5.decode():
self.request.send(b"200")
break
else:
self.request.send(b"403")
loginErrorCount -= 1
if loginErrorCount == 0:
sys.exit(0)
def auth_register(self):
self.request.send('请输入用户名:'.encode("utf-8"))
userName = self.request.recv(config.buffer).decode() # 一定要记得解码解码
while True:
self.request.send('请输入密码:'.encode("utf-8"))
passWord_md5 = self.request.recv(config.buffer).decode()
self.request.send('请再次输入密码:'.encode("utf-8"))
passWord_again_md5 = self.request.recv(config.buffer).decode()
if passWord_md5 == passWord_again_md5:
self.request.send(b'200')
os.makedirs(config.HOME_DIR + userName + "\\root\\") # 为用户创建家目录
self.userMessDirt = dirt(userName, passWord_md5, config.memory, config.HOME_DIR + userName + "\\root")
with open(config.USER_DIR + userName, "w") as fw:
json.dump(self.userMessDirt, fw) # 将用户的信息存到文件中
break
else:
self.request.send(b'403')
server.auth
# cd
import os
def cd(self, *args):
path_list = self.current_path.split("\\")
cmd_str = args[0]
cmd_path = cmd_str["path"]
if cmd_path == "/":
path_list_len = len(path_list)
while True:
if path_list[path_list_len - 1] == self.userMessDirt["userName"]:
break
else:
path_list.pop()
path_list_len -= 1
elif cmd_path == "..":
path_list.pop()
else:
cmd_path_list = cmd_path.split("\\")
path_list += cmd_path_list
cmd_current_path = "\\".join(path_list)
if os.path.exists(cmd_current_path):
self.current_path = cmd_current_path
os.chdir(self.current_path)
print(self.current_path)
self.request.send(self.current_path.encode("utf-8"))
# ls
import os
def ls(self, *args):
cmd_str = args[0]
mess = os.popen("dir" ).read()
self.request.send(mess.encode("utf-8"))
# mkdir
import os
def mkdir(self, *args):
cmd_str = args[0]
fileName = cmd_str["fileName"] # 需要创建的文件名字
os.popen("mkdir %s" %fileName)
self.request.send(b'200')
cd & ls &mkdir
import os, json
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
from conf import config
def put(self, *args):
cmd_dirt = args[0]
fileName = cmd_dirt["fileName"]
fileSize = cmd_dirt["fileSize"]
overridden = cmd_dirt["overridden"]
if overridden:
fw = open(fileName, "wb")
else:
fw = open(fileName + '.new', 'wb')
self.request.send(b"200") # 确认准备好接收文件了
fileReceive = 0
while fileSize > fileReceive:
date_md5 = self.request.recv(config.buffer)
self.request.send(date_md5)
http_code = self.request.recv(config.buffer).decode()
if http_code == "200":
self.request.send(b"ok")
if (fileSize - fileReceive) > config.buffer:
date = self.request.recv(config.buffer)
else:
date = self.request.recv(fileSize - fileReceive)
fw.write(date)
fileReceive += len(date)
elif http_code == "403":
print("文件错误。。。")
break
else:
fw.close()
print(fileName, "接收完毕。。。")