python作业 - 支持多用户在线的FTP程序

 

 github源码:https://github.com/xieyousheng/ftp

 思路分析:

作者: xieyousheng
版本:Ftp_v1
开发环境: python3.6.4

程序介绍:

1. 用户认证
2. 多用户同时登陆
3. 每个用户有自己的家目录且只能访问自己的家目录
4. 对用户进行磁盘配额、不同用户配额可不同
5. 用户可以登陆server后,可切换目录
6. 查看当前目录下文件
7. 上传下载文件
8. 传输过程中现实进度条
9. 支持断点续传
10.可通过root对用户操作

使用说明:
1.可以在Linux和Windows都可以运行
2.root用户可以调用所有命令
3.其他用户只能调用了cd,ls,mkdir,rm,wget,put,命令

服务端启动命令
    python ftp_server.py start
客户端启动命令
    python ftp_client -s 服务端地址 -P 服务端端口 -u 用户名 -p 密码 

put 上传
wget 下载
mkdir 创建目录
ls  查看文件信息
rm  删除文件或目录
cd 切换目录
useradd 添加用户
usermod 修改用户
userdel 删除用户

服务端与客户端的功能对应,通过自省/反射来映射功能(hasattr、getattr)

文件目录结构

 

 

#coding=utf8
import optparse
from socket import *
import json
import os,sys
import struct
import hashlib

STATUS_CODE = {
    250 :   "Invalid cmd format, e.g : {'action':'get','filename':'test.py','size':344}",
    251 :   "Invalid cmd",
    252 :   "Invalid auth data",
    253 :   "Wrong username or password",
    254 :   "Passed authentication",
    255 :   "Filename doesn't provided",
    256 :   "File doesn't exist on server",
    257 :   "Ready to send file",
    258 :   "md5 verification",
    259 :   "Insufficient space left",

    800 :   "the file exist ,but not enough , is continue?",
    801 :   "the file exist!",
    802 :   "ready to receive datas",
    803 :   "User already exists",
    804 :   "This directory is used by other users",
    805 :   "This user does not exist",
    806:    "Delete home directory or not",

    900 :   "md5 valdate success",
    901: "OK",
    902: "the directory exist!"
}

class ClientHandler():
    def __init__(self):
        '''
        初始化,通过make_connection()得到一个socket连接,
        然后执行handler通信处理函数
        '''
        self.op = optparse.OptionParser()
        self.op.add_option('-s','--server',dest="server",help="server name or server ip")
        self.op.add_option('-P', '--port', dest="port", help="server port(0-65535)")
        self.op.add_option('-u', '--username', dest="username", help="username")
        self.op.add_option('-p', '--password', dest="password", help="password")
        self.options,self.argv = self.op.parse_args()
        self.main_path = os.path.dirname(os.path.abspath(__file__))
        self.verify_argv(self.options,self.argv)
        self.make_connection()
        self.handler()

    def handler(self):
        '''
        如果认证通过就进入通信循环,
        接受用户输入的命令,如果是quit或者exit就退出,输入命令为空就跳过此次循环
        根据用户输入的命令通过hasattr与getattr进行分发功能
        如果没有通过,接关闭socket连接
        :return:
        '''
        if self.authenticate():
            while True:
                cmd_info = input(self.pwd).strip()
                if cmd_info == 'quit' or cmd_info == 'exit':exit()
                if not cmd_info: continue
                cmd_list = cmd_info.split()
                if hasattr(self,cmd_list[0]):
                    func = getattr(self,cmd_list[0])
                    func(*cmd_list)
                else:
                    print("无效的命令")
        else:
            self.sock.close()

    def authenticate(self):
        '''
        认证判断用户输入的username和password是否为None
        如果是就提示用户输入,然后进入认证get_auth_result
        如果不是为None就直接认证get_auth_result
        :return:
        '''
        if (self.options.username is None) or (self.options.password is None):
            username = input("username :")
            password = input("password :")
            return self.get_auth_result(username,password)
        return self.get_auth_result(self.options.username,self.options.password)

    def get_auth_result(self,username,password):
        '''
        认证函数
        准备一个字典,字典中的action 是固定的 "auth"对应服务端的auth功能,认证功能字典中应该带有用户名和密码
        把字典传给resphonse功能进行发送给服务端
        通过request功能进行接受服务端发过来的字典
        判断服务端发来的状态码
        如果为254 即  254 :   "Passed authentication" 认证通过,
        就把self.user = username
        self.pwd = 服务端发送过来的路径
        如果不为254,直接输入状态码信息
        :param username:
        :param password:
        :return:
        '''
        data = {
            "action": "auth",
            "username":username,
            "password":password
        }
        self.resphonse(data)
        res = self.request()
        if res['status_code'] == 254:
            self.user = username
            print(STATUS_CODE[res['status_code']])
            self.pwd = res["bash"]
            return True
        else:
            print(STATUS_CODE[res['status_code']])

    def request(self):
        '''
        接受功能函数,
        从服务端接受包的长度,然后再从服务端接受包,这样可以解决粘包的问题,这里包编码前的格式为json
        接收到包之后进行解码,然后把json字符串转为为原有的格式(字典)

        :return:
        '''
        length = struct.unpack('i',self.sock.recv(4))[0]
        data = json.loads(self.sock.recv(length).decode('utf-8'))
        return data

    def resphonse(self,data):
        '''
        发送功能
        把接受到的字典,转换为json字符串然后进行编码
        使用struct.pack封装json字符串的长度
        向服务端发送长度,然后再发送已经编码的json字符串
        :param data:
        :return:
        '''
        data = json.dumps(data).encode('utf8')
        length = struct.pack('i',len(data))
        self.sock.send(length)
        self.sock.send(data)

    def make_connection(self):
        '''
        创建连接
        :return: 
        '''
        self.sock = socket(AF_INET,SOCK_STREAM)
        self.sock.connect((self.options.server,int(self.options.port)))

    def verify_argv(self,options,argv):
        '''
        端口参数验证
        :param options: 
        :param argv: 
        :return: 
        '''
        if int(options.port) > 0 and int(options.port) < 65535:
            return True
        else:
            exit("端口范围0-65535")

    def processbar(self,num,total):  # 进度条
        rate = num / total
        rate_num = int(rate * 100)
        is_ok = 0
        if rate_num == 100:
            r = '\r%s>%d%%\n' % ('=' * rate_num, rate_num,)
            is_ok = 1
        else:
            r = '\r%s>%d%%' % ('=' * rate_num, rate_num,)
        sys.stdout.write(r)
        sys.stdout.flush
        return is_ok


    def put(self,*cmd_list):
        cmd_list = cmd_list[1:]
        if not cmd_list:
            print("请输入要上传的文件路径!")
            return
        file_path = os.path.join(self.main_path,cmd_list[0])
        filename = os.path.basename(cmd_list[0])
        filesize = os.path.getsize(file_path)
        data = {
            "action" : "put",
            "filename" : filename,
            "filesize" :filesize
        }
        if len(cmd_list) == 1:
            data['target_path']= "."
        else:
            data['target_path'] = cmd_list[1]

        self.resphonse(data)

        is_exist = self.request()
        f = open(file_path,'rb')
        if is_exist["status_code"] == 802:
            has_received = 0
            f.seek(has_received)

        elif is_exist["status_code"] == 801 or is_exist["status_code"] == 259:
            print(STATUS_CODE[is_exist["status_code"]])
            return
        elif is_exist["status_code"] == 800:
            u_choice = input("the file exist,but not enough,is continue?[Y/N]").strip()
            self.resphonse({"choice": u_choice.upper()[0]})
            if u_choice.upper()[0] == "Y":
                has_received = self.request()['has_received']
                f.seek(has_received)
            else:
                has_received = 0
                f.seek(has_received)

        while has_received < filesize:
            file_data = f.read(1024)
            self.sock.send(file_data)
            has_received += len(file_data)
            self.processbar(has_received,filesize)
        f.close()

        print("put success!")

    def mkdir(self,*cmd_list):
        data = {
            "action" : "mkdir",
            "dirname": cmd_list[1:]
        }
        self.resphonse(data)
        res = self.request()
        if res["status_code"] != 901:
            print(STATUS_CODE[res["status_code"]])

    def rm(self,*cmd_list):
        data = {
            "action":"rm",
            "dirname":cmd_list[1:]
        }
        self.resphonse(data)
        res = self.request()


    def cd(self,*cmd_list):
        if len(cmd_list)==1 : return
        data = {
            "action" : "cd",
            "dirname" : cmd_list[1]
        }
        self.resphonse(data)
        res = self.request()
        self.pwd = res["bash"]

    def ls(self,*cmd_list):
        data = {
            "action": "ls",
        }
        if len(cmd_list) == 1:
            data["dirname"] = "."
        else:
            data["dirname"] = cmd_list[1]
        self.resphonse(data)
        res = self.request()
        if res["status_code"] == 903 :
            print(res["data"])
        else:
            print('\n'.join(res["data"]))

    def useradd(self,*cmd_list):
        if self.user != 'root':
            print("你无权限执行此命令!")
            return
        data = {
            "action" : "useradd",
        }
        data = self.useradd_verify_argv(*cmd_list,**data)
        self.resphonse(data)
        print(STATUS_CODE[self.request()["status_code"]])


    def useradd_verify_argv(self,*cmd_list,**data):
        op = optparse.OptionParser()
        op.add_option('-u', '--username', dest="username")
        op.add_option('-p', '--password', dest="password")
        op.add_option('-d', '--drictory', dest="drictory")
        op.add_option('-m', '--maxsize', dest="maxsize")
        options, argv = op.parse_args(list(cmd_list))
        data["username"] = options.username
        data["password"] = options.password
        data["home"] = options.drictory
        data["homemaxsize"] = options.maxsize
        if data["username"] is None: data["username"] = input("username : ")
        if data["action"] == "useradd":
            if data["password"] is None: data["password"] = input("password : ")
        if data["home"] is None: data["home"] = data["username"]
        if data["homemaxsize"] is None: data["homemaxsize"] = 1000
        return data

    def usermod(self,*cmd_list):
        if self.user != 'root':
            print("你无权限执行此命令!")
            return
        data = {
            "action" : "usermod",
        }
        data = self.useradd_verify_argv(*cmd_list, **data)
        self.resphonse(data)
        print(STATUS_CODE[self.request()["status_code"]])

    def userdel(self,*cmd_list):
        if self.user != 'root':
            print("你无权限执行此命令!")
            return
        data = {
            "action":"userdel",
            "username":cmd_list[1]
        }
        self.resphonse(data)
        res = self.request()
        if res["status_code"] == 805:
            print(STATUS_CODE[res["status_code"]])
            return
        choice = input("Delete home directory or not,Y/N:").strip()
        self.sock.send(choice.upper()[0].encode('utf8'))
        print(STATUS_CODE[res["status_code"]])

    def wget(self,*cmd_list):
        data = {
            "action" : "wget",
        }
        file_path = os.path.dirname(os.path.abspath(__file__))
        if len(cmd_list) == 1:
            print("请输入文件名!")
            return
        elif len(cmd_list) >= 3:
            if os.path.isabs(cmd_list[2]):
                file_path = cmd_list[2]
                if not os.path.exists(cmd_list[2]):
                    print("目标路径不存在!")
                    return
            else:
                file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),cmd_list[2])
                if not os.path.exists(file_path):
                    print("目标路径不存在!")
                    return


        data["filename"] = cmd_list[1]
        self.resphonse(data)
        res = self.request()
        if res["status_code"] == 256:
            print(STATUS_CODE[res["status_code"]])
            return
        try:
            f = open(os.path.join(file_path,os.path.basename(data["filename"])),'wb')

        except PermissionError as e:
            print(e)
            self.sock.send('0'.encode('utf-8'))
            return
        if res["filesize"] == 0 :
            f.close()
            return
        self.sock.send('1'.encode('utf-8'))
        size = 0
        while True:
            file_data = self.sock.recv(4096)
            f.write(file_data)
            size += len(file_data)
            if self.processbar(size,res["filesize"]):
                break

        f.close()








if __name__ == '__main__':
    c = ClientHandler()
    c.handler()
ftp_client.py
import os,sys

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

from src import main

if __name__ == '__main__':
    main.ArgvHandler()
ftp_server.py
[root]
username = root
password = 123
home = root
homemaxsize = 1000

[xie]
username = xie
password = 123
home = xie
homemaxsize = 1000
account.cfg
import os
BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

IP = '0.0.0.0'
PORT = 8090
HOME_PATH = os.path.join(BASEDIR,'data')
ACCOUNT_PATH = os.path.join(BASEDIR,'conf','account.cfg')

STATUS_CODE = {
    250 :   "Invalid cmd format, e.g : {'action':'get','filename':'test.py','size':344}",
    251 :   "Invalid cmd",
    252 :   "Invalid auth data",
    253 :   "Wrong username or password",
    254 :   "Passed authentication",
    255 :   "Filename doesn't provided",

    256 :   "File doesn't exist on server",

    257 :   "Ready to send file",
    258 :   "md5 verification",

    259 :   "Insufficient space left",

    800 :   "the file exist ,but not enough , is continue?",
    801 :   "the file exist!",
    802 :   "ready to receive datas",

    803 :   "User already exists",
    804 :   "This directory is used by other users",
    805 :   "This user does not exist",

    900 :   "md5 valdate success",

    901 :   "OK",

    902 :   "the directory exist!",

    903 :   "the directory not exist!",

    904 :   "No such file or directory"
}
setting.py
import os
from conf import setting

'''
服务端处理客户的类

'''

class Client:

    def __init__(self,user,passwd,home,maxsize):
        '''

        :param user:    用户名
        :param passwd:  密码
        :param home:    家目录
        :param maxsize: 家目录空间大小
        self.basedir ftp的根目录,所有家目录都是从这个目录开始
        self.path 目录列表,最开始的是家目录,然后添加到列表的是cd的目录,这里就限制了用户的根目录就是自己的家目录
        self.free 表示用户家目录剩余的空间大小
        '''
        self.user = user
        self.passwd = passwd
        self.basedir = setting.HOME_PATH
        self.home = home
        self.path = [self.home]
        self.maxsize = maxsize
        self.free = float(maxsize) - float(self.use_size()/1024)

    def get_bash(self):
        return '%s @ %s >>> :' % (self.user,self.path[-1])


    def use_size(self):
        '''
        用户家目录已使用的大小
        :param path:
        :return:
        '''
        size = 0
        for i in os.listdir(os.path.join(self.basedir,self.home)):
            if os.path.isdir(i):
                size += self.use_size(os.path.join(self.basedir,self.home,i))
            size += os.path.getsize(os.path.join(self.basedir,self.home,i))
        size = float(size/1024)
        #由于系统4K 对齐,所以最小存储单位为 4KB
        res = divmod(size,4)
        if res[1]:
            res = res[0] + 1
        else:
            res = res[0]
        return res

    def get_path(self):
        '''
        获取当前所在目录的绝对路径
        :return:
        '''
        basepath = self.basedir
        for i in self.path:
            basepath = os.path.join(basepath,i)
        return basepath





if __name__ == '__main__':
    c = Client('xie','asd','src',100)
    c.free_size(c.home)
client.py
import optparse
from src import server_handler
import socketserver
from conf.setting import *

class ArgvHandler():
    '''
    参数处理类,
    使用optparse.OptionParser来处理参数
    '''
    def __init__(self):
        self.op = optparse.OptionParser()
        options,argv = self.op.parse_args()
        self.verify_argv(options,argv)

    def verify_argv(self,options,argv):
        '''
        验证参数函数,通过获取的argv 参数,然后利用hasattr和getattr反射功能来分发功能,比如 start 、help等
        :param options:
        :param argv:
        :return:
        '''
        cmd = argv[0]

        if hasattr(self,cmd):
            func = getattr(self,cmd)
            func()
        else:
            print("参数错误!")

    def start(self):
        '''
        服务启动功能
        :return:
        '''
        print("server is working...")
        s = socketserver.ThreadingTCPServer((IP,PORT), server_handler.FtpHandler)
        s.serve_forever()

    def help(self):
        pass
main.py
#coding=utf8
import socketserver
import struct
import json,os
import configparser
from conf import setting
from lib import client

class FtpHandler(socketserver.BaseRequestHandler):
    '''
    sockerserver多线程类
    '''
    def ser_recv(self):
        '''
        接受客户端数据
        :return: 返回客户端发给来的字典
        '''
        try:
            length = struct.unpack('i',self.request.recv(4))[0]
            return json.loads(self.request.recv(length).decode('utf8'))
        except Exception as e:
            print(e)
            return False
    def ser_resphone(self,data):
        '''
        给客户端相应的函数
        :param data:
        :return:
        '''
        data = json.dumps(data).encode('utf-8')
        length = struct.pack('i',len(data))
        self.request.send(length)
        self.request.sendall(data)

    def handle(self):
        '''
        通信循环,获取客户端发过来的字典,通过字典中的action来判断功能,用hasattr/getattr来分发功能
        :return:
        '''
        while True:
            data = self.ser_recv()
            if not data: break
            if data.get("action") is not None:
                if hasattr(self,data.get("action")):
                    func = getattr(self,data["action"])
                    func(**data)
                else:
                    print("无效的操作")
            else:
                print("无效的操作!")

    def auth(self,**data):
        username = data["username"]
        password = data["password"]
        user = self.authenticate(username,password)
        if user:
            res = {
                "status_code": 254,
                'bash': self.user.get_bash()
            }
        else:
            res = {
                "status_code":253,
            }
        self.ser_resphone(res)

    def authenticate(self,username,password):
        cfg = configparser.ConfigParser()
        cfg.read(setting.ACCOUNT_PATH)
        if username in cfg.sections() and password == cfg[username]["password"]:
            self.user = client.Client(username,password,cfg[username]['home'],cfg[username]['homemaxsize'])
            return username

    def cd(self,**data):
        if data["dirname"] == ".":
            res = {"bash":self.user.get_bash()}
        elif data["dirname"] == "..":
            if len(self.user.path) > 1:
                self.user.path.pop()
            os.chdir(self.user.get_path())

            res = {"bash":self.user.get_bash()}
        else:
            self.user.path.append(data["dirname"])
            os.chdir(self.user.get_path())
            res = {"bash":self.user.get_bash()}
        self.ser_resphone(res)

    def ls(self,**data):
        pwd = self.user.get_path()
        if data["dirname"] == "." :
            new_path = pwd
        else:
            new_path = os.path.join(pwd, data["dirname"])
        if (data["dirname"] == ".") or (os.path.exists(new_path)):
            listdir = os.listdir(new_path)
            res = {
                "status_code":901,
                "data" : listdir
            }
        else:
            res = {
                "status_code": 903,
                "data": "该目录不存在"
            }

        self.ser_resphone(res)




    def wget(self,**data):
        file_name = os.path.basename(data["filename"])
        file_path = os.path.join(self.user.get_path(),data["filename"])
        if os.path.exists(file_path):
            if os.path.isfile(file_path):
                self.ser_resphone({"status_code": 901,"filesize":os.path.getsize(file_path)})
                if self.request.recv(1).decode("utf8") == "1":
                    with open(file_path,'rb') as f:
                        while True:
                            data = f.read(4096)
                            if not data: break
                            self.request.send(data)
                return

        return self.ser_resphone({"status_code":256})


    def put(self,**data):
        file_name = data['filename']
        file_size = data['filesize']
        if self.user.free < file_size/1024/1024:
            return self.ser_resphone({"status_code": 259})

        target_path = data['target_path']

        if target_path == '.':
            abs_path = os.path.join(self.user.get_path(),file_name)
        else:
            abs_path = os.path.join(self.user.get_path(),target_path,file_name)

        has_received = 0
        if os.path.exists(abs_path):
            file_has_size = os.path.getsize(abs_path)
            if file_has_size < file_size:
                #断点续传
                self.ser_resphone({"status_code":800})
                client_choice = self.ser_recv()
                if client_choice["choice"] == "Y":
                    self.ser_resphone({"has_received":file_has_size})
                    f = open(abs_path,"ab")
                    has_received += file_has_size
                else:
                    f = open(abs_path,"wb")
            else:
                return self.ser_resphone({"status_code":801})

        else:
            self.ser_resphone({"status_code":802})
            f = open(abs_path,"wb")


        while has_received < file_size:
            data = self.request.recv(1024)
            f.write(data)
            has_received += len(data)

        f.close()



    def mkdir(self,**data):
        dir_list = data["dirname"]
        for i in dir_list:
            try:
                os.makedirs(os.path.join(self.user.get_path(),i))
            except FileExistsError as e:
                print(e)
                res = {"status_code":902}
                return self.ser_resphone(res)

        res = {"status_code":901}
        self.ser_resphone(res)

    def rm_handle(self,**data):
        recv_data = data
        basedir = self.user.get_path()
        if data["action"] == "userdel": basedir = setting.HOME_PATH
        for i in recv_data["dirname"]:
            path = os.path.join(basedir,*data["path"],i)
            print(path)
            if os.path.exists(path):
                if os.path.isfile(path):
                    os.remove(path)
                else:
                    data["dirname"] =[i for i in os.listdir(path)]
                    data["path"].append(i)
                    self.rm_handle(**data)
                    data["path"].pop()
                    os.rmdir(path)
            else:
                return  {"status": 904 ,"dirname":i}
        return {"status": 901}

    def rm(self,**data):
        data["path"] = []
        res = self.rm_handle(**data)
        self.ser_resphone(res)


    def useradd(self,**data):
        config = configparser.ConfigParser()
        code = self.useradd_verify_argv(config,**data)
        if code == 803 or code == 804:return self.ser_resphone({"status_code":code})
        del data["action"]
        config[data["username"]] = data
        if not os.path.exists(os.path.join(setting.HOME_PATH,data['home'])): os.mkdir(os.path.join(setting.HOME_PATH,data['home']))
        with open(setting.ACCOUNT_PATH, 'w') as configfile:
            config.write(configfile)
        self.ser_resphone({"status_code": code})

    def useradd_verify_argv(self,conf,**data):
        conf.read(setting.ACCOUNT_PATH)
        if data["action"] == "useradd":
            if data["username"] in conf.sections():
                return 803
            for i in conf.sections():
                if data["home"] == conf[i]["home"]:
                    return 804
            return 901
        elif data["action"] == "usermod":
            if data["username"] in conf.sections():
                if data["password"] is None:
                    data["password"] = conf[data["username"]]["password"]
                print(data["home"],conf[data["username"]]["home"])
                if data["home"] != conf[data["username"]]["home"]:
                    for i in conf.sections():
                        if data["home"] == conf[i]["home"]:
                            return 804
                del data["action"]
                return 901, data
            else:
                return 805
        else:
            if data["username"] in conf.sections():
                return 901
            else:
                return 805


    def usermod(self,**data):
        config = configparser.ConfigParser()
        code = self.useradd_verify_argv(config, **data)
        if code == 805 or code == 804 : return self.ser_resphone({"status_code": code})
        if not os.path.exists(os.path.join(setting.HOME_PATH,data['home'])): os.mkdir(os.path.join(setting.HOME_PATH, data['home']))
        config[data["username"]] = code[1]
        with open(setting.ACCOUNT_PATH, 'w') as configfile:
            config.write(configfile)
        self.ser_resphone({"status_code": code[0]})

    def userdel(self,**data):
        config = configparser.ConfigParser()
        config.read(setting.ACCOUNT_PATH)
        code = self.useradd_verify_argv(config, **data)
        if code == 805 : return self.ser_resphone({"status_code": code})
        self.ser_resphone({"status_code": code})
        choice = self.request.recv(1).decode('utf8')
        if choice == "Y":
            home = config[data["username"]]["home"]
            self.rm_handle(**{"dirname":[home],"action":"userdel","path":[]})
        config.remove_section(data["username"])
        config.write(open(setting.ACCOUNT_PATH, 'w'))
server_handler.py

 

服务端启动:

 

 

 客户端启动:

 

 

 ls命令:

 

 

 put命令

 

 

 断点续传,将s1.avi 上传到 a目录

 

 

 上面用ctrl+c中断了上传

 

 

 

 

cd命令:

 

 wget命令:

 

 mkdir命令

 

rm命令

 

 useradd命令

 

 

 

 usermod命令

 

 

userdel命令

 

 

 

 

 

 

 

 

 

 

 断点续传

posted @ 2019-10-16 17:48  Mr-谢  阅读(364)  评论(0编辑  收藏  举报