FTP程序

需求:开发一个支持多用户同时在线的FTP程序

要求:
1、用户加密认证
2、允许同时多用户登录(用到并发编程的知识,选做)
3、每个用户有自己的家目录,且只能访问自己的家目录
4、对用户进行磁盘配额,每个用户的可用空间不同(选做)
5、允许用户在ftp server上随意切换目录
6、允许用户查看当前目录下的文件
7、允许上传和下载文件,并保证文件的一致性
8、文件传输过程中显示进度条
9、附加:支持文件的断点续传(选做)
开发的程序需符合PEP8开发规范,及专业的生产软件设计规范,包括目录、代码命名、功能接口等


 

client

conf\settings

import os

BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOWN_PATH = os.path.join(BASE_PATH, "download")
UP_PATH = os.path.join(BASE_PATH, "upload")


CODING = "utf-8"
MAX_PACKET_SIZE = 8192

client

import os
import sys
import socket
import struct
import json
import hashlib
import shelve

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from conf import settings


class MYClient:
    """
    ftp客户端
    """
    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM

    def __init__(self, server_address):
        self.server_address = server_address
        self.socket = socket.socket(self.address_family, self.socket_type)
        self.client_connect()
        self.username = None
        self.client_status = False
        self.terimal = None
        self.shelve_obj = shelve.open("db")
        self.server_file_path = None
        self.home_path = None

    def client_connect(self):
        """与服务器连接"""
        self.socket.connect(self.server_address)

    def read_file(self, path):
        """读取文件"""
        with open(path, "rb") as f:
            return f.read()

    def hash_md5(self, msg):
        """加密"""
        m = hashlib.md5()
        m.update(msg)
        # print(m.hexdigest())
        return m.hexdigest()

    def header(self, status, **kwargs):
        """制作、发送报头"""
        header_dic = kwargs
        header_dic["status"] = status

        header_json = json.dumps(header_dic)
        header_bytes = header_json.encode(settings.CODING)
        self.socket.send(struct.pack("i", len(header_bytes)))    # header_dic的大小传送给客户端
        self.socket.send(header_bytes)                      # header_dic数据传送给客户端

    def recv_header(self):
        """接收报头"""
        header = self.socket.recv(4)      # 接收报头
        header_size = struct.unpack("i", header)[0]
        header_bytes = self.socket.recv(header_size)     # 接收报头信息
        header_json = header_bytes.decode(settings.CODING)
        header_dic = json.loads(header_json)
        return header_dic

    def get(self, data):
        """
        下载
        :param data: 指令、文件名、用户名
        :return:
        """
        if len(data) == 2:
            username = data[1]
            filename = input("请输入上传文件名:")
        else:
            username = data[2]
            filename = data[1]
        msg = {"action_type": "get", "filename": filename, "username": username, "s_file_path": self.server_file_path}
        self.socket.send(json.dumps(msg).encode(settings.CODING))

        header_dic = self.recv_header()
        if header_dic["status"] == "200":
            self.socket.send(header_dic["status"].encode(settings.CODING))
            c_file_path = os.path.join(os.path.join(settings.DOWN_PATH, msg["username"]), msg["filename"])
            self.server_file_path = header_dic["s_file_path"]
            long = str(len(self.shelve_obj.keys())+1)
            while True:
                if long in self.shelve_obj.keys():
                    long = str(int(long) + 1)
                else:
                    break
            if os.path.isfile(c_file_path):
                print("%s文件已存在" % filename)
                self.socket.send("000".encode(settings.CODING))
                return
            else:
                self.socket.send("999".encode(settings.CODING))

            self.shelve_obj[long] = {
                "filename": msg["filename"]+".download",
                "s_file_path": self.server_file_path+".download",
                "file_size": header_dic["file_size"]
            }

            with open("%s.download" % c_file_path, "wb")as f:     # 接收数据
                recv_size = 0
                while recv_size < header_dic["file_size"]:
                    line = self.socket.recv(settings.MAX_PACKET_SIZE)
                    f.write(line)
                    recv_size += len(line)

                    self.progress_bar(recv_size, header_dic["file_size"])

                f.close()
                os.rename("%s.download" % c_file_path, c_file_path)
                num = self.hash_md5(self.read_file(c_file_path))
                if num == header_dic["md5"]:
                    self.socket.send("999".encode(settings.CODING))
                    print("下载完成")
                    del self.shelve_obj[long]
                else:
                    self.socket.send("000".encode(settings.CODING))
                    print("文件下载出错")
        elif header_dic["status"] == "210":

            self.socket.send(header_dic["status"].encode(settings.CODING))
            print(header_dic["status_msg"])

    def resume(self):
        """
        断点续传
        :return:
        """
        if len(self.shelve_obj.keys()) == 0:
            return
        print("未传送完成文件".center(50, "-"))
        for k in self.shelve_obj.keys():
            relative_path = self.shelve_obj[k]["s_file_path"].replace(self.home_path, "")
            print("序号:%s,文件名:%s,文件大小:%s,文件地址:%s" %
                  (k, self.shelve_obj[k]["filename"], self.shelve_obj[k]["file_size"], relative_path))
        while True:
            print("请输入继续传送文件的序号,退出请输“q”")
            choice = input(">>")
            if not choice:
                continue
            elif choice == "q":
                return
            elif choice.isdigit():
                file_path = os.path.join(os.path.join(self.home_path, self.username),
                                         self.shelve_obj[choice]["s_file_path"]).rstrip(".download")
                filename = self.shelve_obj[choice]["filename"].rstrip(".download")
                complete_size = self.shelve_obj[choice]["file_size"]
                incomplete_size = os.path.getsize(
                    os.path.join(os.path.join(settings.DOWN_PATH, self.username), self.shelve_obj[choice]["filename"]))
                header_dic = {"filename": filename, "s_file_path": file_path, "incomplete_size": incomplete_size}
                client_path = os.path.join(os.path.join(settings.DOWN_PATH, self.username), filename)
                header_dic["client_path"] = client_path
                msg = {"action_type": "resume", "filename": filename, "username": self.username}

                self.socket.send(json.dumps(msg).encode(settings.CODING))
                if int(choice) > 0 and int(choice) <= len(self.shelve_obj.keys()):
                    if self.socket.recv(3).decode(settings.CODING) == "999":
                        status = "500"
                        self.header(status, **header_dic)
                        header_dic = self.recv_header()

                        with open("%s.download" % header_dic["client_path"], "ab")as f:  # 接受真实的数据
                            while incomplete_size < complete_size:
                                line = self.socket.recv(settings.MAX_PACKET_SIZE)
                                f.write(line)
                                incomplete_size += len(line)

                                self.progress_bar(incomplete_size, complete_size)
                            f.close()
                            os.rename("%s.download" % header_dic["client_path"], header_dic["client_path"])
                            num = self.hash_md5(self.read_file(header_dic["client_path"]))
                            if num == header_dic["md5"]:
                                self.socket.send("999".encode(settings.CODING))
                                print("下载完成")
                                del self.shelve_obj[choice]
                            else:
                                self.socket.send("000".encode(settings.CODING))
                                print("文件下载出错")
            else:
                print("输入错误,请重新输入!")

    def put(self, data):
        """
        上传
        :param data:
        :return:
        """
        if len(data) == 2:
            username = data[1]
            filename = input("请输入上传文件名:")
        else:
            username = data[2]
            filename = data[1]
        c_file_path = os.path.join(os.path.join(settings.UP_PATH, username), filename)
        if os.path.isfile(c_file_path):
            msg = {"action_type": "put", "filename": filename, "username": username}
            self.socket.send(json.dumps(msg).encode(settings.CODING))

            res = self.socket.recv(3).decode(settings.CODING)
            if res == "999":
                status = "200"
                file_size = os.path.getsize(c_file_path)
                header_dic = {
                    "filename": data[1],
                    "md5": self.hash_md5(self.read_file(c_file_path)),
                    "file_size": file_size,
                    "s_file_path": self.server_file_path
                }
                self.header(status, **header_dic)
                if self.socket.recv(3).decode(settings.CODING) == "000":
                    ask = input("%s文件已存在,是否覆盖?" % filename)
                    if ask == "n":
                        self.socket.send("000".encode(settings.CODING))
                        return
                    elif ask == "y":
                        self.socket.send("999".encode(settings.CODING))
                    else:
                        print("输入错误")
                        self.socket.send("000".encode(settings.CODING))
                        return
                else:
                    self.socket.send("999".encode(settings.CODING))
                header_dic = self.recv_header()
                if header_dic["status"] == "300":
                    send_size = 0
                    with open(c_file_path, "rb")as f:
                        for line in f:
                            self.socket.send(line)
                            send_size += len(line)

                            self.progress_bar(send_size, file_size)

                        f.close()
                        res = self.socket.recv(3).decode(settings.CODING)
                        if res == "999":
                            print("上传成功!")
                        else:
                            print("上传失败!")
                else:
                    print(header_dic["status_msg"])
                    return
            else:
                return
        else:
            print("文件不存在")
            return

    def progress_bar(self, recv_size, file_size):
        """
        进度条
        :param recv_size: 已接收大小
        :param file_size: 总共大小
        :return:
        """
        rate = recv_size / file_size
        rate_num = int(rate * 100)
        number = int(50 * rate)
        r = '\r[%s%s]%d%%' % ("#" * number, " " * (50 - number), rate_num,)
        print("\r {}".format(r), end=" ")

    def login(self):
        """
        用户验证
        :return:
        """
        count = 0
        while count < 3:
            username = input("请输入用户名:").strip()
            if not username:
                continue
            password = input("请输入密码:").strip()
            msg = {"action_type": "login", "username": username, "password": password}
            self.socket.send(json.dumps(msg).encode(settings.CODING))

            header_dic = self.recv_header()
            if header_dic["status"] == "100":
                self.home_path = header_dic["home_path"]
                self.username = username
                print("登陆成功,欢迎%s" % username)
                self.terimal = "%s" % username
                return True
            elif header_dic["status"] == "110":
                print("用户名或密码错误")
                count += 1
                # return False

    def run(self):
        """
        与服务器的所有交互
        :return:
        """
        if not self.username:
            self.client_status = self.login()
        if self.client_status:
            self.resume()
            while True:
                print("输入help可看帮助")
                user_input = input("%s,请输入命令:" % self.terimal).strip()
                if not user_input:
                    continue
                data = user_input.split()
                cmd = data[0]
                data.append(self.username)      # [get,1.mp3,username]
                # print(data)
                if hasattr(self, cmd):
                    func = getattr(self, cmd)
                    func(data)
                else:
                    print("输入有误,请重新输入")

    def help(self, data):
        """
        帮助
        :param data:
        :return:
        """
        msg = {"get 文件名": "下载文件",
               "put 文件名": "上传文件",
               "dir": "查看当前路径",
               "cd 目标路径": "切换目录"
               }
        for k in msg:
            print("指令:“%s”,功能:%s" % (k, msg[k]))

    def dir(self, data):
        """
        查看当前目录
        :param data:
        :return:
        """
        msg = {"action_type": "dir",  "username": data[1]}
        self.socket.send(json.dumps(msg).encode(settings.CODING))

        msg_dic = self.recv_header()
        if msg_dic["status"] == "200":
            print(msg_dic["msg"])
        else:
            print(msg_dic["status_msg"])

    def cd(self, data):
        """
        切换目录
        :param data:
        :return:
        """
        if len(data) == 2:
            target = input("请输入切换到的目录:")
        else:
            target = data[1]
        msg = {"action_type": "cd", "target": target}
        self.socket.send(json.dumps(msg).encode(settings.CODING))

        msg_dic = self.recv_header()
        if msg_dic["status"] == "400":
            print("目录切换成功")
            self.server_file_path = msg_dic["path"]
            # print(msg_dic["path"])
            self.terimal = msg_dic["current_path"]
        else:
            print(msg_dic["status_msg"])


client = MYClient(('127.0.0.1', 8080))
client.run()

  


 

server

bin\server

import os
import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)


if __name__ == "__main__":
    from core import main
    from conf import settings
    ftp_server = main.MYServer(settings.server_address)
    ftp_server.run()

conf\settings

import os


BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CONF_PATH = os.path.join(BASE_PATH, "conf")
SERVER_PATH = os.path.join(BASE_PATH, "core")
HOME_PATH = os.path.join(BASE_PATH, "home")

SERVER_ADDRESS = ("127.0.0.1", 8080)


REQUEST_QUEUEST_SIZE = 5
MAX_PACKET_SIZE = 8192
CODING = "utf-8"
ALLOW_REUSE_ADDRESS = False

用户初始化信息

import configparser
import hashlib

config = configparser.ConfigParser()
config["alex"] = {}
password = "abc123"
n = hashlib.md5()
n.update(password.encode("utf-8"))
config["alex"]["name"] = "alex"
config["alex"]["password"] = n.hexdigest()
config["alex"]["quato"] = "5"

config["egon"] = {}
password = "efg456"
m = hashlib.md5()
m.update(password.encode("utf-8"))
config["egon"]["name"] = "egon"
config["egon"]["password"] = m.hexdigest()
config["alex"]["quato"] = "3"

config["jack"] = {}
password = "hij789"
l = hashlib.md5()
l.update(password.encode("utf-8"))
config["jack"]["name"] = "jack"
config["jack"]["password"] = l.hexdigest()
config["alex"]["quato"] = "2.5"

with open("config.ini", "w")as f:
    config.write(f)

core\main

import socket
import os
import json
import hashlib
import configparser
import struct
import subprocess
from conf import settings


class MYServer(object):
    """
    ftp服务端
    """
    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    STATUS = {
        "100": "用户验证成功!",
        "110": "用户名或密码错误!",
        "200": "文件存在",
        "210": "文件不存在",
        "300": "存储空间足够",
        "310": "存储空间不足",
        "400": "路径存在",
        "410": "路径不存在",
        "500": "文件续传",
        "999": "文件传输成功",
        "000": "文件传输失败"
    }

    def __init__(self, server_address):
        self.server_address = server_address
        self.socket = socket.socket(self.address_family, self.socket_type)
        self.server_bind()
        self.server_listen()
        self.user_current_dir = ""
        self.file_size = 0

    def server_bind(self):
        """
        绑定
        :return:
        """
        if settings.ALLOW_REUSE_ADDRESS:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)

    def server_listen(self):
        """
        监听
        :return:
        """
        self.socket.listen(settings.REQUEST_QUEUEST_SIZE)

    def run(self):
        """
        建立连接,启动socket server
        :return:
        """
        while True:
            self.conn, self.client_addr = self.socket.accept()
            try:
                self.manage()
            except ConnectionRefusedError:
                print("客户端发生错误,断开连接")
                self.socket.close()

    def manage(self):
        """
        处理与用户的所有指令交互
        :return:
        """
        while True:
            data = self.conn.recv(settings.MAX_PACKET_SIZE)    # 接收客户端指令
            if not data:
                print("连接断开... ")
                del self.conn, self.client_addr
                break

            cmd_data = json.loads(data.decode(settings.CODING))
            action_type = cmd_data["action_type"]
            if action_type:
                if hasattr(self, action_type):
                    func = getattr(self, action_type)
                    func(cmd_data)
            else:
                print("未接收到有效指令")

    def header(self, status, **kwargs):
        """
        制作、发送报头
        :param status: 状态码
        :param kwargs:
        :return:
        """
        header_dic = kwargs
        header_dic["status"] = status
        header_dic["status_msg"] = self.STATUS[status]
        header_dic["home_path"] = settings.HOME_PATH
        # print(header_dic)
        header_json = json.dumps(header_dic)
        header_bytes = header_json.encode(settings.CODING)
        self.conn.send(struct.pack("i", len(header_bytes)))    # header_dic的大小传送给客户端
        self.conn.send(header_bytes)                      # header_dic数据传送给客户端

    def recv_header(self):
        """
        接收报头
        :return:
        """
        header = self.conn.recv(4)      # 接收报头
        header_size = struct.unpack("i", header)[0]
        header_bytes = self.conn.recv(header_size)     # 接收报头信息
        header_json = header_bytes.decode(settings.CODING)
        header_dic = json.loads(header_json)
        return header_dic

    def read_info(self):
        """
        加载所有账户信息
        :return:
        """
        conf = configparser.ConfigParser()
        conf.read(r"%s/%s" % (settings.CONF_PATH, "config.ini"))
        return conf

    def login(self, data):
        """
        用户登陆验证
        :param data: 指令、用户名、密码
        :return:
        """
        username = data["username"]
        password = data["password"]
        conf = self.read_info()
        psd = self.hash_md5(password.encode(settings.CODING))
        if username in conf:
            if psd == conf[username]["password"]:
                print("认证成功")
                self.header("100")
                self.user_current_dir = os.path.join(settings.HOME_PATH, username)
                return True
            else:
                self.header("110")
                print("认证失败")
                return False
        else:
            self.header("110")
            print("认证失败")
            return False

    def read_file(self, path):
        """
        打开文件
        :param path: 文件路径
        :return:
        """
        with open(path, "rb") as f:
            return f.read()

    def hash_md5(self, msg):
        """
        md5加密
        :param msg: 加密信息
        :return:
        """
        m = hashlib.md5()
        m.update(msg)
        # print(m.hexdigest())
        return m.hexdigest()

    def get(self, data):
        """
        下载
        :param data:  指令、文件名,用户名、服务器路径
        :return:
        """
        if data["s_file_path"] is None:
            file_path = os.path.join(os.path.join(settings.HOME_PATH, data["username"]), data["filename"])
        else:
            file_path = os.path.join(data["s_file_path"], data["filename"])
        if os.path.isfile(file_path):
            status = "200"
            self.file_size = os.path.getsize(file_path)
            header_dic = {
                "filename": data["filename"],
                "md5": self.hash_md5(self.read_file(file_path)),
                "file_size":  self.file_size,
                "s_file_path": file_path
            }
            self.header(status, **header_dic)
        else:
            status = "210"
            self.header(status)

        if self.conn.recv(3).decode(settings.CODING) == "200":
            if self.conn.recv(4).decode(settings.CODING) == "999":
                send_size = 0
                with open(file_path, "rb")as f:
                    for line in f:
                        self.conn.send(line)
                        send_size += len(line)

                        self.progress_bar(send_size, self.file_size)

                    f.close()
                    res = self.conn.recv(4).decode(settings.CODING)
                    if res == "999":
                        print("下载成功!")
                    else:
                        print("下载失败!")
            else:
                return
        else:
            print(self.STATUS["210"])

    def progress_bar(self, recv_size, file_size):
        """进度条
        :param recv_size: 已接收大小
        :param file_size: 总共大小
        :return:
        """
        rate = recv_size / file_size
        rate_num = int(rate * 100)
        number = int(50 * rate)
        r = '\r[%s%s]%d%%' % ("#" * number, " " * (50 - number), rate_num,)
        print("\r {}".format(r), end=" ")

    def put(self, data):
        """
        上传
        :param data: 指令、文件名,用户名
        :return:
        """
        self.conn.send("999".encode(settings.CODING))
        header_dic = self.recv_header()
        if header_dic["s_file_path"] is None:
            file_path = os.path.join(os.path.join(settings.HOME_PATH, data["username"]), data["filename"])
        else:
            file_path = os.path.join(header_dic["s_file_path"], data["filename"])
        quato = float(self.read_info()[data["username"]]["quato"]) * 1024 * 1024 * 1024
        full_size = 0
        for parent, dirs, files in os.walk(file_path):
            for file in files:
                fullname = os.path.join(parent, file)
                filesize = os.path.getsize(fullname)
                full_size += filesize
        header_dic.pop("status")
        header_dic["file_path"] = file_path
        if full_size + header_dic["file_size"] <= quato:
            status = "300"
        else:
            status = "310"
        if os.path.isfile(file_path):
            print("%s文件已存在" % data["filename"])
            self.conn.send("000".encode(settings.CODING))
        else:
            self.conn.send("999".encode(settings.CODING))
        if self.conn.recv(3).decode(settings.CODING) == "999":
            self.header(status, **header_dic)
            if status == "300":
                recv_size = 0
                with open(file_path, "wb")as f:  # 接受真实的数据
                    while recv_size < header_dic["file_size"]:
                        line = self.conn.recv(settings.MAX_PACKET_SIZE)
                        f.write(line)
                        recv_size += len(line)

                        self.progress_bar(recv_size, header_dic["file_size"])

                    f.close()
                    num = self.hash_md5(self.read_file(file_path))
                    # print(num)
                    if num == header_dic["md5"]:
                        self.conn.send("999".encode(settings.CODING))
                        print("上传成功")
                    else:
                        self.conn.send("000".encode(settings.CODING))
                        print("文件上传失败")
            else:
                print(self.STATUS[status])
        else:
            return


    def dir(self, data):
        """
        查看当前目录
        :param data: 指令、用户名
        :return:
        """
        cmd_obj = subprocess.Popen("dir %s" % self.user_current_dir, shell=True, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        stdout = cmd_obj.stdout.read()
        stderr = cmd_obj.stderr.read()
        result = stdout + stderr

        if not result:
            result = "当前目录下没有任何文件".encode("gbk")

        status = "200"
        msg = {"msg": result.decode("gbk")}
        self.header(status, **msg)

    def cd(self, data):
        """
        改变目录
        :param data: 指令,目标路径
        :return:
        """
        current_dir = os.path.abspath(os.path.join(self.user_current_dir, data["target"]))
        print(current_dir)
        if os.path.isdir(current_dir):
            if current_dir.startswith(settings.HOME_PATH):
                status = "400"
                current_path = current_dir.replace(settings.HOME_PATH, "")
                self.user_current_dir = current_dir
                msg = {"path": current_dir, "current_path": current_path}
                self.header(status, **msg)
            else:
                status = "410"
                self.header(status)
        else:
            status = "410"
            self.header(status)

    def resume(self, data):
        """
        断点续传
        :param data: 指令、文件名,用户名
        :return:
        """
        self.conn.send("999".encode(settings.CODING))
        header_dict = self.recv_header()
        incomplete_size = header_dict["incomplete_size"]
        complete_size = os.path.getsize(header_dict["s_file_path"])
        balance = complete_size - incomplete_size
        header_dict["balance"] = balance
        header_dict["md5"] = self.hash_md5(self.read_file(header_dict["s_file_path"]))
        status = "500"
        header_dict.pop("status")
        self.header(status, **header_dict)
        with open(header_dict["s_file_path"], "rb")as f:
            f.seek(incomplete_size)
            for line in f:
                self.conn.send(line)
                incomplete_size += len(line)

                self.progress_bar(incomplete_size, complete_size)

            f.close()
            res = self.conn.recv(4).decode(settings.CODING)
            if res == "999":
                print("下载成功!")
            else:
                print("下载失败!")


# server = MYServer(('127.0.0.1', 8080))
# server.run()

README

服务端入口:bin-->server.py
客户端入口:client.py
客户端默认下载到download中
客户端从upload文件夹中上传文件
用户信息存在conf-->config.ini中

  

posted @ 2018-12-11 21:15  混世妖精  阅读(393)  评论(0编辑  收藏  举报