作业:开发支持多用户在线FTP程序
要求:
用户加密认证;
允许同时多用户登陆;
每个用户有自己的家目录,且只能访问自己的家目录;
对用户进行磁盘配额,每个用户的可用空间不同;
允许用户在ftp server上随意切换目录;
允许用户查看当前目录下的文件;
允许上传和现在文件,保证文件一致性;
文件传输过程中显示进度条;
附加功能:支持文件断点续传;
服务器端系统程序目录
├── bin
│ ├── ftp_server.py
│ └── __init__.py
├── conf
│ ├── __init__.py
│ └── user_data
│ ├── brace.json
│ ├── create_user.py
│ ├── __init__.py
│ └── kitty.json
├── core
│ ├── comman_func.py
│ ├── __init__.py
│ └── __pycache__
│ ├── comman_func.cpython-36.pyc
│ └── __init__.cpython-36.pyc
├── home
│ ├── brace
│ ├── __init__.py
│ └── kitty
├── __init__.py
└── logs
└── __init__.py
1. ftp_server.py 主程序:
import socketserver, json, os, sys, shutil
import hashlib
base_dir = os.path.dirname(os.path.dirname(os.path.abspath("__file__")))
sys.path.append(base_dir)
from core import comman_func
def auth(self, *args, **kwargs):
"""
for user authentication;
:param user_info:
:return:
"""
user_info = args[0]
confirm_data = {
"confirm_status": False,
"user_name": user_info["username"],
"home_path": "",
"max_size": "",
"used_size": ""
}
print("Client: ", user_info)
user_file = comman_func.get_path(user_info["username"], "user")
user_dir = comman_func.get_path(user_info["username"], "data")
print("file: ", user_file)
print("dir: ", user_dir)
if os.path.isfile(user_file):
server_user = comman_func.get_data(user_info["username"])
print("Server: ", server_user)
if user_info["userpassword"] == server_user["user_password"] and server_user["status"]:
confirm_data = {
"confirm_status": True,
"user_name": user_info["username"],
"home_path": user_dir,
"max_size": server_user["max_size"],
"used_size": server_user["used_size"]
}
self.user = user_info["username"]
self.request.send(json.dumps(confirm_data).encode("utf-8"))
new_dir = comman_func.path_combine(args[0]["current_dir"], args[0]["new_dir"])
print("args:", args[0])
print(new_dir)
if os.path.isdir(new_dir):
confirm_data = {"confirm_status": True}
else:
confirm_data = {"confirm_status": False}
self.request.send(json.dumps(confirm_data).encode("utf-8"))
current_dir = args[0]["current_dir"]
data = os.listdir(current_dir)
self.request.send(json.dumps(data).encode("utf-8"))
new_dir = comman_func.path_combine(args[0]["current_dir"], args[0]["new_dir"])
print(new_dir)
os.makedirs(new_dir)
data = [True, ]
self.request.send(json.dumps(data).encode("utf-8"))
rm_dir = comman_func.path_combine(args[0]["current_dir"], args[0]["rm_dir"])
print(rm_dir)
shutil.rmtree(rm_dir)
else:
os.remove(rm_dir)
self.request.send(json.dumps(data).encode("utf-8"))
"""
put file to ftp server
:param cmd_action:
:return:
"""
total_size = comman_func.calculate_size(self.user)
user_data = comman_func.get_data(self.user)
user_data["used_size"] = total_size
file_size = args[0]["file_size"]
response_data = {"balance_size": balance_size}
if os.path.isfile(file_name):
response_data["file_status"] = True
else:
response_data["file_status"] = False
self.request.send(json.dumps(response_data).encode("utf-8"))
if response_data["file_status"]:
res_confirm = json.loads(self.request.recv(1024).strip().decode())
if not res_confirm["overridden"]:
return True
m = hashlib.md5()
f = open(file_name, "wb")
while receive_size < file_size:
data = self.request.recv(4096)
f.write(data)
m.update(data)
receive_size += len(data)
else:
result = {"server_md5": m.hexdigest()}
f.close()
self.request.send(json.dumps(result).encode("utf-8"))
result = json.loads(self.request.recv(1024).strip().decode())
if result["status"]:
user_data["used_size"] += file_size
else:
os.remove(file_name)
comman_func.write_data(self.user, user_data)
server_file = os.path.join(args[0]["file_dir"], args[0]["file_name"])
file_info = {}
if os.path.isfile(server_file):
file_info["file_size"] = os.path.getsize(server_file)
else:
file_info["file_size"] = 0
r = self.request.recv(1024)
f = open(server_file, "rb")
m = hashlib.md5()
for line in f:
m.update(line)
self.request.send(line)
else:
server.md5 = m.hexdigest()
r = self.request.recv(1024)
self.request.send(json.dumps({"server_md5": server.md5}).encode("utf-8"))
while True:
try:
data = self.request.recv(1024).strip()
cmd_info = json.loads(data.decode())
action = cmd_info["action"]
if hasattr(self, action):
func = getattr(self, action)
func(cmd_info)
except (ConnectionResetError, json.decoder.JSONDecodeError, TypeError) as e:
print("客户端断开了!", e)
break
host, port = "0.0.0.0", 8888
server = socketserver.ThreadingTCPServer((host, port), MyTCPHandler)
server.serve_forever()
import os, json, platform
base_dir = os.path.dirname(os.path.dirname(os.path.abspath("__file__")))
"""
get os type
:return: W for Windows, L for Linux
"""
ostype = platform.system()
if ostype == 'Windows':
return "W"
else:
return "L"
"""
for get user's data and its home directory
:param user: for user name
:param flag: user or data, user for user info, data for home directory
:return: return the full path of file or directory'
"""
ostype = get_os_type()
if ostype == "L":
user_file = "%s/conf/user_data/%s.json" % (base_dir, user)
user_dir = "%s/home/%s" % (base_dir, user)
else:
user_file = r"%s\conf\user_data\%s.json" % (base_dir, user)
user_dir = "%s\home\%s" % (base_dir, user)
if flag == "user":
return user_file
else:
return user_dir
"""
get data from file
:param user:
:return:
"""
file = get_path(user_name, "user")
with open(file, "r") as fs:
return json.load(fs)
file = get_path(user_name, "user")
with open(file, "w") as fs:
json.dump(data, fs)
def path_combine(dir_name, file_name):
"""
only for combine directory and file
:param dir_name:directory name
:param file_name:file name
:return:
"""
os_type = get_os_type()
if os_type == "W":
dir_data = r"%s\%s" % (dir_name, file_name.replace("/", "\\"))
else:
dir_data = r"%s/%s" % (dir_name, file_name.replace("\\", "/"))
return dir_data
"""
calculate total fisk space size
:param user_name:
:return:
"""
data_dir = get_path(user_name, "data")
total_size = 0
for root, dirs, files in os.walk(data_dir):
total_size += sum([os.path.getsize(os.path.join(root, filename)) for filename in files])
return total_size
│ ├── ftp_client.py
│ └── __init__.py
├── conf
│ └── __init__.py
├── core
│ ├── comman_func.py
│ ├── __init__.py
│ └── __pycache__
│ ├── comman_func.cpython-36.pyc
│ └── __init__.cpython-36.pyc
├── __init__.py
└── logs
└── __init__.py
import socket, os, json, sys
import hashlib
# load parents directory into environment
base_dir = os.path.dirname(os.path.dirname(os.path.abspath("__file__")))
sys.path.append(base_dir)
"""
Ftp Client
Just for user to operate ftp by ftp client
"""
def __init__(self):
self.client = socket.socket()
msg = """
some thing for help
ls
pwd
cd .. or cd path
mkdir dirname
rm file_name
get file_name
put file_name
"""
print(msg)
self.client.connect((ip, port))
err_count = 0
while err_count < 3:
while True:
user_name = comman_func.input_check("用户名称:", (), "F")
user_password = comman_func.input_check("用户密码:", (), "F")
check_code = comman_func.get_validation_code(6)
print("验证码:%s" %check_code)
user_code = comman_func.input_check("输入验证码:", (), "F")
if user_code != check_code:
comman_func.show_error("验证码不匹配!")
continue
else:
break
"action": "auth",
"username": user_name,
"userpassword": user_password
}
server_confirm = json.loads(self.client.recv(1024).decode())
if server_confirm["confirm_status"]:
print(server_confirm)
# self.current_dir = server_confirm["home_path"].split("ftp_server")[1]
self.current_dir = server_confirm["home_path"]
self.base_dir = server_confirm["home_path"]
print(self.current_dir)
print(self.base_dir)
return server_confirm
else:
err_count += 1
comman_func.show_error("账号密码错误,重新输入!")
else:
sys.exit("错误超过3次,系统退出.....!")
"""
show current directory
:param args:
:param kwargs:
:return:
"""
print(self.current_dir.split("ftp_server")[1])
cmd_split = args[0].split()
if len(cmd_split) > 1:
if cmd_split[1] == "..":
if len(self.base_dir) < len(self.current_dir):
self.current_dir = os.path.dirname(self.current_dir)
else:
cmd_info = {
"action": "check_dir",
"current_dir": self.current_dir,
"new_dir": cmd_split[1]
}
self.client.send(json.dumps(cmd_info).encode("utf-8"))
server_confirm = json.loads(self.client.recv(1024).decode())
if server_confirm["confirm_status"]:
dir_data = comman_func.path_combine(self.current_dir, cmd_split[1])
self.current_dir = dir_data
else:
comman_func.show_error("目录不存在.....")
cmd_info = {
"action": "ls",
"current_dir": self.current_dir
}
self.client.send(json.dumps(cmd_info).encode("utf-8"))
server_confirm = json.loads(self.client.recv(1024).decode())
for n in server_confirm:
print(n)
cmd_split = args[0].split()
if len(cmd_split) > 1:
cmd_info = {
"action": "mkdir",
"current_dir": self.current_dir,
"new_dir": cmd_split[1]
}
self.client.send(json.dumps(cmd_info).encode("utf-8"))
server_confirm = json.loads(self.client.recv(1024).decode())
if server_confirm: pass
cmd_split = args[0].split()
if len(cmd_split) > 1:
cmd_info = {
"action": "rm",
"current_dir": self.current_dir,
"rm_dir": cmd_split[1]
}
self.client.send(json.dumps(cmd_info).encode("utf-8"))
server_confirm = json.loads(self.client.recv(1024).decode())
if server_confirm: pass
cmd_split = args[0].split()
if len(cmd_split) > 1:
file_name = comman_func.path_combine(os.getcwd(), cmd_split[1])
print(file_name)
if os.path.isfile(file_name):
file_size = os.stat(file_name).st_size
file_info = {
"action": "put",
"file_dir": self.current_dir,
"file_name": cmd_split[1],
"file_size": file_size
}
print(file_info)
self.client.send(json.dumps(file_info).encode("utf-8"))
server_response = json.loads(self.client.recv(1024).decode())
confirm_data = {"overridden": False}
if server_response["file_status"]:
confirm_flag = comman_func.input_check("文件存在,是否覆盖(Y/N):", ("Y", "N"), "F")
if confirm_flag == "Y": confirm_data["overridden"] = True
self.client.send(json.dumps(confirm_data).encode("utf-8"))
if confirm_flag == "N":
return True
m = hashlib.md5()
x = 0
total = 0
f = open(file_name, "rb")
for line in f:
m.update(line)
self.client.send(line)
if len(line) < step:
if total > step * x:
sys.stdout.write(">")
sys.stdout.flush()
x += 1
else:
n = int(len(line)/step)
for i in range(n):
sys.stdout.write(">")
sys.stdout.flush()
x += 1
else:
client_md5 = m.hexdigest()
server_md5 = json.loads(self.client.recv(1024).decode())["server_md5"]
#print("client_md5:", client_md5)
#print("server_md5:", server_md5)
if client_md5 == server_md5:
result = {"status": True}
print("\nupload pass ....!")
else:
result = {"status": False}
print("\nupload fail ....!")
self.client.send(json.dumps(result).encode("utf-8"))
else:
comman_func.show_error("空间不足........")
else:
comman_func.show_error("%s is not exist ....." %cmd_split[1])
cmd_split = args[0].split()
if len(cmd_split) > 1:
file_info = {
"action": "get",
"file_dir": self.current_dir,
"file_name": cmd_split[1],
}
self.client.send(json.dumps(file_info).encode("utf-8"))
server_response = json.loads(self.client.recv(1024).decode())
if server_response["file_size"] > 0:
self.client.send(b"OK")
file_name = comman_func.path_combine(os.getcwd(), cmd_split[1])
f = open(file_name, "wb")
receive_size = 0
m = hashlib.md5()
x = 0
line = self.client.recv(4096)
m.update(line)
f.write(line)
receive_size += len(line)
sys.stdout.write(">")
sys.stdout.flush()
x += 1
else:
self.client.send(b"OK")
client_md5 = m.hexdigest()
server_md5 = json.loads(self.client.recv(1024).decode())["server_md5"]
if server_md5 == client_md5:
print("\nget %s pass" % cmd_split[1])
else:
print("\nget %s fail" % cmd_split[1])
else:
comman_func.show_error("文件 [ %s ] 不存在!")
def interactive(self, auth_data):
comman_func.clear_screen()
print("*" * 80)
print("*%s*" %" ".center(78))
print("*%s*" % ">>>>>>> FTP System <<<<<<<".center(78))
print("*%s*" % " ".center(78))
temp_str = "登陆用户:%s, 磁盘空间:%s MB, 剩余空间:%s MB" % (auth_data["user_name"], auth_data["max_size"]/(1024*1024), round((auth_data["max_size"]-auth_data["used_size"])/(1024*1024), 2))
print("*%s*" % temp_str.center(66))
print("*%s*" % " ".center(78))
print("*" * 80)
while True:
cmd_str = comman_func.input_check(">>>", (), "F")
cmd = cmd_str.split()[0]
if cmd == "exit":
sys.exit()
elif hasattr(self, "cmd_%s" % cmd):
func = getattr(self, "cmd_%s" % cmd)
func(cmd_str)
else:
self.cmd_help()
#ftp.connection("127.0.0.1", 8888)
ftp.connection("192.168.137.11", 8888)
auth_data = ftp.auth()
ftp.interactive(auth_data)
import random, sys, platform, os
def show_error(info):
"""
Show error message in process
:param info: prompt message
:return: None, Only print error message in screen.
"""
print("Error:%s" % info)
"""
get os type
:return: W for Windows, L for Linux
"""
ostype = platform.system()
if ostype == 'Windows':
return "W"
else:
return "L"
if get_os_type() == "W":
x = os.system("cls")
else:
x = os.system("clear")
return x
"""
parameter n is for loading the length of code
"""
check_code = ""
for i in range(n):
current = random.randrange(0, n)
if i == current:
check_code += chr(random.randint(65, 90))
else:
check_code += str(random.randint(0, 9))
"""
校验输入正确性, eg: input_check("select action:", (1, 2), "T")
:param prompt_msg: 输入提示信息
:param check_range: check range, if it is none, use "()" instead
:param flag: number or string, T is for int number, F is anything
:return: the value of input.
"""
while True:
data = input(prompt_msg).strip()
if not data:
show_error("input is null!")
continue
if data.isdigit():
data = int(data)
else:
show_error("data's type is invalid !")
continue
if data in check_range:
break
else:
show_error("Invalid input!")
continue
else:
break
return data
"""
only for combine directory and file
:param dir_name:directory name
:param file_name:file name
:return:
"""
os_type = get_os_type()
if os_type == "W":
dir_data = r"%s\%s" % (dir_name, file_name.replace("/", "\\"))
else:
dir_data = r"%s/%s" % (dir_name, file_name.replace("\\", "/"))
return dir_data