作业:开发支持多用户在线FTP程序

时间:2022-08-29 12:10:35

作业:开发支持多用户在线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      主程序:

# Author:Brace Li
import socketserver, json, os, sys, shutil
import hashlib
# load parents directory into environment
base_dir = os.path.dirname(os.path.dirname(os.path.abspath("__file__")))
sys.path.append(base_dir)
from core import comman_func
class MyTCPHandler(socketserver.BaseRequestHandler):
    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"))
    def check_dir(self, *args, **kwargs):
        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"))
    def ls(self, *args, **kwargs):
        current_dir = args[0]["current_dir"]
        data = os.listdir(current_dir)
        self.request.send(json.dumps(data).encode("utf-8"))
    def mkdir(self, *args, **kwargs):
        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"))
    def rm(self, *args, **kwargs):
        rm_dir = comman_func.path_combine(args[0]["current_dir"], args[0]["rm_dir"])
        print(rm_dir)
        if os.path.isdir(rm_dir):
            shutil.rmtree(rm_dir)
        else:
            os.remove(rm_dir)
        data = [True, ]
        self.request.send(json.dumps(data).encode("utf-8"))
    def put(self, *args, **kwargs):
        """
        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_name = comman_func.path_combine(args[0]["file_dir"], args[0]["file_name"])
        file_size = args[0]["file_size"]
        balance_size = user_data["max_size"] - user_data["used_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 balance_size > file_size:
            if response_data["file_status"]:
                res_confirm = json.loads(self.request.recv(1024).strip().decode())
                if not res_confirm["overridden"]:
                    return True
            receive_size = 0
            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)
    def get(self, *args, **kwargs):
        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
        self.request.send(json.dumps(file_info).encode("utf-8"))
        if 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"))
 
    def handle(self):
        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
if __name__ == "__main__":
    host, port = "0.0.0.0", 8888
    server = socketserver.ThreadingTCPServer((host, port), MyTCPHandler)
    server.serve_forever()
 
 
 
2.  comman_func.py        #方法函数
 
# Author:Brace Li
import os, json, platform
base_dir = os.path.dirname(os.path.dirname(os.path.abspath("__file__")))
def get_os_type():
    """
    get os type
    :return: W for Windows, L for Linux
    """
    ostype = platform.system()
    if ostype == 'Windows':
        return "W"
    else:
        return "L"
def get_path(user, flag):
    """
    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
def get_data(user_name):
    """
    get data from file
    :param user:
    :return:
    """
    file = get_path(user_name, "user")
    with open(file, "r") as fs:
        return json.load(fs)
def write_data(user_name, data):
    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
def calculate_size(user_name):
    """
    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
 
3.   brace.json  数据库文件
{"user_name": "brace", "user_password": "123456", "max_size": 104857600, "used_size": 70701937, "status": true}
 
 
客户端程序目录:
├── bin
│   ├── 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
 
1.    ftp_client.py  主程序;
# Author:Brace Li
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)
from core import comman_func
class FtpClient(object):
    """
    Ftp Client
    Just for user to operate ftp by ftp client
    """
    def __init__(self):
        self.client = socket.socket()
    def cmd_help(self, *args, **kwargs):
        msg = """
        some thing for help
        ls
        pwd
        cd .. or cd path
        mkdir dirname
        rm file_name
        get file_name
        put file_name
        """
        print(msg)
    def connection(self, ip, port):
        self.client.connect((ip, port))
    def auth(self):
        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
            user_info = {
                "action": "auth",
                "username": user_name,
                "userpassword": user_password
            }
            self.client.send(json.dumps(user_info).encode("utf-8"))
            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次,系统退出.....!")
    def cmd_pwd(self, *args, **kwargs):
        """
        show current directory
        :param args:
        :param kwargs:
        :return:
        """
        print(self.current_dir.split("ftp_server")[1])
    def cmd_cd(self, *args, **kwargs):
        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("目录不存在.....")
    def cmd_ls(self, *args, **kwargs):
        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)
    def cmd_mkdir(self,*args, **kwargs):
        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
    def cmd_rm(self, *args, **kwargs):
        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
    def cmd_put(self, *args, **kwargs):
        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())
                if server_response["balance_size"] > file_size:
                    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()
                    step = int(file_size / 80)
                    x = 0
                    total = 0
                    f = open(file_name, "rb")
                    for line in f:
                        m.update(line)
                        self.client.send(line)
                        total += len(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])
    def cmd_get(self, *args, **kwargs):
        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()
                step = int(server_response["file_size"]/80)
                x = 0
                while receive_size < server_response["file_size"]:
                    line = self.client.recv(4096)
                    m.update(line)
                    f.write(line)
                    receive_size += len(line)
                    if receive_size > step * x:
                        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 = FtpClient()
#ftp.connection("127.0.0.1", 8888)
ftp.connection("192.168.137.11", 8888)
auth_data = ftp.auth()
ftp.interactive(auth_data)
 
 
2.   comman_func.py    主要方法函数:
# Author:Brace Li
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)
def get_os_type():
    """
    get os type
    :return: W for Windows, L for Linux
    """
    ostype = platform.system()
    if ostype == 'Windows':
        return "W"
    else:
        return "L"
def clear_screen():
    if get_os_type() == "W":
        x = os.system("cls")
    else:
        x = os.system("clear")
    return x
def get_validation_code(n):
    """
    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))
    return check_code
def input_check(prompt_msg, check_range, flag):
    """
    校验输入正确性, 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 flag == "T":
            if data.isdigit():
                data = int(data)
            else:
                show_error("data's type is invalid !")
                continue
        if check_range:
            if data in check_range:
                break
            else:
                show_error("Invalid input!")
                continue
        else:
            break
    return data
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