Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现

时间:2022-08-29 11:56:50

作业:

开发一个支持多用户在线的FTP程序

要求:

  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
    移动和重命名: mv filename/dirname filename/dirname
    上传文件:put filename [True] (True代表覆盖)
    下载文件:get filename [True]
    上传断点续传: newput filename [o/r] (o代表覆盖,r代表断点续传)
    下载断点续传: newget filename [o/r]
4.涉及到目录的操作,用户登录后,程序会给用户一个“锚位”----以用户名字命名的家目录,使用户无论怎么操作,都只能在这个目录底下。而在发给用户的目录信息时,隐去上层目录信息。
5.用户在创建时,磁盘配额大小默认是100M,在上传文件时,程序会计算当前目录大小加文件大小是否会超过配额上限。未超过,上传;超过,返回磁盘大小不够的信息。磁盘配额可通过用户管理程序修改。
6.文件上传和下载后都会进行MD5值比对,验证文件是否一致。
7.服务端和客户端都有显示进度条功能,启用该功能会降低文件传输速度,这是好看的代价。
8.文件断点续传,支持文件上传和下载断点续传。断点续传上传功能还会检测用户磁盘空间是否足够。(断点续传命令使用前面new+put/get命名,包含put/get所有功能,由于逻辑增多,代码复杂,特地保留原put/get,以备后用)。

程序结构:

Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现

完整代码:

1.客户端 

Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
#Author:Zheng Na

import os,sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  ####获取当前文件的上一级的上一级目录
sys.path.append(BASE_DIR)

from core.client import FtpClient

if __name__ == '__main__':
    ftp = FtpClient()
    ftp.connect('localhost', 9999)

    auth_tag = False
    count = 0
    while auth_tag != True:  ####功能:3次验证不通过即退出
        count += 1
        if count <= 3:
            auth_tag = ftp.auth()
        else:
            exit()

    ftp.interactive()
    ftp.close()
main.py
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
####用户端配置文件####
[DEFAULT]
logfile = ../log/client.log
download_dir= ../temp

####日志文件位置####
[log]
logfile = ../log/client.log

####下载文件存放位置####
[download]
download_dir= ../temp
client.conf
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
#Author:Zheng Na

import  os,configparser,logging

####读取配置文件####
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, encoding='utf-8')

####设定日志目录####
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+')
settings.py
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
# Author:Zheng Na

import socket,os,json,hashlib,sys,time,getpass,logging
import core.settings

def hashmd5(*args):  ####MD5加密
    m = hashlib.md5()
    m.update(str(*args).encode())
    ciphertexts = m.hexdigest()  ####密文
    return ciphertexts

def processbar(part, total):  ####进度条,运行会导致程序变慢
    if total != 0:
        done = int(50 * part / total)
        sys.stdout.write("\r[%s%s]" % ('' * done, '  ' * (50 - done)))  ####注意:一个方块对应2个空格
        sys.stdout.write('{:.2%}'.format(part / total) + ' ' * 3 + str(part) + '/' + str(total))
        sys.stdout.flush()

class FtpClient(object):
    def __init__(self):
        self.client = socket.socket()

    def connect(self, ip, port):  ####连接
        self.client.connect((ip, port))

    def auth(self):  ####用户认证
        username = input("请输入用户名>>>:").strip()
        # password = getpass.getpass("请输入密码>>>:").strip()  ####在linux上输入密码不显示,此模块在pycharm中无法使用
        password = input("请输入密码>>>:").strip()  ####Windows测试用
        password = hashmd5(password)
        msg = {
            'username': username,
            'password': password
        }
        self.client.send(json.dumps(msg).encode('utf-8'))
        server_response = self.client.recv(1024).decode('utf-8')
        logging.info(server_response)
        if server_response == 'ok':
            print("认证通过!")
            return True
        else:
            print(server_response)
            return False

    def interactive(self): ####交互
        while True:
            self.pwd('pwd')                       ####打印家目录
            cmd = input(">> ").strip()
            if len(cmd) == 0: continue
            cmd_str = cmd.split()[0]              ####用户输入的第一个值必定是命令
            if hasattr(self, cmd_str):            ####反射:判断一个对象中是否有字符串对应的方法或属性
                func = getattr(self, cmd_str)     ####利用反射来解耦:根据字符串去获取对象里对应的方法的内存地址或对应属性的值
                func(cmd)                         ####调用命令对应的方法
            else:
                self.help()

    def help(self):  ####帮助
        msg = '''
         仅支持如下命令:
         ls
         du
         pwd
         cd dirname/cd ./cd ..
         mkdir dirname
         rm  filename
         rmdir dirname
         mv filename/dirname filename/dirname  
         get filename [True] (True代表覆盖)
         put filename [True] (True代表覆盖)
         newget filename [o/r] (后续增加的新功能,支持断点续传,o代表覆盖,r代表断点续传)
         newput filename [o/r] (后续增加的新功能,支持断点续传,o代表覆盖,r代表断点续传)
         '''
        print(msg)

    def pwd(self, *args):  ####查看当前目录
        cmd_split = args[0].split()
        if len(cmd_split) == 1:
            msg = {'action': 'pwd'}
            self.exec_linux_cmd(msg)
        else:
            self.help()

    def ls(self, *args):  ####文件浏览
        cmd_split = args[0].split()
        if len(cmd_split) == 1:
            msg = {'action': 'ls'}
            self.exec_linux_cmd(msg)
        else:
            self.help()

    def du(self, *args):  ####查看当前目录大小
        cmd_split = args[0].split()
        if len(cmd_split) == 1:
            msg = {'action': 'du'}
            self.exec_linux_cmd(msg)
        else:
            self.help()

    def cd(self, *args):  ####切换目录
        cmd_split = args[0].split()
        if len(cmd_split) == 1:
            dirname = ''
        elif len(cmd_split) == 2:
            dirname = cmd_split[1]
        else:
            return help()

        msg = {
            "action": 'cd',
            "dirname": dirname
        }
        self.exec_linux_cmd(msg)

    def mkdir(self, *args):  ####生成目录
        cmd_split = args[0].split()
        if len(cmd_split) == 2:
            dirname = cmd_split[1]
            msg = {
                "action": 'mkdir',
                "dirname": dirname
            }
            self.exec_linux_cmd(msg)
        else:
            help()

    def rm(self, *args):  ####删除文件
        cmd_split = args[0].split()
        if len(cmd_split) == 2:
            filename = cmd_split[1]
            msg = {
                "action": 'rm',
                "filename": filename,
                "confirm": True  ####确认是否直接删除标志
            }
            self.exec_linux_cmd(msg)
        else:
            help()

    def rmdir(self, *args):  ####删除目录
        cmd_split = args[0].split()
        if len(cmd_split) == 2:
            dirname = cmd_split[1]
            msg = {
                "action": 'rmdir',
                "dirname": dirname,
                "confirm": True  ####确认是否直接删除标志
            }
            self.exec_linux_cmd(msg)
        else:
            help()

    def mv(self, *args):  ####实现功能:移动文件,移动目录,文件重命名,目录重命名
        cmd_split = args[0].split()
        if len(cmd_split) == 3:
            objname = cmd_split[1]
            dstname = cmd_split[2]
            msg = {
                "action": 'mv',
                "objname": objname,
                "dstname": dstname
            }
            self.exec_linux_cmd(msg)
        else:
            help()

    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):  ####判断是否为list类型
            for i in server_response:
                print(i)
        else:
            print(server_response)

    def get(self, *args):  ####下载文件
        cmd_split = args[0].split()
        override = cmd_split[-1]  ####override:是否覆盖参数,True表示覆盖,放在最后一位
        # print(override,type(override))
        if override != 'True':
            override = 'False'
        # print(override)
        if len(cmd_split) > 1:
            filename = cmd_split[1]
            filepath = os.path.join(core.settings.download_dir, filename)
            if override != 'True' and os.path.isfile(filepath):  ####判断下载目录是否已存在同名文件
                override_tag = input('文件已存在,要覆盖文件请输入yes >>>:').strip()
                if override_tag == 'yes':
                    self.put('put %s True' % filename)
                else:
                    print('下载取消')
            else:
                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
                    f = open(filepath, 'wb')
                    while filesize > receive_size:
                        if filesize - receive_size > 1024:
                            size = 1024
                        else:
                            size = filesize - receive_size
                        data = self.client.recv(size)
                        f.write(data)
                        receive_size += len(data)
                        processbar(receive_size, filesize)  ####打印进度条
                    f.close()
                    # receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                    receive_filemd5 = 'a'  ####Windows测试用
                    print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                    if receive_filemd5 == filemd5:
                        print('文件接收完成!')
                    else:
                        print('Error,文件接收异常!')
        else:
            help()

    def put(self, *args):  ####上传文件
        cmd_split = args[0].split()
        override = cmd_split[-1]  ####override:是否覆盖参数,True表示覆盖,放在最后一位
        if override != 'True':
            override = 'False'
        # print(cmd_split,override)

        if len(cmd_split) > 1:
            filename = cmd_split[1]
            filepath = os.path.join(core.settings.download_dir, filename)
            if os.path.isfile(filepath):
                filesize = os.path.getsize(filepath)  ####法1
                # filesize = os.stat(filepath).st_size  ####法2

                ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三步,代码量更多,效率也低
                # filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                filemd5 = 'a'  ####Windows测试

                msg = {
                    "action": 'put',
                    "filename": filename,
                    "filesize": filesize,
                    "filemd5": filemd5,
                    "override": override
                }
                # logging.info(msg)
                self.client.send(json.dumps(msg).encode('utf-8'))
                ###防止粘包,等服务器确认:这里最好列出一些标准请求码,告诉客户端是否有权限传输文件,类似200 403等
                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':
                        f = open(filepath, 'rb')
                        send_size = 0
                        for line in f:
                            send_size += len(line)
                            self.client.send(line)
                            processbar(send_size, filesize)
                        else:
                            print('\r\n', "file upload success...")
                            f.close()
                            server_response = self.client.recv(1024).decode('utf-8')
                            print(server_response)
            else:
                print(filename, 'is not exist')
        else:
            self.help()

    def newget(self, *args):  ####下载文件,具有断点续传功能
        cmd_split = args[0].split()
        tag = cmd_split[-1]  ####tag:o代表覆盖,r代表续传,放在最后一位

        if len(cmd_split) > 1:
            filename = cmd_split[1]
            filepath = os.path.join(core.settings.download_dir, filename)
            if tag not in ('o', 'r'):
                if os.path.isfile(filepath):  ####判断下载目录是否已存在同名文件
                    tag = input('文件已存在,要覆盖文件请输入o,要断点续传请输入r >>>:').strip()
                else:
                    tag = 'o'

            if tag in ('o', 'r'):
                if tag == 'r':
                    local_filesize = os.path.getsize(filepath)
                else:
                    local_filesize = 0  # 本地文件大小

                msg = {
                    'action': 'newget',
                    'filename': filename,
                    'filesize': local_filesize,
                    'filemd5': '',
                }
                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 tag == 'r':
                        f = open(filepath, 'ab+')   ####用于断点续传
                    else:
                        f = open(filepath, 'wb+')  ####用于覆盖或者新生成文件
                    while filesize > receive_size:
                        if filesize - receive_size > 1024:
                            size = 1024
                        else:
                            size = filesize - receive_size
                        data = self.client.recv(size)
                        f.write(data)
                        receive_size += len(data)
                        # print(receive_size, len(data))  ####打印数据流情况
                        processbar(receive_size, filesize)  ####打印进度条
                    f.close()
                    # receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                    receive_filemd5 = 'a'  ####Windows测试用
                    print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                    if receive_filemd5 == filemd5:
                        print('文件接收完成!')
                    else:
                        print('Error,文件接收异常!')
            else:
                print("文件未下载")
        else:
            help()

    def newput(self, *args):  ####上传文件,具有断点续传功能
        cmd_split = args[0].split()
        tag = cmd_split[-1]  ####tag:r代表续传,o代表覆盖,放在最后一位
        if tag not in ('o', 'r'):
            tag = 'unknown'
        # print(cmd_split,tag)

        if len(cmd_split) > 1:
            filename = cmd_split[1]
            filepath = os.path.join(core.settings.download_dir, filename)
            if os.path.isfile(filepath):
                filesize = os.path.getsize(filepath)  ####法1
                # filesize = os.stat(filepath).st_size  ####法2

                ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低
                # filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                filemd5 = 'a'  # Windows测试

                msg = {
                    "action": 'newput',
                    "filename": filename,
                    "filesize": filesize,
                    "filemd5": filemd5,
                    "tag": tag
                }
                # logging.info(msg)
                self.client.send(json.dumps(msg).encode('utf-8'))  ####发送msg
                server_response1 = self.client.recv(1024).decode()  ####接收文件存在或者文件不存在
                # logging.info(server_response)
                print(server_response1)

                if server_response1 == '文件存在':  ####再确认一遍tag
                    if tag == 'unknown':
                        tag = input('文件已存在,要覆盖文件请输入o,要断点续传请输入r >>>:').strip()
                        if tag not in ('o', 'r'):
                            tag = 'unknown'
                else:  ####文件不存在时
                    tag = 'o'


                self.client.send(tag.encode())
                server_response2 = json.loads(self.client.recv(1024).decode('utf-8'))
                # print('server_response2:', server_response2)
                content = server_response2['content']

                if tag == 'o' or tag == 'r':
                    if content == 'begin':
                        position = server_response2['position']
                        print(position)
                        f = open(filepath, 'rb')
                        f.seek(position, 0)
                        send_size = position
                        for line in f:
                            send_size += len(line)
                            self.client.send(line)
                            processbar(send_size, filesize)
                        else:
                            print('\n', "file upload success...")
                            f.close()
                            server_response3 = self.client.recv(1024).decode('utf-8')  ####服务端对比md5后发送是否成功接收文件,成功或失败
                            print(server_response3)
                    else:
                        print(content)  ####content:服务器已存在同名文件 或。。。
                else:
                    print(content)  ####content:文件未上传
            else:
                print(filename, 'is not exist')
        else:
            self.help()

    def newput2(self, *args):  ####上传文件,具有断点续传功能,网友写的,与我写的newput功能差不多
        cmd_split = args[0].split()
        override = cmd_split[-1]  ####override:是否覆盖参数,放在最后一位
        if override != 'True':
            override = 'False'
        # print(cmd_split,override)

        if len(cmd_split) > 1:
            filename = cmd_split[1]
            filepath = os.path.join(core.settings.download_dir, filename)
            if os.path.isfile(filepath):
                filesize = os.path.getsize(filepath)  ####法1
                # filesize = os.stat(filepath).st_size  ####法2
                ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低
                filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                filemd5 = 'a'  ####Windows测试
                msg = {
                    "action": 'newput2',
                    "filename": filename,
                    "filesize": filesize,
                    "filemd5": filemd5,
                    "override": override
                }
                # logging.info(msg)
                self.client.send(json.dumps(msg).encode('utf-8'))
                ####防止粘包,等服务器确认:这里最好列出一些标准请求码,告诉客户端是否有权限传输文件,类似200 403等
                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('utf-8'))
                        print(server_response)
                        if server_response['state'] == 'True':
                            exist_file_size = server_response['position']
                            f = open(filepath, 'rb')
                            f.seek(exist_file_size, 0)
                            send_size = exist_file_size
                            for line in f:
                                send_size += len(line)
                                self.client.send(line)
                                processbar(send_size, filesize)
                            else:
                                print('\r\n', '文件传输完毕')
                                f.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':
                        f = open(filepath, 'rb')
                        send_size = 0
                        for line in f:
                            send_size += len(line)
                            self.client.send(line)
                            processbar(send_size, filesize)
                        else:
                            print('\r\n', "file upload success...")
                            f.close()
                            server_response = self.client.recv(1024).decode('utf-8')
                            print(server_response)
            else:
                print(filename, 'is not exist')
        else:
            self.help()

    def close(self):
        self.client.close()
client.py

2.服务端

Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
# Author:Zheng Na

####os.path.abspath(__file__) 获取当前当前文件的绝对路径
####os.path.dirname()获取当前文件上一层目录
import os,sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  ####获取当前文件的上一级的上一级目录
sys.path.append(BASE_DIR)


import socketserver
from core.server import MyTCPHandler
from core.usermanagement import UserOpr

if __name__ == '__main__':

    mainpage = '''
    主页
        1、启动服务器
        2、进入用户管理
        退出请按q
    '''

    while True:
        print('\033[1;35m{}\033[0m'.format(mainpage))
        choice = input('>>>:')
        if choice == 'q':
            exit()
        elif choice == '1':
            HOST, PORT = "localhost", 9999
            server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
            server.serve_forever()
        elif choice == '2':
            useropr = UserOpr()
            # useropr.query_all_user()  ####查询所有用户信息
            useropr.interactive()
        else:
            print("\033[1;31m输入错误,请重新输入\033[0m")
            continue
main.py
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
####用户端配置文件####
[DEFAULT]
logfile = ../log/server.log
usermgr_log = ../log/usermgr.log
upload_dir = ../user_files
userinfo_dir = ../user_info

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

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

####用户信息存放位置####
[userinfo]
userinfo_dir = ../user_info
server.conf
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
#Author:Zheng Na

import os,configparser,logging

####读取配置文件####
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  ####获取当前文件的上一级的上一级目录
config_file = os.path.join(base_dir, 'conf/server.conf')  #####将2个路径组合后返回
cf = configparser.ConfigParser()
cf.read(config_file,encoding='utf-8')  # 不编码会报错:UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 12: illegal multibyte sequence

####设定日志目录####
'''先判断日志文件是否存在,如果不存在,则创建'''
if os.path.exists(cf.get('log', 'usermgr_log')):
    usermgr_log = cf.get('log', 'usermgr_log')
else:
    usermgr_log = 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('userinfo', 'userinfo_dir')):
    userinfo_dir = cf.get('userinfo', 'userinfo_dir')
else:
    userinfo_dir = os.path.join(base_dir, 'user_info')

####设置日志格式####
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename=usermgr_log,
                    filemode='a+')
settings.py
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
#Author:Zheng Na

import os,json
import core.settings

def query_user(username):  ####查询用户
    filelist = os.listdir(core.settings.userinfo_dir)  ####列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印
    dict = {}
    for filename in filelist:
        with open (os.path.join( core.settings.userinfo_dir,filename),'r',encoding='utf-8') as f:
            content = json.load(f)  ####json反序列化
            if content['username'] == username:
                dict = {'filename':filename,'content':content}
                # print("查询结果:",dict)
                return dict
common.py
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
# Author:Zheng Na

import socketserver,sys,json,os,time,shutil
import core.common

def processbar(part, total):  ####进度条,运行会导致程序变慢
    if total != 0:
        done = int(50 * part / total)
        sys.stdout.write("\r[%s%s]" % ('' * done, '  ' * (50 - done)))  ####注意:一个方块对应2个空格
        sys.stdout.write('{:.2%}'.format(part / total)+' '*3+str(part)+'/'+str(total))
        sys.stdout.flush()

def timestamp_to_formatstringtime(timestamp):  ####时间戳转化为格式化的字符串
    structtime = time.localtime(timestamp)
    formatstringtime = time.strftime("%Y%m%d %H:%M:%S",structtime)
    return formatstringtime

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        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.spacesize = auth_result['content']['spacesize']
                auth_tag = True
                print(self.username,self.spacesize)
                user_homedir = os.path.join(core.settings.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.decode())
            # logging.info(self.client_address)
            if not self.data:
                print(self.client_address, "断开了")
                break
            cmd_dic = json.loads(self.data.decode('utf-8'))
            # print(cmd_dic)
            action = cmd_dic["action"]
            # logging.info(cmd_dic)
            if hasattr(self, action):
                func = getattr(self, action)
                func(cmd_dic)
            else:
                print("未支持指令:",action)
                # logging.info('current direcory: %s' % self.positoion)

    def auth(self):  ####用户认证
        self.data = json.loads(self.request.recv(1024).decode('utf-8'))
        print(self.data)
        recv_username = self.data['username']
        recv_password = self.data['password']
        query_result = core.common.query_user(recv_username)
        print(query_result)
        if query_result == None:
            self.request.send(b'user does not exist')
        elif query_result['content']['password'] == recv_password:
            self.request.send(b'ok')
            return query_result  ####返回查询结果
        elif query_result['content']['password'] != recv_password:
            self.request.send(b'password error')
        else:
            self.request.send(b'unkonwn error')

    def pwd(self,*args):  ####查看当前目录
        current_position = self.position
        result = current_position.replace(core.settings.file_dir,'')  ####截断目录信息,使用户只能看到自己的家目录信息
        print(result)
        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):
            f_abspath = os.path.join(self.position,f)  ####给出文件的绝对路径,不然程序会找不到文件
            if os.path.isdir(f_abspath):
                type = 'd'
            elif os.path.isfile(f_abspath):
                type = 'f'
            else:
                type = 'unknown'
            fsize = os.path.getsize(f_abspath)
            ftime = timestamp_to_formatstringtime(os.path.getctime(f_abspath))
            result.append('%-20s%-7s%-10s%-23s' % (f,type,fsize,ftime))
        self.request.send(json.dumps(result).encode('utf-8'))

    def du_calc(self): # 注意不能使用os.path.getsize('D:\python-study\s14')返回的是所有目录大小的和
        '''统计纯文件和目录占用空间大小,结果小于在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 in dirs:              #计算目录占用空间,Linux中每个目录占用4096bytes,实际上也可以按这个值来相加
                    dirsize += os.path.getsize(os.path.join(root,d))
                for f in files:             #计算文件占用空间
                    filesize += os.path.getsize(os.path.join(root,f))
            totalsize = dirsize + filesize
            return totalsize

    def du(self,*args):  ####查看当前目录大小
        totalsize = self.du_calc()
        result =  'current directory total sizes: %d' % totalsize
        print(result)
        self.request.send(json.dumps(result).encode('utf-8'))
        return totalsize

    def cd(self,*args):  ####切换目录,这个函数实在是没怎么看懂
        print(*args)
        user_homedir = os.path.join(core.settings.file_dir,self.username)
        cmd_dic = args[0]
        error_tag = False
        '''判断目录信息'''
        if cmd_dic['dirname'] == '':
            self.position = user_homedir
        elif cmd_dic['dirname'] in ('.','/') or '//' in cmd_dic['dirname']:  ####'.','/','//','///+'匹配
            pass
        elif cmd_dic['dirname'] == '..':
            if user_homedir != self.position and user_homedir in self.position:  ####当前目录不是家目录,并且当前目录是家目录下的子目录
                self.position = os.path.dirname(self.position)
        elif '.' not in cmd_dic['dirname'] and os.path.isdir(os.path.join(self.position,cmd_dic['dirname'])):####'.' not in cmd_dict['dir'] 防止../..输入
            self.position = os.path.join(self.position,cmd_dic['dirname'])
        else:
            error_tag = True

        if error_tag:
            result = 'Error,%s is not path here,or path does not exist!' % cmd_dic['dirname']
            self.request.send(json.dumps(result).encode('utf-8'))
        else:
            self.pwd()

    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 success' % 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 deleted." % filename
            else:
                result = 'Not file deleted'
        elif os.path.isdir(file_abspath):
            result = '%s is a dir,please use 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']
        dir_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(dir_abspath):
            if confirm == True:
                shutil.rmtree(dir_abspath)
                result = '%s have been deleted.' % dirname
            else:
                result = 'Not dir deleted.'
        elif os.path.isfile(dir_abspath):
            result = '%s is a file,please use rm' % dirname
        else:
            result = 'directory %s not exist!' % dirname
        self.request.send(json.dumps(result).encode('utf-8'))

    def mv(self,*args):  ####实现功能:移动文件,移动目录,文件重命名,目录重命名
        try:
            print(args)
            objname = args[0]['objname']
            dstname = args[0]['dstname']
            obj_abspath = os.path.join(self.position,objname)
            dst_abspath = os.path.join(self.position,dstname)
            if os.path.isfile(obj_abspath):
                if os.path.isdir(dst_abspath) or not os.path.exists(dst_abspath):
                    shutil.move(obj_abspath,dst_abspath)
                    result = 'move success'
                elif os.path.isfile(dst_abspath):
                    result = 'moving cancel,file has been exist.'
            elif os.path.isdir(obj_abspath):
                if os.path.isdir(dst_abspath) or not os.path.exists(dst_abspath):
                    shutil.move(obj_abspath,dst_abspath)
                    result = 'move success'
                elif os.path.isfile(dst_abspath):
                    result = 'moving cancel,%s is a file.'% dst_abspath
            else:
                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 get(self,*args):  ####发送给客户端文件
        cmd_dic = args[0]
        filename = cmd_dic['filename']
        filepath = os.path.join(self.position, filename)
        if os.path.isfile(filepath):
            filesize = os.path.getsize(filepath)
            ####直接调用系统命令取得MD5值,如果使用hashlib,需要写open打开文件-》read读取文件(可能文件大会很耗时)-》m.update计算三部,代码量更多,效率也低
            # filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
            filemd5 = 'a'  ####Windows测试用
            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信号,下一步发送文件
            f = open(filepath, 'rb')
            send_size = 0
            for line in f:
                send_size += len(line)
                self.request.send(line)
                # processbar(send_size, filesize)     ####服务端进度条,不需要可以注释掉
            else:
                print('文件传输完毕')
                f.close()
        else:
            print(filepath, '文件未找到')
            self.request.send(json.dumps('Filenotfound').encode('utf-8'))

    def put(self, *args):  ####接收客户端文件
        cmd_dic = args[0]
        filename = os.path.basename(cmd_dic['filename'])  ####传输进来的文件名可能带有路径,将路径去掉
        filesize = cmd_dic['filesize']
        filemd5 = cmd_dic['filemd5']
        override = cmd_dic['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.spacesize, current_size, filesize)
            if self.spacesize >= current_size + filesize:
                self.request.send(b'begin')  ####发送开始传输信号
                f = 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)
                    f.write(data)
                    receive_size += len(data)
                    # print(receive_size,len(data))   ####打印每次接收的数据
                    # processbar(receive_size, filesize)  ####服务端进度条,不需要可以注释掉
                else:
                    print("file [%s] has uploaded..." % filename)
                    f.close()
                # receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
                receive_filemd5 = 'a'  ####windows 测试用
                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.spacesize,current_size, self.spacesize - current_size, filesize))

    def newget(self, *args):  ####发送给客户端文件,具有断点续传功能
        # print('get receive the cmd',args[0])
        cmd_dic = args[0]
        filename = cmd_dic['filename']
        send_size = cmd_dic['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]
            filemd5 = 'a' #Windows测试用
            msg = {
                'action': 'newget',
                'filename': filename,
                'filesize': filesize,
                'filemd5': filemd5,
            }
            print(msg)
            self.request.send(json.dumps(msg).encode('utf-8'))
            self.request.recv(1024)  ####接收ACK信号,下一步发送文件
            f = open(file_path, 'rb')
            f.seek(send_size,0)
            for line in f:
                send_size += len(line)
                self.request.send(line)
                # processbar(send_size, filesize)     ####服务端进度条,不需要可以注释掉
            else:
                print('文件传输完毕')
                f.close()

        else:
            print(file_path, '文件未找到')
            self.request.send(json.dumps('Filenotfound').encode('utf-8'))

    def newput(self, *args):  ####接收客户端文件,具有断点续传功能
        cmd_dict = args[0]
        filename = os.path.basename(cmd_dict['filename'])  ####传输进来的文件名可能带有路径,将路径去掉
        filesize = cmd_dict['filesize']
        filemd5 = cmd_dict['filemd5']
        tag = cmd_dict['tag']
        receive_size = 0
        file_path = os.path.join(self.position, filename)
        if os.path.isfile(file_path):  ####检测文件是否已经存在
            self.request.send('文件存在'.encode())
            tag = self.request.recv(1024).decode()  ####接收客户端ack信号
            if tag == 'o':
                os.remove(file_path)####如果文件已经存在,先删除,再计算磁盘空间大小
                self.upload(tag,filename, filemd5, filesize, file_path, receive_size)
            elif tag == 'r':
                exist_file_size = os.path.getsize(file_path)
                if exist_file_size <= filesize:
                    receive_size = exist_file_size
                    self.upload(tag,filename, filemd5, filesize, file_path, receive_size)
                else:
                    print('服务器已存在同名文件且比原文件大')
                    msg = {
                        "content": '服务器已存在同名文件且比原文件大'
                    }
                    self.request.send(json.dumps(msg).encode('utf-8'))
            else:
                msg = {
                    "content": '文件未上传'
                }
                self.request.send(json.dumps(msg).encode('utf-8'))
        else:  ####文件不存在:如果文件不存在的话,就不用管tag了,直接计算磁盘空间,然后上传
            self.request.send('文件不存在!'.encode())
            tag = self.request.recv(1024).decode()  ####接收客户端ack信号
            self.upload(tag,filename,filemd5,filesize,file_path,receive_size)

    def upload(self,tag,filename,filemd5,filesize,file_path,receive_size):
        current_size = self.du_calc()
        print('用户总空间:',self.spacesize, '目前剩余空间:',current_size,'文件大小:', filesize)
        if tag == 'r':
            needrecv_size = filesize - receive_size
        else:
            needrecv_size = filesize
        if self.spacesize >= current_size + needrecv_size:
            msg = {
                "position":receive_size,
                "content":'begin'
            }
            self.request.send(json.dumps(msg).encode('utf-8'))   ####发送开始传输信号
            if tag == 'r':
                f = open(file_path, 'ab')
            else:
                f = 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)
                f.write(data)
                receive_size += len(data)
                # processbar(receive_size, filesize)  ####服务端进度条,不需要可以注释掉
            f.close()
            # receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
            receive_filemd5 = 'a'
            print('\r\n', filename, '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 = {
                "content":'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % (
                    self.spacesize, current_size, self.spacesize - current_size, filesize)
            }
            self.request.send(json.dumps(msg).encode('utf-8'))   ##

    def newput2(self, *args):  ####接收客户端文件,具有断点续传功能
        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':           ####执行断点续传功能
                    exist_file_size = os.path.getsize(file_path)
                    current_size = self.du()
                    time.sleep(0.5) ####防止粘包
                    print('用户空间上限:%d, 当前已用空间:%d, 已存在文件大小:%d, 上传文件大小:%d ' % (self.spacesize,current_size,exist_file_size,filesize))
                    if self.spacesize >= (current_size - exist_file_size + filesize):  ####判断剩余空间是否足够
                        if exist_file_size < filesize:
                            receive_size = exist_file_size
                            print('服务器上已存在的文件大小为:',exist_file_size)
                            msg = {
                                'state': True,
                                'position': exist_file_size,
                                'content': 'ready to receive file'
                            }
                            self.request.send(json.dumps(msg).encode('utf-8'))
                            f = 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)
                                f.write(data)
                                receive_size += len(data)
                                # print(receive_size,len(data))   ####打印每次接收的数据
                                # processbar(receive_size, filesize)  ####服务端进度条,不需要可以注释掉

                            f.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.spacesize, current_size, filesize)
            if self.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.spacesize, current_size, self.spacesize - current_size, filesize))
server.py
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
#Author:Zheng Na
import os,time,json,shutil,hashlib
import core.common


def hashmd5(self, *args):
    m = hashlib.md5()
    m.update(str(*args).encode())
    ciphertexts = m.hexdigest()  # 密文
    return ciphertexts

# 用户操作类
class UserOpr(object):
    def __init__(self):
        pass

    def query_userinfo(self,username):
        query_result = core.common.query_user(username)
        if query_result != None: # 用户存在
            print(query_result)
        else:
            print("用户不存在")

    def save_userinfo(self,username):             # 保存用户信息
        query_result = core.common.query_user(username)  # 检查是否已存在同名用户,如果没有查询结果应该为None
        if query_result == None:                 # 用户不存在
            id = time.strftime('%Y%m%d%H%M%S', time.localtime()) # 将结构化时间(即元组)转换成格式化的字符串,比如20181211110148
            password = '123456'
            userinfo = {
                'username':username,
                'id':id,
                'phonenumber':'',
                'password':hashmd5(password),
                'spacesize': 104857600, ## 初始分配100MB存储空间
                'level':1 # 会员等级,初始为1,普通会员
            }

            with open(os.path.join(core.settings.userinfo_dir,id),'w',encoding='utf-8') as f:
                json.dump(userinfo,f)
                print("用户信息保存完毕")
                try:                        # 创建用户家目录
                    os.mkdir(os.path.join(core.settings.file_dir,username))
                    print('用户目录创建成功!')
                except Exception as e:
                    print('用户目录创建失败!',e)

        else:
            print("用户名重复,信息未保存")


    def change_userinfo(self,username): # 修改用户信息
        query_result = core.common.query_user(username)  # 检测用户是否存在,不存在不处理
        if query_result != None:                 # 用户存在
            filename = query_result['filename']
            userinfo = query_result['content']
            print('before update: ', userinfo)
            update_item = input("请输入要修改的项目,例如password,phonenumber,spacesize,level:")

            if update_item in ('username','id'):
                print(update_item, "项不可更改")
            elif update_item in ('password','phonenumber','spacesize','level'):
                print("update item: %s" % update_item)
                update_value = input("请输入要修改的项目的新值:")
                if update_item == 'password':
                    userinfo[update_item] = hashmd5(update_value)
                else:
                    userinfo[update_item] = update_value
                with open(os.path.join(core.settings.userinfo_dir, filename), 'w', encoding='utf-8') as f:
                    json.dump(userinfo, f)
                    print(update_item, "项用户信息变更保存完毕")
                    print('after update: ', userinfo)
            else:
                print('输入信息错误,', update_item, '项不存在')
        else:
            print('用户不存在,无法修改')

    def delete_user(self,username):
        query_result = core.common.query_user(username)  # 检测用户是否存在,不存在不处理
        if query_result != None:                 # 用户存在
            filename = query_result['filename']
            userfile_path = os.path.join(core.settings.userinfo_dir,filename)
            os.remove(userfile_path)
            query_result_again = core.common.query_user(username)
            if query_result_again == None:
                print('用户信息文件删除成功!')
                try:
                    shutil.rmtree(os.path.join(core.settings.file_dir,username))
                    print('用户家目录删除成功')
                except Exception as e:
                    print('用户家目录删除失败:',e)
            else:
                print('用户信息文件删除失败!')
        else:
            print('用户不存在或者已经被删除')

    def query_all_user(self):           # 查询所有用户信息,用于调试使用
        filelist = os.listdir(core.settings.userinfo_dir)
        if filelist != []:
            for filename in filelist:
                with open(os.path.join(core.settings.userinfo_dir,filename),'rb') as f:
                    userinfo = json.load(f)
                    print(filename,userinfo)
        else:
            print("用户信息为空")

    def interactive(self):
        userpage = '''
        用户管理界面
            1、新增用户
            2、查询用户
            3、修改用户
            4、删除用户
            退出请按q
            返回上一界面请按r
         '''

        userpage_data = {
            '1': 'save_userinfo',
            '2': 'query_userinfo',
            '3': 'change_userinfo',
            '4': 'delete_user'
        }

        while True:
            print('\033[1;35m{}\033[0m'.format(userpage))
            choice = input('请输入你的选择:').strip()

            if choice == 'q':
                exit("退出程序!")
            elif choice == 'r':
                break
            elif choice in userpage_data:
                username = input("请输入用户名:").strip()
                if username == '':
                    print('用户不能为空')
                    continue
                if hasattr(self,userpage_data[choice]):
                    f = getattr(self, userpage_data[choice])
                    f(username)
            else:
                print("\033[1;31m输入错误,请重新输入\033[0m")
                continue
usermanagement.py

运行示例:

Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/ftp/ftp_server/bin/main.py

    主页
        1、启动服务器
        2、进入用户管理
        退出请按q
    
>>>:2

        用户管理界面
            1、新增用户
            2、查询用户
            3、修改用户
            4、删除用户
            退出请按q
            返回上一界面请按r
         
请输入你的选择:1
请输入用户名:xiaoming
用户信息保存完毕
用户目录创建成功!

        用户管理界面
            1、新增用户
            2、查询用户
            3、修改用户
            4、删除用户
            退出请按q
            返回上一界面请按r
         
请输入你的选择:2
请输入用户名:xiaoming
{'filename': '20181220164706', 'content': {'username': 'xiaoming', 'id': '20181220164706', 'phonenumber': '', 'password': 'd41d8cd98f00b204e9800998ecf8427e', 'spacesize': 104857600, 'level': 1}}

        用户管理界面
            1、新增用户
            2、查询用户
            3、修改用户
            4、删除用户
            退出请按q
            返回上一界面请按r
         
请输入你的选择:3
请输入用户名:xiaoming
before update:  {'username': 'xiaoming', 'id': '20181220164706', 'phonenumber': '', 'password': 'd41d8cd98f00b204e9800998ecf8427e', 'spacesize': 104857600, 'level': 1}
请输入要修改的项目,例如password,phonenumber,spacesize,level:phonenumber
update item: phonenumber
请输入要修改的项目的新值:1234567890
phonenumber 项用户信息变更保存完毕
after update:  {'username': 'xiaoming', 'id': '20181220164706', 'phonenumber': '1234567890', 'password': 'd41d8cd98f00b204e9800998ecf8427e', 'spacesize': 104857600, 'level': 1}

        用户管理界面
            1、新增用户
            2、查询用户
            3、修改用户
            4、删除用户
            退出请按q
            返回上一界面请按r
         
请输入你的选择:4
请输入用户名:xiaoming
用户信息文件删除成功!
用户家目录删除成功

        用户管理界面
            1、新增用户
            2、查询用户
            3、修改用户
            4、删除用户
            退出请按q
            返回上一界面请按r
         
请输入你的选择:q
退出程序!

Process finished with exit code 1
用户管理程序运行示例
Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现Python3学习之路~8.6 开发一个支持多用户在线的FTP程序-代码实现
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/ftp/ftp_server/bin/main.py

    主页
        1、启动服务器
        2、进入用户管理
        退出请按q
    
>>>:1




D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/ftp/ftp_client/bin/main.py
请输入用户名>>>:zhengna
请输入密码>>>:123123
认证通过!
\zhengna
>> pwd
\zhengna
\zhengna
>> ls
filename            type   bytes     creationtime           
test                d      0         20181214 17:17:05      
test.txt            f      5028331   20181220 15:43:55      
vedio2.avi          f      86453774  20181214 17:17:35      
\zhengna
>> du
current directory total sizes: 96510422
\zhengna
>> cd test
\zhengna\test
\zhengna\test
>> ls
filename            type   bytes     creationtime           
test2               d      0         20181217 11:21:07      
\zhengna\test
>> rmdir test2
test2 have been deleted.
\zhengna\test
>> cd ..
\zhengna
\zhengna
>> rm test
test is a dir,please use rmdir
\zhengna
>> rm test.txt
test.txt have been deleted.
\zhengna
>> mkdir aa
aa created success
\zhengna
>> mv aa bb
move success
\zhengna
>> put test.ttx
test.ttx is not exist
\zhengna
>> put test.txt
begin
[██████████████████████████████████████████████████]100.00%   5028331/5028331
 file upload success...
file received successfully!
\zhengna
>> put test.txt 
文件已存在,要覆盖文件请输入yes >>>:yes
begin
[██████████████████████████████████████████████████]100.00%   5028331/5028331
 file upload success...
file received successfully!
\zhengna
>> put test.txt True
begin
[██████████████████████████████████████████████████]100.00%   11178154/11178154
 file upload success...
file received successfully!
\zhengna
>> get test.ttx
File no found!
\zhengna
>> get test.txt
{'action': 'get', 'filename': 'test.txt', 'filesize': 11178154, 'filemd5': 'a', 'override': 'True'}
[██████████████████████████████████████████████████]100.00%   11178154/11178154
 test.txt md5: a 原文件md5: a
文件接收完成!
\zhengna
>> get test.txt
文件已存在,要覆盖文件请输入yes >>>:yes
begin
[██████████████████████████████████████████████████]100.00%   11178154/11178154
 file upload success...
file received successfully!
\zhengna
>> get test.txt True
{'action': 'get', 'filename': 'test.txt', 'filesize': 11178154, 'filemd5': 'a', 'override': 'True'}
[██████████████████████████████████████████████████]100.00%   11178154/11178154
 test.txt md5: a 原文件md5: a
文件接收完成!
\zhengna
>> newput test.txt
文件存在
文件已存在,要覆盖文件请输入o,要断点续传请输入r >>>:o
0
[██████████████████████████████████████████████████]100.00%   11178154/11178154
 file upload success...
file received successfully!
\zhengna
>> newput test.txt
文件存在
文件已存在,要覆盖文件请输入o,要断点续传请输入r >>>:r
11178154

 file upload success...
file received successfully!
\zhengna
>> newget test.txt
文件已存在,要覆盖文件请输入o,要断点续传请输入r >>>:o
{'action': 'newget', 'filename': 'test.txt', 'filesize': 11178154, 'filemd5': 'a'}
[██████████████████████████████████████████████████]100.00%   11178154/11178154
 test.txt md5: a 原文件md5: a
文件接收完成!
\zhengna
>> newget test.txt 
文件已存在,要覆盖文件请输入o,要断点续传请输入r >>>:r
{'action': 'newget', 'filename': 'test.txt', 'filesize': 11178154, 'filemd5': 'a'}

 test.txt md5: a 原文件md5: a
文件接收完成!
\zhengna
>> newput vedio.avi
文件不存在!
Error, disk space do not enough! Nothing done! Total: 104857600, current: 97631928, rest:7225672, filesize:86453774
\zhengna
>> 
主程序运行示例

参考:http://blog.51cto.com/tryagaintry/1969589

总结:这是我第一次写一个这么复杂的程序,虽然大多数的代码都是参考别人写好的。实现它我大概用了2周左右的时间,在这过程中,我一直都在努力思考,尽量让自己弄明白每段代码实现了什么功能?为什么这么写?有没有更好的实现方式?我知道最终的程序并不完美,但是对我来说,重要的不是我在上方贴的大段大段的代码,而是在这2周码代码的过程中,我从中学到了什么。