[ 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)\n登录(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('\033[32;1m登录成功.\033[0m')
                    self.interactive()
                elif status_code == '201':
                    print('\033[32;1m注册成功.\033[0m')
                elif status_code == '400':
                    print('\033[31;1m用户名或密码错误.\033[0m')
                elif status_code == '401':
                    print('\033[31;1m注册用户名已存在.\033[0m')
                else:
                    print('[%s]Error!' % status_code)


            else:
                print('\033[31;1m输入错误,请重新输入.\033[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('\033[31;1m[%s]空间不足.\033[0m' % status_code)

            else:
                print('\033[31;1m[%s]文件不存在.\033[0m' % filename)

        else:
            print('\033[31;1m命令格式错误.\033[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('\r%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('\n')[:-1]
        if self.user not in user_list:
            Auth.file_oper(settings.USER_LIST_FILE, 'a', self.user + '\n')
            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('\n')[:-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)下载(续传功能)

 

本文作者:hukey

本文链接:https://www.cnblogs.com/hukey/p/10182876.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   hukey  阅读(743)  评论(1编辑  收藏  举报
历史上的今天:
2016-12-27 Centos7 / RHEL 7 双网卡绑定
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 彩虹 Jay
彩虹 - Jay
00:00 / 00:00
An audio error has occurred.

彩虹 + 轨迹 (Live) - 周杰伦 (Jay Chou)

彩虹

词:周杰伦

曲:周杰伦

哪里有彩虹告诉我

哪里有彩虹告诉我

能不能把我的愿望还给我

能不能把我的愿望还给我

为什么天这么安静

为什么天这么安静

所有的云都跑到我这里

有没有口罩一个给我

有没有口罩一个给我

释怀说了太多就成真不了

释怀说了太多就成真不了

也许时间是一种解药

也许时间是一种解药

也是我现在正服下的毒药

也是我现在正服下的毒药

看不见你的笑 我怎么睡得着

看不见你的笑 我怎么睡得着

你的声音这么近我却抱不到

你的声音这么近我却抱不到

没有地球太阳还是会绕

没有地球太阳还是会绕

没有理由我也能自己走

没有理由我也能自己走

你要离开 我知道很简单

你要离开 我知道很简单

你说依赖 是我们的阻碍

你说依赖 是我们的阻碍

就算放开 但能不能别没收我的爱

就算放开 但能不能别没收我的爱

当作我最后才明白

当作我最后才明白

看不见你的笑 要我怎么睡得着

看不见你的笑 要我怎么睡得着

你的声音这么近我却抱不到

没有地球太阳还是会绕 会绕

没有理由我也能自己走掉

释怀说了太多就成真不了

也许时间是一种解药 解药

也是我现在正服下的毒药

轨迹

词:黄俊郎

曲:周杰伦

我会发着呆然后忘记你

接着紧紧闭上眼

想着哪一天 会有人代替

想着哪一天 会有人代替

让我不再想念你

我会发着呆 然后微微笑

我会发着呆 然后微微笑

接着紧紧闭上眼

又想了一遍 你温柔的脸

又想了一遍 你温柔的脸

在我忘记之前