[ 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)下载(续传功能)

原文地址:https://www.cnblogs.com/hukey/p/10182876.html