• [ python ] FTP作业进阶


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

    要求:

    1. 用户加密认证
    2. 允许同时多用户登录
    3. 每个用户有自己的家目录 ,且只能访问自己的家目录
    4. 对用户进行磁盘配额,每个用户的可用空间不同
    5. 允许用户在ftp server上随意切换目录
    6. 允许用户查看当前目录下文件
    7. 允许上传和下载文件,保证文件一致性
    8. 文件传输过程中显示进度条
    9. 附加功能:支持文件的断点续传

    之前作业的链接地址:https://www.cnblogs.com/hukey/p/8909046.html     这次的重写是对上次作业的补充,具体实现功能点如下:

    README

    # 作者介绍:
        author: hkey
    
    # 博客地址:
        https://www.cnblogs.com/hukey/p/10182876.html
        
    # 功能实现:
    
        作业:开发一个支持多用户在线的FTP程序
    
        要求:
    
            用户加密认证
            允许同时多用户登录
            每个用户有自己的家目录 ,且只能访问自己的家目录
            对用户进行磁盘配额,每个用户的可用空间不同
            允许用户在ftp server上随意切换目录
            允许用户查看当前目录下文件
            允许上传和下载文件,保证文件一致性
            文件传输过程中显示进度条
            附加功能:支持文件的断点续传
            
    
    # 目录结构:
                
    FTP
    ├── ftp_client/        # ftp客户端程序
    │   └── ftp_client.py    # 客户端主程序
    └── ftp_server/        # ftp服务端程序
        ├── bin/
        │   ├── __init__.py
        │   └── start.py    
        ├── conf/        # 配置文件目录
        │   ├── __init__.py
        │   ├── settings.py
        │   └── user.list    # 记录注册用户名
        ├── db/            # 用户数据库
        ├── home/        # 用户家目录
        ├── logs/        # 记录日志目录
        └── modules/    # 程序核心功能目录
            ├── auth.py    # 用户认证(注册和登录)
            ├── __init__.py
            ├── log.py    # 日志初始化类
            └── socket_server.py    # socket网络模块
    
                    
    # 功能实现:
        1. 实现了用户注册和登录验证(新增)。
        2. 用户注册时,将用户名添加到 conf/user.list里并创建home/[username],为每个用户生成独立的数据库文件 db/[username].db
        2. 每个用户的磁盘配额为10M, 在conf/settings.py 中声明, 可以修改
        3. 本程序适用于windows,命令:cd / mkdir / pwd / dir / put / get
        4. 实现了get下载续传的功能:
                服务器存在文件, 客户端不存在,直接下载;
                服务器存在文件, 客户端也存在文件,比较大小, 一致则不传,不一致则追加续传;
        5. 实现日志记录(新增)
                
    # 状态码:
    
    400 登录验证(用户名或密码错误)
    401 注册验证(注册的用户名已存在)
    402    命令不正确
    403    空间不足
    405 续传
    406 get(客户端文件存在)
    
    
    200 登录成功
    201 注册成功
    202 命令执行成功
    203 文件一致
    
    
    000 系统交互码
    README

    程序结构

    具体代码实现

    1. ftp客户端程序

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # Author: hkey
    import os, sys
    import socket
    
    
    class MyClient:
        def __init__(self, ip_port):
            self.client = socket.socket()
            self.ip_port = ip_port
    
        def connect(self):
            self.client.connect(self.ip_port)
    
        def start(self):
            self.connect()
            while True:
                print('注册(register)
    登录(login)')
                auth_type = input('>>>').strip()
                if not auth_type: continue
                if auth_type == 'register' or auth_type == 'login':
                    user = input('用户名:').strip()
                    pwd = input('密码:').strip()
                    auth_info = '%s:%s:%s' % (auth_type, user, pwd)
                    self.client.sendall(auth_info.encode())
                    status_code = self.client.recv(1024).decode()
                    if status_code == '200':
                        print('33[32;1m登录成功.33[0m')
                        self.interactive()
                    elif status_code == '201':
                        print('33[32;1m注册成功.33[0m')
                    elif status_code == '400':
                        print('33[31;1m用户名或密码错误.33[0m')
                    elif status_code == '401':
                        print('33[31;1m注册用户名已存在.33[0m')
                    else:
                        print('[%s]Error!' % status_code)
    
    
                else:
                    print('33[31;1m输入错误,请重新输入.33[0m')
    
        def interactive(self):
            while True:
                command = input('>>>').strip()
                if not command: continue
                command_str = command.split()[0]
                if hasattr(self, command_str):
                    func = getattr(self, command_str)
                    func(command)
    
        def dir(self, command):
            self.__universal_method_data(command)
    
        def pwd(self, command):
            self.__universal_method_data(command)
    
        def mkdir(self, command):
            self.__universal_method_none(command)
    
        def cd(self, command):
            self.__universal_method_none(command)
    
        def __universal_method_none(self, command):
            self.client.sendall(command.encode())
            status_code = self.client.recv(1024).decode()
            if status_code == '202':
                self.client.sendall(b'000')
            else:
                print('[%s]Error!' % status_code)
    
        def __universal_method_data(self, command):
            self.client.sendall(command.encode())
            status_code = self.client.recv(1024).decode()
            if status_code == '202':
                self.client.sendall(b'000')
                result = self.client.recv(4096)
                print(result.decode('gbk'))
            else:
                print('[%s]Error!' % status_code)
    
        def put(self, command):
            if len(command.split()) > 1:
                filename = command.split()[1]
                if os.path.isfile(filename):
                    self.client.sendall(command.encode())
                    file_size = os.path.getsize(filename)
                    response = self.client.recv(1024)
                    self.client.sendall(str(file_size).encode())
                    status_code = self.client.recv(1024).decode()
                    if status_code == '202':
                        with open(filename, 'rb') as f:
                            while True:
                                data = f.read(1024)
                                send_size = f.tell()
                                if not data: break
                                self.client.sendall(data)
                                self.__progress(send_size, file_size, '上传中')
                    else:
                        print('33[31;1m[%s]空间不足.33[0m' % status_code)
    
                else:
                    print('33[31;1m[%s]文件不存在.33[0m' % filename)
    
            else:
                print('33[31;1m命令格式错误.33[0m')
    
        def __progress(self, trans_size, file_size, mode):
            bar_length = 100
            percent = float(trans_size) / float(file_size)
            hashes = '=' * int(percent * bar_length)
            spaces = ' ' * int(bar_length - len(hashes))
            sys.stdout.write('
    %s %.2fM/%.2fM %d%% [%s]'
                             % (mode, trans_size / 1048576, file_size / 1048576, percent * 100, hashes + spaces))
    
        def get(self, command):
            self.client.sendall(command.encode())
            status_code = self.client.recv(1024).decode()
            if status_code == '202':
                filename = command.split()[1]
                if os.path.isfile(filename):
                    self.client.sendall(b'406')
                    response = self.client.recv(1024)
                    has_send_data = os.path.getsize(filename)
                    self.client.sendall(str(has_send_data).encode())
                    status_code = self.client.recv(1024).decode()
                    if status_code == '405':
                        print('续传.')
                        response = self.client.sendall(b'000')
                    elif status_code == '203':
                        print('文件一致.')
                        return
                else:
                    self.client.sendall(b'202')
                    has_send_data = 0
    
                file_size = int(self.client.recv(1024).decode())
                self.client.sendall(b'000')
                with open(filename, 'ab') as f:
                    while has_send_data != file_size:
                        data = self.client.recv(1024)
                        has_send_data += len(data)
                        f.write(data)
                        self.__progress(has_send_data, file_size, '下载中')
    
            else:
                print('[%s]Error!' % status_code)
    
    
    if __name__ == '__main__':
        ftp_client = MyClient(('localhost', 8080))
        ftp_client.start()
    ftp_client.py

    2. ftp服务端程序

    (1)ftp启动程序

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # Author: hkey
    import os, sys
    
    BASE_DIR = os.path.dirname(os.getcwd())
    
    sys.path.insert(0, BASE_DIR)
    
    from conf import settings
    from modules import socket_server
    
    if __name__ == '__main__':
        server = socket_server.socketserver.ThreadingTCPServer(settings.IP_PORT, socket_server.MyServer)
        server.serve_forever()
    start.py

    (2)conf配置文件

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # Author: hkey
    import os
    
    BASE_DIR = os.path.dirname(os.getcwd())
    
    HOME_PATH = os.path.join(BASE_DIR, 'home')
    LOG_PATH = os.path.join(BASE_DIR, 'logs')
    DB_PATH = os.path.join(BASE_DIR, 'db')
    USER_LIST_FILE = os.path.join(BASE_DIR, 'conf', 'user.list')
    
    LOG_SIZE = 102400
    LOG_NUM = 5
    
    LIMIT_SIZE = 10240000000
    
    IP_PORT = ('localhost', 8080)
    settings.py

    (3)modules 核心模块

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # Author: hkey
    import os, sys
    import pickle
    from conf import settings
    from modules.log import Logger
    
    
    class Auth:
        def __init__(self, user, pwd):
            self.user = user
            self.pwd = pwd
    
        def register(self):
            user_list = Auth.file_oper(settings.USER_LIST_FILE, 'r').split('
    ')[:-1]
            if self.user not in user_list:
                Auth.file_oper(settings.USER_LIST_FILE, 'a', self.user + '
    ')
                user_home_path = os.path.join(settings.HOME_PATH, self.user)
                if not os.path.isdir(user_home_path):
                    os.makedirs(user_home_path)
                user_dict = {'user': self.user, 'pwd': self.pwd, 'home_path': user_home_path,
                             'limit_size': settings.LIMIT_SIZE}
                user_pickle = pickle.dumps(user_dict)
                user_db_file = os.path.join(settings.DB_PATH, self.user) + '.db'
                Auth.file_oper(user_db_file, 'ab', user_pickle)
                Logger.info('[%s]注册成功。' % self.user)
                return '201'
            else:
                Logger.warning('[%s]注册用户名已存在。' % self.user)
                return '401'
    
        def login(self):
            user_list = Auth.file_oper(settings.USER_LIST_FILE, 'r').split('
    ')[:-1]
            if self.user in user_list:
                user_db_file = os.path.join(settings.DB_PATH, self.user) + '.db'
                user_pickle = Auth.file_oper(user_db_file, 'rb')
                user_dict = pickle.loads(user_pickle)
                if self.user == user_dict['user'] and self.pwd == user_dict['pwd']:
                    Logger.info('[%s]登录成功.' % self.user)
                    return user_dict
                else:
                    Logger.error('[%s]用户名或密码错误.' % self.user)
    
            else:
                Logger.warning('[%s]登录用户不存在.' % self.user)
    
        @staticmethod
        def file_oper(file, mode, *args):
            if mode == 'a' or mode == 'ab':
                data = args[0]
                with open(file, mode) as f:
                    f.write(data)
            elif mode == 'r' or mode == 'rb':
                with open(file, mode) as f:
                    data = f.read()
                    return data
    auth.py
    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # Author: hkey
    import os, sys
    import logging.handlers
    
    from conf import settings
    
    
    class Logger:
        logger = logging.getLogger()
        formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S')
        logfile = os.path.join(settings.LOG_PATH, sys.argv[0].split('/')[-1].split('.')[0]) + '.log'
        fh = logging.handlers.RotatingFileHandler(filename=logfile, maxBytes=settings.LOG_SIZE,
                                                  backupCount=settings.LOG_NUM, encoding='utf-8')
        ch = logging.StreamHandler()
    
        fh.setFormatter(formatter)
        ch.setFormatter(formatter)
    
        logger.setLevel(level=logging.INFO)
        
        logger.addHandler(fh)
        logger.addHandler(ch)
    
        @classmethod
        def info(cls, msg):
            cls.logger.info(msg)
    
        @classmethod
        def warning(cls, msg):
            cls.logger.warning(msg)
    
        @classmethod
        def error(cls, msg):
            cls.logger.error(msg)
    log.py
    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # Author: hkey
    
    import os
    import socketserver
    import subprocess
    from os.path import getsize, join
    from modules.auth import Auth
    from modules.log import Logger
    
    
    class MyServer(socketserver.BaseRequestHandler):
        def handle(self):
            try:
                while True:
                    auth_info = self.request.recv(1024).decode()
                    auth_type, user, pwd = auth_info.split(':')
                    auth_user = Auth(user, pwd)
                    if auth_type == 'register':
                        status_code = auth_user.register()
                        self.request.sendall(status_code.encode())
                    elif auth_type == 'login':
                        user_dict = auth_user.login()
                        if user_dict:
                            self.request.sendall(b'200')
                            self.user_current_path = user_dict['home_path']
                            self.user_home_path = user_dict['home_path']
                            self.user_limit_size = user_dict['limit_size']
                            while True:
                                command = self.request.recv(1024).decode()
                                command_str = command.split()[0]
                                if hasattr(self, command_str):
                                    func = getattr(self, command_str)
                                    func(command)
    
                        else:
                            self.request.sendall(b'400')
            except ConnectionResetError as e:
                print('Error:', e)
    
        def dir(self, command):
            if len(command.split()) == 1:
                Logger.info('[%s] 执行成功.' % command)
                self.request.sendall(b'202')
                response = self.request.recv(1024)
                cmd_res = subprocess.Popen('dir %s' % self.user_current_path, stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE, shell=True)
                stdout = cmd_res.stdout.read()
                stderr = cmd_res.stderr.read()
                result = stdout if stdout else stderr
                self.request.sendall(result)
            else:
                Logger.warning('[%s] 命令格式错误.' % command)
                self.request.sendall(b'402')
    
        def pwd(self, command):
            if len(command.split()) == 1:
                self.request.sendall(b'202')
                Logger.info('[%s] 执行成功.' % command)
                response = self.request.recv(1024)
                self.request.sendall(self.user_current_path.encode())
            else:
                Logger.warning('[%s] 命令格式错误.' % command)
                self.request.sendall(b'402')
    
        def mkdir(self, command):
            if len(command.split()) > 1:
                dir_name = command.split()[1]
                dir_path = os.path.join(self.user_current_path, dir_name)
                if not os.path.isdir(dir_path):
                    Logger.info('[%s] 执行成功.' % command)
                    self.request.sendall(b'202')
                    response = self.request.recv(1024)
                    os.makedirs(dir_path)
                else:
                    Logger.warning('[%s] 命令格式错误.' % command)
                    self.request.sendall(b'402')
    
        def cd(self, command):
            if len(command.split()) > 1:
                dir_name = command.split()[1]
                dir_path = os.path.join(self.user_current_path, dir_name)
                if dir_name == '..' and len(self.user_current_path) > len(self.user_home_path):
                    self.request.sendall(b'202')
                    response = self.request.recv(1024)
                    self.user_current_path = os.path.dirname(self.user_current_path)
                elif os.path.isdir(dir_path):
                    self.request.sendall(b'202')
                    response = self.request.recv(1024)
                    if dir_name != '.' and dir_name != '..':
                        self.user_current_path = dir_path
                else:
                    self.request.sendall(b'403')
            else:
                Logger.warning('[%s] 命令格式错误.' % command)
                self.request.sendall(b'402')
    
        def put(self, command):
            filename = command.split()[1]
            file_path = os.path.join(self.user_current_path, filename)
            response = self.request.sendall(b'000')
            file_size = self.request.recv(1024).decode()
            file_size = int(file_size)
            used_size = self.__getdirsize(self.user_home_path)
            if self.user_limit_size > file_size + used_size:
                self.request.sendall(b'202')
                Logger.info('[%s] 执行成功.' % command)
                recv_size = 0
                Logger.info('[%s] 文件开始上传.' % file_path)
                with open(file_path, 'wb') as f:
                    while recv_size != file_size:
                        data = self.request.recv(1024)
                        recv_size += len(data)
                        f.write(data)
                Logger.info('[%s] 文件上传完成.' % file_path)
    
            else:
                self.request.sendall(b'403')
    
        def __getdirsize(self, user_home_path):
            size = 0
            for root, dirs, files in os.walk(user_home_path):
                size += sum([getsize(join(root, name)) for name in files])
            return size
    
        def get(self, command):
            if len(command.split()) > 1:
                filename = command.split()[1]
                file_path = os.path.join(self.user_current_path, filename)
                if os.path.isfile(file_path):
                    self.request.sendall(b'202')
                    file_size = os.path.getsize(file_path)
                    status_code = self.request.recv(1024).decode()
                    if status_code == '406':
                        self.request.sendall(b'000')
                        recv_size = int(self.request.recv(1024).decode())
                        if file_size > recv_size:
                            self.request.sendall(b'405')
                            respon = self.request.recv(1024)
                        elif file_size == recv_size:
                            self.request.sendall(b'203')
                            print('一致.')
                            return
                    else:
                        recv_size = 0
    
                    self.request.sendall(str(file_size).encode())
                    resonse = self.request.recv(1024)
                    with open(file_path, 'rb') as f:
                        f.seek(recv_size)
                        while True:
                            data = f.read(1024)
                            if not data: break
                            self.request.sendall(data)
    
            else:
                self.request.sendall(b'402')
    socket_server.py

    (4)其他目录

    db/  - 注册成功后生成个人数据库文件
    home/ - 注册成功后创建个人家目录
    log/ - 日志文件目录

    程序运行效果图

    (1)注册、登录及命令的执行

    client:

    server:

    (2)上传

     (3)下载(续传功能)

  • 相关阅读:
    python timeit模块用法
    boto3库限速
    golang-Beego-orm创建的坑
    Java07
    Java06
    Java04
    Java03
    c
    Mac 安装GCC
    命令: go build
  • 原文地址:https://www.cnblogs.com/hukey/p/10182876.html
Copyright © 2020-2023  润新知