python 多用户在线的FTP程序

时间:2022-08-29 11:31:42


要求:

1、用户加密认证

2、允许同时多用户登录

3、每个用户有自己的家目录 ,且只能访问自己的家目录

4、对用户进行磁盘配额,每个用户的可用空间不同

5、允许用户在ftp server上随意切换目录

6、允许用户查看当前目录下文件

7、允许上传和下载文件,保证文件一致性

8、文件传输过程中显示进度条

9、附加功能:支持文件的断点续传


README:

设计说明

1、client连接server端需要验证账号密码,密码使用MD5加密传输。

2、用户信息保存在本地文件中,密码MD5加密存储。磁盘配额大小也保存在其中。

3、用户连接上来后,可以执行命令如下

    目录变更:cd /cd dirname / cd . /cd ..

    文件浏览:ls

     文件删除:rm filename

    目录增删:mkdir dirname /rmdir dirname

    查看当前目录:pwd

    查看当前目录大小: du

    上传文件:put filename

    下载文件:get filename

    移动和重命名: mv filename/dirname filename/dirname

     上传断点续传: newput filename

    下载断点续传: newget filename

4、涉及到目录的操作,用户登录后,程序会给用户一个“锚位”----以用户名字命名的家目录,使用户无论怎么操作,都只能在这个目录底下。而在发给用户的目录信息时,隐去上层目录信息。

5、用户在创建时,磁盘配额大小默认是100M,在上传文件时,程序会计算当前目录大小加文件大小是否会超过配额上限。未超过,上传;超过,返回磁盘大小不够的信息。磁盘配额可通过用户管理程序修改。

6、文件上传和下载后都会进行MD5值比对,验证文件是否一致。

7、服务端和客户端都有显示进度条功能,启用该功能会降低文件传输速度,这是好看的代价。

8、文件断点续传,支持文件上传和下载断点续传。断点续传上传功能还会检测用户控件是否足够。(断点续传命令使用前面new+put/get命名,包含put/get所有功能,由于逻辑增多,代码复杂,特地保留原put/get,以备后用)。


暂且说到这,接下来是正式程序


python 多用户在线的FTP程序


试运行截图

python 多用户在线的FTP程序



代码如下:

1、服务端

server.conf

####用户端配置文件####[DEFAULT]
logfile = ../log/server.log
usermgr_log = ../log/usermgr.log
upload_dir= ../user_files
db_dir = ../db

####日志文件位置####
[log]
logfile = ../log/server.log
usermgr_log = ../log/usermgr.log

####上传文件存放位置####
[upload]
upload_dir= ../user_files

####用户信息存放位置####
[db]
db_dir = ../db


main.py

#!/usr/bin/env python# -*- coding:utf-8 -*-import socketserver,osfrom usermanagement import useroprfrom server import MyTCPHandlerinfo = '''        1、启动服务器        2、进入用户管理        按q退出'''if __name__ == '__main__':    while True:        print(info)        choice = input('>>>:')        if choice == 'q':            exit()        elif choice == '1':            ip, port = '0.0.0.0', 9999            server = socketserver.ThreadingTCPServer((ip, port), MyTCPHandler)            server.serve_forever()        elif choice == '2':            useropr.interactive()        else:continue

usermanagement

#!/usr/bin/env python# -*- coding:utf-8 -*-#filename:usermanagement.pyimport os,hashlib,time,pickle,shutil,configparser,logging####读取配置文件####base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))config_file = os.path.join(base_dir, 'conf/server.conf')cf = configparser.ConfigParser()cf.read(config_file)####设定日志目录####if os.path.exists(cf.get('log','usermgr_log')):    logfile = cf.get('log', 'usermgr_log')else:    logfile = os.path.join(base_dir,'log/usermgr.log')####设定用户上传文件目录,这边用于创建用户家目录使用####if os.path.exists(cf.get('upload','upload_dir')):    file_dir = cf.get('upload','upload_dir')else:    file_dir = os.path.join(base_dir,'user_files')####设定用户信息存储位置####if os.path.exists(cf.get('db','db_dir')):    db_path = cf.get('db','db_dir')else:    db_path = os.path.join(base_dir,'db')def hashmd5(*args):             ####用于加密密码信息    m = hashlib.md5()    m.update(str(*args).encode())    return m.hexdigest()class useropr(object):    def __init__(self,user_name,passwd = '123456',phone_number=''):        self.user_name = user_name        self.id = time.strftime("%Y%m%d%H%M%S", time.localtime())        self.phone_number = phone_number        self.passwd = passwd        self.space_size = 104857600         ####初始分配100MB存储空间        self.member_level = 1               ####会员等级,初始为1,普通会员    @staticmethod                       ####使用静态方法,可以直接用类命调用,如user.search_user(username),否则需要实例化一个对象后才能调用    def query_user(user_name):        ####查询用户        db_filelist=os.listdir(db_path)        #print(db_filelist)        dict={}        for filename in db_filelist:            with open(os.path.join(db_path,filename),'rb') as f:                content=pickle.load(f)                #print(filename,content)    ####开启会打印出所有用户信息                if content['username'] == user_name:                    #print(filename, content)                    dict={'filename':filename,'content':content}                    return dict    def save_userinfo(self):                    ####保存用户信息        query_result = self.query_user(self.user_name)      ####检查是否已存在同名用户,如果没有查询结果应该为None        if query_result == None:            user_info = {                'username':self.user_name,                'id':self.id,                'phonenumber':self.phone_number,                'passwd':hashmd5(self.passwd),                'spacesize':self.space_size,                'level':self.member_level            }            with open(os.path.join(db_path,self.id),'wb') as f:                pickle.dump(user_info,f)                print('用户信息保存完毕')                try:                                                    ####创建用户家目录                    os.mkdir(os.path.join(file_dir, self.user_name))                    print('用户目录创建成功!')                except Exception as e:                    print('用户目录创建失败,',e)        else:            print('用户名重复,信息未保存')    @staticmethod    def change_info(user_name,**kwargs):           ####修改信息        query_result = useropr.query_user(user_name)  ####用于检测用户是否存在,不存在不处理        if query_result != None:            userinfo_filename = query_result['filename']            user_info = query_result['content']            print('before update:',user_info)            for key in kwargs:                if key in ('username','id'):            ####用户名和ID不可更改                    print(key,'项不可更改')                elif key in ('passwd','phonenumber','spacesize','level'):         ####允许修改的键值                    if key == 'passwd':                        user_info[key] = hashmd5(kwargs[key])  ####加密密码保存                    else:                        user_info[key] = kwargs[key]                    with open(os.path.join(db_path, userinfo_filename), 'wb') as f:                        pickle.dump(user_info, f)                        print(key,'项用户信息变更保存完毕')                else:                    print('输入信息错误,',key,'项不存在')            print('after update:',user_info)        else:            print('用户不存在')    @staticmethod    def delete_user(user_name):              ####删除用户        query_result = useropr.query_user(user_name)  ####用于检测用户是否存在,不存在不处理        if query_result != None:            userinfo_filename = query_result['filename']            userfile_path=os.path.join(db_path, userinfo_filename)            os.remove(userfile_path)            query_result_again = useropr.query_user(user_name)            if query_result_again == None:                print('用户DB文件删除成功')                try:                    shutil.rmtree(os.path.join(file_dir,user_name))                    print('用户家目录删除成功')                except Exception as e:                    print('用户家目录删除失败:',e)            else:                print('用户DB文件删除失败')        else:            print('用户不存在或者已经被删除')    @staticmethod    def query_alluser():        ####查询所有用户信息,用于调试使用        db_filelist=os.listdir(db_path)        for filename in db_filelist:            with open(os.path.join(db_path,filename),'rb') as f:                content=pickle.load(f)                print(filename,content)    @staticmethod    def interactive():        '''使用说明:        新增用户请输入类似: a=useropr(username,passwd)                            a.save_userinfo()        查询用户请输入:useropr.query_user(username)        更改用户信息请输入:useropr.change_info(username,id=123,level=1,passwd=123,phonenumber=123),其中字典部分为可选项        用户删除请输入:useropr.delete_user(username)        '''        info='''        1、新增用户        2、查询用户        3、修改用户        4、删除用户        退出请按q        '''        #useropr.query_alluser()        ####查询所有用户信息,调试用        while True:            print(info)            choice = input('请输入你的选择:').strip()            #print('operation choice: %s' % choice)            if choice == 'q':                exit()            else:                username = input('请输入用户名:').strip()                #print('username: %s' % username)                if username == '':                    print('用户不能为空')                    continue                elif choice == '1':                    passwd = input('请输入密码:')                    new_user = useropr(username, passwd)                    new_user.save_userinfo()                elif choice == '2':                    print(useropr.query_user(username))                elif choice == '3':                    update_item = input('请输入要修改的项目,例如:level,passwd,phonenumber:')                    print('update item: %s' % update_item)                    update_value = input('请输入要修改的项目新值:')                    useropr.change_info(username,**{update_item:update_value})      #### ‘**{}’ 不加**系统无法识别为字典。不能直接使用update_item=update_value,update_item会直接被当成key值,而不是其中的变量。                elif choice == '4':                    useropr.delete_user(username)                else:                    print('输入错误')                    continueif __name__ == '__main__':    useropr.interactive()

server.py

#!/usr/bin/env python# -*- coding:utf-8 -*-# filename:server.pyimport socketserver, json, os, sys, time, shutil, configparser, loggingfrom usermanagement import useropr####读取配置文件####base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))config_file = os.path.join(base_dir, 'conf/server.conf')cf = configparser.ConfigParser()cf.read(config_file)####设定日志目录####if os.path.exists(cf.get('log', 'logfile')):    logfile = cf.get('log', 'logfile')else:    logfile = os.path.join(base_dir, 'log/server.log')####设定用户上传文件目录####if os.path.exists(cf.get('upload', 'upload_dir')):    file_dir = cf.get('upload', 'upload_dir')else:    file_dir = os.path.join(base_dir, 'user_files')####设置日志格式###logging.basicConfig(level=logging.INFO,                    format='%(asctime)s %(levelname)s %(message)s',                    datefmt='%Y-%m-%d    %H:%M:%S',                    filename=logfile,                    filemode='a+')def TimeStampToTime(timestamp):  ####输入timestamp格式化输出时间,输出格式如:2017-09-16 16:32:35    timeStruct = time.localtime(timestamp)    return time.strftime('%Y-%m-%d %H:%M:%S', timeStruct)def ProcessBar(part, total):  ####进度条模块,运行会导致程序变慢    if total != 0:        i = round(part * 100 / total)        sys.stdout.write(            '[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r')        sys.stdout.flush()        # if part == total:        #     print()class MyTCPHandler(socketserver.BaseRequestHandler):    def put(self, *args):  ####接收客户端文件        # self.request.send(b'server have been ready to receive')    ####发送ACK        cmd_dict = args[0]        filename = os.path.basename(cmd_dict['filename'])  ####传输进来的文件名可能带有路径,将路径去掉        filesize = cmd_dict['filesize']        filemd5 = cmd_dict['filemd5']        override = cmd_dict['override']        receive_size = 0        file_path = os.path.join(self.position, filename)        if override != 'True' and os.path.exists(file_path):  ####检测文件是否已经存在            self.request.send(b'file have exits, do nothing!')        else:            if os.path.isfile(file_path):  ####如果文件已经存在,先删除,再计算磁盘空间大小                os.remove(file_path)            current_size = self.du()  ####调用du查看用户磁盘空间大小,但是du命令的最后会发送一个结果信息给client,会和前面和后面的信息粘包,需要注意            self.request.recv(1024)  ####接收客户端ack信号,防止粘包,代号:P01            print(self.user_spacesize, current_size, filesize)            if self.user_spacesize >= current_size + filesize:                self.request.send(b'begin')  ####发送开始传输信号                fk = open(file_path, 'wb')                while filesize > receive_size:                    if filesize - receive_size > 1024:                        size = 1024                    else:                        size = filesize - receive_size                    data = self.request.recv(size)                    fk.write(data)                    receive_size += len(data)                    # print(receive_size,len(data))   ####打印每次接收的数据                    # ProcessBar(receive_size, filesize)  ####服务端进度条,不需要可以注释掉                fk.close()                receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]                print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5)                if receive_filemd5 == filemd5:                    self.request.send(b'file received successfully!')                else:                    self.request.send(b'Error, file received have problems!')            else:                self.request.send(                    b'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % (                    self.user_spacesize, current_size, self.user_spacesize - current_size, filesize))    def get(self, *args):  ####发送给客户端文件        # print('get receive the cmd',args[0])        filename = args[0]['filename']        print(filename)        # self.request.send(b'server have been ready to send')  ####发送ACK        file_path = os.path.join(self.position, filename)        if os.path.isfile(file_path):            filesize = os.path.getsize(file_path)            ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低            filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]            msg = {                'action': 'get',                'filename': filename,                'filesize': filesize,                'filemd5': filemd5,                'override': 'True'            }            print(msg)            self.request.send(json.dumps(msg).encode('utf-8'))            '''接下来发送文件给客户端'''            self.request.recv(1024)  ####接收ACK信号,下一步发送文件            fk = open(file_path, 'rb')            send_size = 0            for line in fk:                send_size += len(line)                self.request.send(line)                # ProcessBar(send_size, filesize)     ####服务端进度条,不需要可以注释掉            else:                print('文件传输完毕')                fk.close()        else:            print(file_path, '文件未找到')            self.request.send(json.dumps('Filenotfound').encode('utf-8'))    def newput(self, *args):  ####接收客户端文件,具有断点续传功能        # self.request.send(b'server have been ready to receive')    ####发送ACK        cmd_dict = args[0]        filename = os.path.basename(cmd_dict['filename'])  ####传输进来的文件名可能带有路径,将路径去掉        filesize = cmd_dict['filesize']        filemd5 = cmd_dict['filemd5']        override = cmd_dict['override']        receive_size = 0        file_path = os.path.join(self.position, filename)        print(file_path,os.path.isdir(file_path))        if override != 'True' and os.path.exists(file_path):  ####检测文件是否已经存在            if os.path.isdir(file_path):                self.request.send(b'file have exits, and is a directory, do nothing!')            elif os.path.isfile(file_path):                self.request.send(b'file have exits, do nothing!')                resume_signal = self.request.recv(1024)     ####接收客户端发来的是否从文件断点续传的信号                if resume_signal == b'ready to resume from break point':           ####执行断点续传功能                    exits_file_size = os.path.getsize(file_path)                    current_size = self.du()                    time.sleep(0.5) ####防止粘包                    print('用户空间上限:%d, 当前已用空间:%d, 已存在文件大小:%d, 上传文件大小:%d ' % (self.user_spacesize,current_size,exits_file_size,filesize))                    if self.user_spacesize >= (current_size - exits_file_size + filesize):  ####判断剩余空间是否足够                        if exits_file_size < filesize:                            receive_size = exits_file_size                            print('服务器上已存在的文件大小为:',exits_file_size)                            msg = {                                'state': True,                                'position': exits_file_size,                                'content': 'ready to receive file'                            }                            self.request.send(json.dumps(msg).encode('utf-8'))                            fk = open(file_path, 'ab+')                            while filesize > receive_size:                                if filesize - receive_size > 1024:                                    size = 1024                                else:                                    size = filesize - receive_size                                data = self.request.recv(size)                                fk.write(data)                                receive_size += len(data)                                # print(receive_size,len(data))   ####打印每次接收的数据                                # ProcessBar(receive_size, filesize)  ####服务端进度条,不需要可以注释掉                            fk.close()                            receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]                            print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5)                            if receive_filemd5 == filemd5:                                self.request.send(b'file received successfully!')                            else:                                self.request.send(b'Error, file received have problems!')                        else:       ####如果上传的文件小于当前服务器上的文件,则为同名但不同文件,不上传。实际还需要增加其他判断条件,判断是否为同一文件。                            msg = {                                'state': False,                                'position': '',                                'content': 'Error, file mismatch, do nothing!'                            }                            self.request.send(json.dumps(msg).encode('utf-8'))                    else:       ####如果续传后的用户空间大于上限,拒接续传                        msg = {                            'state': False,                            'position':'',                            'content':'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, need_size:%d' % (self.user_spacesize, current_size, self.user_spacesize - current_size, filesize - exits_file_size)                        }                        self.request.send(json.dumps(msg).encode('utf-8'))                else:                    pass        else:            if os.path.isfile(file_path):  ####如果文件已经存在,先删除,再计算磁盘空间大小                os.remove(file_path)            current_size = self.du()  ####调用du查看用户磁盘空间大小,但是du命令的最后会发送一个结果信息给client,会和前面和后面的信息粘包,需要注意            self.request.recv(1024)  ####接收客户端ack信号,防止粘包,代号:P01            print(self.user_spacesize, current_size, filesize)            if self.user_spacesize >= current_size + filesize:                self.request.send(b'begin')  ####发送开始传输信号                fk = open(file_path, 'wb')                while filesize > receive_size:                    if filesize - receive_size > 1024:                        size = 1024                    else:                        size = filesize - receive_size                    data = self.request.recv(size)                    fk.write(data)                    receive_size += len(data)                    # print(receive_size,len(data))   ####打印每次接收的数据                    # ProcessBar(receive_size, filesize)  ####服务端进度条,不需要可以注释掉                fk.close()                receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]                print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5)                if receive_filemd5 == filemd5:                    self.request.send(b'file received successfully!')                else:                    self.request.send(b'Error, file received have problems!')            else:                self.request.send(                    b'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % (                    self.user_spacesize, current_size, self.user_spacesize - current_size, filesize))    def newget(self, *args):  ####发送给客户端文件,具有断点续传功能        # print('get receive the cmd',args[0])        filename = args[0]['filename']        remote_local_filesize = args[0]['filesize']        print(filename)        # self.request.send(b'server have been ready to send')  ####发送ACK        file_path = os.path.join(self.position, filename)        if os.path.isfile(file_path):            filesize = os.path.getsize(file_path)            ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低            filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]            msg = {                'action': 'newget',                'filename': filename,                'filesize': filesize,                'filemd5': filemd5,                'override': 'True'            }            print(msg)            self.request.send(json.dumps(msg).encode('utf-8'))            '''接下来发送文件给客户端'''            self.request.recv(1024)  ####接收ACK信号,下一步发送文件            fk = open(file_path, 'rb')            fk.seek(remote_local_filesize,0)            send_size = remote_local_filesize            for line in fk:                send_size += len(line)                self.request.send(line)                # ProcessBar(send_size, filesize)     ####服务端进度条,不需要可以注释掉            else:                print('文件传输完毕')                fk.close()        else:            print(file_path, '文件未找到')            self.request.send(json.dumps('Filenotfound').encode('utf-8'))    def pwd(self, *args):        current_position = self.position        result = current_position.replace(file_dir, '')  ####截断目录信息,使用户只能看到自己的家目录信息        self.request.send(json.dumps(result).encode('utf-8'))    def ls(self, *args):  ####列出当前目录下的所有文件信息,类型,字节数,生成时间。        result = ['%-20s%-7s%-10s%-23s' % ('filename', 'type', 'bytes', 'creationtime')]  ####信息标题        for f in os.listdir(self.position):            type = 'unknown'            f_abspath = os.path.join(self.position, f)  ####给出文件的绝对路径,不然程序会找不到文件            if os.path.isdir(f_abspath):                type = 'd'            elif os.path.isfile(f_abspath):                type = 'f'            result.append('%-20s%-7s%-10s%-23s' % (            f, type, os.path.getsize(f_abspath), TimeStampToTime(os.path.getctime(f_abspath))))        self.request.send(json.dumps(result).encode('utf-8'))    def du(self, *args):        '''统计纯文件和目录占用空间大小,结果小于在OS上使用du -s查询,因为有一些(例如'.','..')隐藏文件未包含在内'''        totalsize = 0        if os.path.isdir(self.position):            dirsize, filesize = 0, 0            for root, dirs, files in os.walk(self.position):                for d_item in dirs:  ####计算目录占用空间,Linux中每个目录占用4096bytes,实际上也可以按这个值来相加                    if d_item != '':                        dirsize += os.path.getsize(os.path.join(root, d_item))                for f_item in files:  ####计算文件占用空间                    if f_item != '':                        filesize += os.path.getsize(os.path.join(root, f_item))            totalsize = dirsize + filesize            result = 'current directory total sizes: %d' % totalsize        else:            result = 'Error,%s is not path ,or path does not exist!' % self.position        self.request.send(json.dumps(result).encode('utf-8'))        return totalsize    def cd(self, *args):        print(*args)        user_homedir = os.path.join(file_dir, self.username)        cmd_dict = args[0]        error_tag = False        '''判断目录信息'''        if cmd_dict['dir'] == '':            self.position = user_homedir        elif cmd_dict['dir'] == '.' or cmd_dict['dir'] == '/' or '//' in cmd_dict['dir']:  ####'.','/','//','///+'匹配            pass        elif cmd_dict['dir'] == '..':            if user_homedir != self.position and user_homedir in self.position:  ####当前目录不是家目录,并且当前目录是家目录下的子目录                self.position = os.path.dirname(self.position)        elif '.' not in cmd_dict['dir'] and os.path.isdir(                os.path.join(self.position, cmd_dict['dir'])):  ####'.' not in cmd_dict['dir'] 防止../..输入            self.position = os.path.join(self.position, cmd_dict['dir'])        else:            error_tag = True        '''发送结果'''        if error_tag:            result = 'Error,%s is not path here, or path does not exist!' % cmd_dict['dir']            self.request.send(json.dumps(result).encode('utf-8'))        else:            self.pwd()    def mv(self,*args):        print(*args)        try:            objectname = args[0]['objectname']            dstname = args[0]['dstname']            abs_objectname = os.path.join(self.position,objectname)            abs_dstname = os.path.join(self.position, dstname)            print(abs_objectname,abs_dstname,os.path.isfile(abs_objectname),os.path.isdir(abs_objectname),os.path.isdir(abs_dstname))            result = ''            if os.path.isfile(abs_objectname):                if os.path.isdir(abs_dstname) or not os.path.exists(abs_dstname):                    shutil.move(abs_objectname, abs_dstname)                    print('moving success')                    result = 'moving success'                elif os.path.isfile(abs_dstname):                    print('moving cancel, file has been exits')                    result = 'moving cancel, file has been exits'            elif os.path.isdir(abs_objectname):                if os.path.isdir(abs_dstname) or not os.path.exists(abs_dstname):                    shutil.move(abs_objectname, abs_dstname)                    print('moving success')                    result = 'moving success'                elif os.path.isfile(abs_dstname):                    print('moving cancel, %s is file' % dstname)                    result = 'moving cancel, %s is file' % dstname            else:                print('nothing done')                result = 'nothing done'            self.request.send(json.dumps(result).encode('utf-8'))        except Exception as e:            print(e)            result = 'moving fail,' + e            self.request.send(json.dumps(result).encode('utf-8'))    def mkdir(self, *args):  ####创建目录        try:            dirname = args[0]['dirname']            if dirname.isalnum():  ####判断文件是否只有数字和字母                if os.path.exists(os.path.join(self.position, dirname)):                    result = '%s have existed' % dirname                else:                    os.mkdir(os.path.join(self.position, dirname))                    result = '%s created succes' % dirname            else:                result = 'Illegal character %s, dirname can only by string and num here.' % dirname        except TypeError:            result = 'please input dirname'        self.request.send(json.dumps(result).encode('utf-8'))    def rm(self, *args):  ####删除文件        filename = args[0]['filename']        confirm = args[0]['confirm']        file_abspath = os.path.join(self.position, filename)        if os.path.isfile(file_abspath):            if confirm == True:                os.remove(file_abspath)                result = '%s have been delete.' % filename            else:                result = 'Not file deleted'        elif os.path.isdir(file_abspath):            result = '%s is a dir, plsese using rmdir' % filename        else:            result = 'File %s not exist!' % filename        self.request.send(json.dumps(result).encode('utf-8'))    def rmdir(self, *args):  ###删除目录        dirname = args[0]['dirname']        confirm = args[0]['confirm']        file_abspath = os.path.join(self.position, dirname)        if '.' in dirname or '/' in dirname:  ####不能跨目录删除            result = 'should not rmdir %s this way' % dirname        elif os.path.isdir(file_abspath):            if confirm == True:                shutil.rmtree(file_abspath)                result = '%s have been delete.' % dirname            else:                result = 'Not file deleted'        elif os.path.isfile(file_abspath):            result = '%s is a file, not directory deleted' % dirname        else:            result = 'directory %s not exist!' % dirname        self.request.send(json.dumps(result).encode('utf-8'))    def auth(self):        self.data = json.loads(self.request.recv(1024).decode('utf-8'))        print(self.data)        recv_username = self.data['username']        recv_passwd = self.data['passwd']        query_result = useropr.query_user(recv_username)        print(query_result)        if query_result == None:            self.request.send(b'user does not exits')        elif query_result['content']['passwd'] == recv_passwd:            self.request.send(b'ok')            return query_result  ####返回查询结果        elif query_result['content']['passwd'] != recv_passwd:            self.request.send(b'password error')        else:            self.request.send(b'unknown error')    def handle(self):  ####处理类,调用以上方法        # self.position = file_dir        # print(self.position)        auth_tag = False        while auth_tag != True:            auth_result = self.auth()  ####用户认证,如果通过,返回用户名,不通过为None            print('the authentication result is:', auth_result)            if auth_result != None:                self.username = auth_result['content']['username']                self.user_spacesize = auth_result['content']['spacesize']                auth_tag = True        print(self.username, self.user_spacesize)        user_homedir = os.path.join(file_dir, self.username)        if os.path.isdir(user_homedir):            self.position = user_homedir  ####定锚,用户家目录            print(self.position)            while True:                print('当前连接:', self.client_address)                self.data = self.request.recv(1024).strip()                print(self.data)                logging.info(self.client_address)                if len(self.data) == 0:                    print('客户端断开连接')                    break  ####检查发送来的命令是否为空                cmd_dict = json.loads(self.data.decode('utf-8'))                action = cmd_dict['action']                logging.info(cmd_dict)                if hasattr(self, action):                    func = getattr(self, action)                    func(cmd_dict)                else:                    print('未支持指令:', action)                logging.info('current directory:%s' % self.position)if __name__ == '__main__':    ip, port = '0.0.0.0', 9999    server = socketserver.ThreadingTCPServer((ip, port), MyTCPHandler)    server.serve_forever()




2、客户端

client.conf

####用户端配置文件####[DEFAULT]logfile = ../log/client.logdownload_dir= ../temp####日志文件位置####[log]logfile = ../log/client.log####下载文件存放位置####[download]download_dir= ../temp


main.py

#!/usr/bin/env python# -*- coding:utf-8 -*-import configparser,osfrom client import FtpClientif __name__ == '__main__':    ftp = FtpClient()    ftp.connect('127.0.0.1',9999)    auth_tag=False    while auth_tag != True:        auth_tag=ftp.auth()    ftp.interactive()


client.py

#!/usr/bin/env python# -*- coding:utf-8 -*-# filename:client.pyimport socket, json, os, sys, hashlib, getpass, logging, configparser,time####读取配置文件####base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))config_file = os.path.join(base_dir, 'conf/client.conf')cf = configparser.ConfigParser()cf.read(config_file)####设定日志目录####if os.path.exists(cf.get('log', 'logfile')):    logfile = cf.get('log', 'logfile')else:    logfile = os.path.join(base_dir, 'log/client.log')####设定下载目录####if os.path.exists(cf.get('download', 'download_dir')):    download_dir = cf.get('download', 'download_dir')else:    download_dir = os.path.join(base_dir, 'temp')####设置日志格式###logging.basicConfig(level=logging.INFO,                    format='%(asctime)s %(levelname)s %(message)s',                    datefmt='%Y-%m-%d %H:%M:%S',                    filename=logfile,                    filemode='a+')def hashmd5(*args):  ####用于加密密码信息    m = hashlib.md5()    m.update(str(*args).encode())    return m.hexdigest()def ProcessBar(part, total):  ####进度条模块    if total != 0:        i = round(part * 100 / total)        sys.stdout.write(            '[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r')        sys.stdout.flush()class FtpClient(object):    def __init__(self):        self.client = socket.socket()    def connect(self, ip, port):        self.client.connect((ip, port))    def exec_linux_cmd(self, dict):  ####用于后面调用linux命令        logging.info(dict)  ####将发送给服务端的命令保存到日志中        self.client.send(json.dumps(dict).encode('utf-8'))        server_response = json.loads(self.client.recv(4096).decode('utf-8'))        if isinstance(server_response, list):            for i in server_response:                print(i)        else:            print(server_response)    def help(self):        info = '''        仅支持如下命令:        ls        du        pwd        cd dirname/cd ./cd ..        mkdir dirname        rm  filename        rmdir dirname        put filename        get filename        mv filename/dirname filename/dirname        newput filename (后续增加的新功能,支持断点续传)        newget filename (后续增加的新功能,支持断点续传)        '''        print(info)    def interactive(self):        while True:            self.pwd()  ####打印当前目录位置            cmd = input('>>>:').strip()            if len(cmd) == 0: continue            action = cmd.split()[0]            if hasattr(self, action):                func = getattr(self, action)                func(cmd)            else:                self.help()    def put(self, *args):  ####上传文件        cmd = args[0].split()        override = cmd[-1]  ####override:是否覆盖参数,放在最后一位        if override != 'True':            override = 'False'        # print(cmd,override)        if len(cmd) > 1:            filename = cmd[1]            if os.path.isfile(filename):                filesize = os.path.getsize(filename)                filemd5 = os.popen('md5sum %s' % filename).read().split()[                    0]  ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低                msg = {                    'action': 'put',                    'filename': filename,                    'filesize': filesize,                    'filemd5': filemd5,                    'override': override  ####True ,or False                }                logging.info(msg)                self.client.send(json.dumps(msg).encode('utf-8'))                server_response = self.client.recv(1024)  ####等待服务器确认信号,防止粘包                logging.info(server_response)                if server_response == b'file have exits, do nothing!':                    override_tag = input('文件已存在,要覆盖文件请输入yes >>>:')                    if override_tag == 'yes':                        self.put('put %s True' % filename)                    else:                        print('文件未上传')                else:                    self.client.send(b'client have ready to send')  ####发送确认信号,防止粘包,代号:P01                    server_response = self.client.recv(1024).decode('utf-8')                    print(server_response)  ####注意:用于打印服务器反馈信息,例如磁盘空间不足信息,不能取消                    if server_response == 'begin':                        fk = open(filename, 'rb')                        send_size = 0                        for line in fk:                            # print(len(line))                            send_size += len(line)                            self.client.send(line)                            ProcessBar(send_size, filesize)                        else:                            print('\r\n', '文件传输完毕')                            fk.close()                            server_response = self.client.recv(1024).decode('utf-8')                            print(server_response)            else:                print('文件不存在')        else:            print('请输入文件名')    def get(self, *args):  ####下载文件        cmd = args[0].split()        # print(args[0],cmd)        if len(cmd) > 1:            filename = cmd[1]            filepath = os.path.join(download_dir, filename)            if os.path.isfile(filepath):           ####判断下载目录是否已存在同名文件                override_tag = input('文件已存在,要覆盖文件请输入yes >>>:').strip()                if override_tag == 'yes':                    msg = {                        'action': 'get',                        'filename': filename,                        'filesize': 0,                        'filemd5': '',                        'override': 'True'                    }                    logging.info(msg)                    self.client.send(json.dumps(msg).encode('utf-8'))                    server_response = json.loads(self.client.recv(1024).decode('utf-8'))                    logging.info(server_response)                    if server_response == 'Filenotfound':                        print('File no found!')                    else:                        print(server_response)                        self.client.send(b'client have been ready to receive')  ####发送信号,防止粘包                        filesize = server_response['filesize']                        filemd5 = server_response['filemd5']                        receive_size = 0                        fk = open(filepath, 'wb')                        while filesize > receive_size:                            if filesize - receive_size > 1024:                                size = 1024                            else:                                size = filesize - receive_size                            data = self.client.recv(size)                            fk.write(data)                            receive_size += len(data)                            # print(receive_size, len(data))          ####打印数据流情况                            ProcessBar(receive_size, filesize)  ####打印进度条                        fk.close()                        receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]                        print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5)                        if receive_filemd5 == filemd5:                            print('文件接收完成!')                        else:                            print('Error,文件接收异常!')                else:                    print('下载取消')        else:            print('请输入文件名')    def newput(self, *args):  ####上传文件,具有断点续传功能        cmd = args[0].split()        override = cmd[-1]  ####override:是否覆盖参数,放在最后一位        if override != 'True':            override = 'False'        # print(cmd,override)        if len(cmd) > 1:            filename = cmd[1]            if os.path.isfile(filename):                filesize = os.path.getsize(filename)                filemd5 = os.popen('md5sum %s' % filename).read().split()[                    0]  ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低                msg = {                    'action': 'newput',                    'filename': filename,                    'filesize': filesize,                    'filemd5': filemd5,                    'override': override  ####True ,or False                }                logging.info(msg)                self.client.send(json.dumps(msg).encode('utf-8'))                server_response = self.client.recv(1024)  ####等待服务器确认信号,防止粘包                logging.info(server_response)                print(server_response)                if server_response == b'file have exits, and is a directory, do nothing!':                    print('文件已存在且为目录,请先修改文件或目录名字,然后再上传')                elif server_response == b'file have exits, do nothing!':                    override_tag = input('文件已存在,要覆盖文件请输入yes,要断点续传请输入r >>>:').strip()                    if override_tag == 'yes':                        self.client.send(b'no need to do anything')     ####服务端在等待是否续传的信号,发送给服务端确认(功能号:s1)                        time.sleep(0.5)   ####防止黏贴                        self.put('put %s True' % filename)                    elif override_tag == 'r':                        self.client.send(b'ready to resume from break point')       ####服务端在等待是否续传的信号,发送给服务端确认(功能号:s1)                        self.client.recv(1024) ####这边接收服务端发送过来的du信息,不显示,直接丢弃                        server_response = json.loads((self.client.recv(1024)).decode())                        print(server_response)                        if server_response['state'] == True:                                exits_file_size = server_response['position']                                fk = open(filename, 'rb')                                fk.seek(exits_file_size,0)                                send_size = exits_file_size                                for line in fk:                                    # print(len(line))                                    send_size += len(line)                                    self.client.send(line)                                    ProcessBar(send_size, filesize)                                else:                                    print('\r\n', '文件传输完毕')                                    fk.close()                                    server_response = self.client.recv(1024).decode('utf-8')                                    print(server_response)                        else:                            print(server_response['content'])                    else:                        self.client.send(b'no need to do anything')         ####服务端在等待是否续传的信号,发送给服务端确认(功能号:s1)                        print('文件未上传')                else:                    self.client.send(b'client have ready to send')  ####发送确认信号,防止粘包,代号:P01                    server_response = self.client.recv(1024).decode('utf-8')                    print(server_response)  ####注意:用于打印服务器反馈信息,例如磁盘空间不足信息,不能取消                    if server_response == 'begin':                        fk = open(filename, 'rb')                        send_size = 0                        for line in fk:                            # print(len(line))                            send_size += len(line)                            self.client.send(line)                            ProcessBar(send_size, filesize)                        else:                            print('\r\n', '文件传输完毕')                            fk.close()                            server_response = self.client.recv(1024).decode('utf-8')                            print(server_response)            else:                print('文件不存在')        else:            print('请输入文件名')    def newget(self, *args):  ####下载文件,具有断点续传功能        cmd = args[0].split()        # print(args[0],cmd)        if len(cmd) > 1:            filename = cmd[1]            filepath = os.path.join(download_dir, filename)            transfer_tag = True         ####传输控制信号,默认True为下载            resume_tag = False          ####断点续传信号            local_filesize = 0          ####本地文件大小,后面判断是否有同名文件使用            if os.path.isfile(filepath):           ####判断下载目录是否已存在同名文件                override_tag = input('文件已存在,要覆盖文件请输入yes,要断点续传请输入r >>>:').strip()                if override_tag == 'yes':                    pass                elif override_tag == 'r':                    local_filesize = os.path.getsize(filepath)                    resume_tag = True                else:                    print('下载取消')                    transfer_tag = False            if transfer_tag == True:                msg = {                    'action': 'newget',                    'filename': filename,                    'filesize': local_filesize,                    'filemd5': '',                    'override': 'True'                }                logging.info(msg)                self.client.send(json.dumps(msg).encode('utf-8'))                server_response = json.loads(self.client.recv(1024).decode('utf-8'))                logging.info(server_response)                if server_response == 'Filenotfound':                    print('File no found!')                else:                    print(server_response)                    self.client.send(b'client have been ready to receive')  ####发送信号,防止粘包                    filesize = server_response['filesize']                    filemd5 = server_response['filemd5']                    receive_size = local_filesize                    if resume_tag == True:                        fk = open(filepath, 'ab+')      ####用于断点续传                    else:                        fk = open(filepath, 'wb+')      ####用于覆盖或者新生成文件                    while filesize > receive_size:                        if filesize - receive_size > 1024:                            size = 1024                        else:                            size = filesize - receive_size                        data = self.client.recv(size)                        fk.write(data)                        receive_size += len(data)                        # print(receive_size, len(data))          ####打印数据流情况                        ProcessBar(receive_size, filesize)  ####打印进度条                    fk.close()                    receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]                    print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5)                    if receive_filemd5 == filemd5:                        print('文件接收完成!')                    else:                        print('Error,文件接收异常!')        else:            print('请输入文件名')    def pwd(self, *args):  ####查看用户目录        msg = {            'action': 'pwd',        }        self.exec_linux_cmd(msg)    def ls(self, *args):  ####查看文件信息        msg = {            'action': 'ls',        }        self.exec_linux_cmd(msg)    def du(self, *args):  ####查看当前目录大小        msg = {            'action': 'du',        }        self.exec_linux_cmd(msg)    def cd(self, *args):  ####切换目录        try:  ####如果是直接输入cd,dirname=''            dirname = args[0].split()[1]        except IndexError:            dirname = ''        msg = {            'action': 'cd',            'dir': dirname        }        self.exec_linux_cmd(msg)    def mkdir(self, *args):  ####生成目录        try:  ####如果是直接输入rm,跳出            dirname = args[0].split()[1]            msg = {                'action': 'mkdir',                'dirname': dirname,            }            self.exec_linux_cmd(msg)        except IndexError:            print('Not dirname input, do nothing.')            pass    def rm(self, *args):  ####删除文件        try:  ####如果是直接输入rm,跳出            filename = args[0].split()[1]            msg = {                'action': 'rm',                'filename': filename,                'confirm': True  ####确认是否直接删除标志            }            self.exec_linux_cmd(msg)        except IndexError:            print('Not filename input, do nothing.')            pass    def rmdir(self, *args):        try:  ####如果是直接输入rm,跳出            dirname = args[0].split()[1]            msg = {                'action': 'rmdir',                'dirname': dirname,                'confirm': True  ####确认是否直接删除标志            }            self.exec_linux_cmd(msg)        except IndexError:            print('Not dirname input, do nothing.')            pass    def mv(self,*args): ####实现功能:移动文件,移动目录,文件重命名,目录重命名        try:            objectname = args[0].split()[1]            dstname = args[0].split()[2]            msg = {                'action': 'mv',                'objectname': objectname,                'dstname': dstname            }            print(msg)            self.exec_linux_cmd(msg)        except Exception as e:            print(e)            pass    def auth(self):        user_name = input('请输入用户名>>>:').strip()        passwd = getpass.getpass('请输入密码>>>:').strip()  ####在linux上输入密码不显示        msg = {            'username': user_name,            'passwd': hashmd5(passwd)        }        self.client.send(json.dumps(msg).encode('utf-8'))        server_response = self.client.recv(1024).decode('utf-8')        if server_response == 'ok':            print('认证通过!')            return True        else:            print(server_response)            return Falseif __name__ == '__main__':    ftp = FtpClient()    ftp.connect('127.0.0.1', 9999)    auth_tag = False    while auth_tag != True:        auth_tag = ftp.auth()    ftp.interactive()



注:配置文件中的中文注释,可能会使程序在启动时报出ASCII decode error,可以去掉。

  另外服务端最好在Linux下启动,我在windows下启动日志输出模块会报错。