python开发多用户在线FTP

2018-09-05 07:57:23来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

功能实现
作业:开发一个支持多用户在线的FTP程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server上随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
附加功能:支持文件的断点续传

服务端
|-------conf(配置文件夹)
| |----settings.py 路径配置信息
|
|-------database(数据库文件夹)
| |----home(家目录)
| |----public
|
|-------modules(服务端功能模块)
| |----server.py 服务端线程交互主模块。。。。。
| |----user.py 用户验证需要的hash 和 login 验证
|
|-------user_db(用户配置项信息)本来想就单文件放,后来觉得还是建了目录
|
|-------start.py 服务端程序启动文件

服务端:
import os
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

DATABASE=os.path.join(BASE_DIR,'database','home',) #用户属主目录

USER_DB=os.path.join(BASE_DIR,'user_db')  #用户账户数据库

PUBLIC=os.path.join(BASE_DIR,'database','public')


#===============下列代码与配置无关   为查询目录大小-----------------------
# for root, dirs, files in os.walk(DATABASE, topdown=False):
#     for name in files:
#         print(root,name)
#     for name in dirs:
#         print(root,name)

# for root, dirs, files in os.walk(DATABASE, topdown=False):
#     for name in files:
#         print(os.path.join(root, name))
#     for name in dirs:
#         print(os.path.join(root, name))
settings.py
#:coding:utf-8

import socketserver, os, subprocess, hashlib
from modules.user import login
from conf import settings


class MyTcphandler(socketserver.BaseRequestHandler):

    def handle(self):
        try:
            while True:
                '''认证开始'''
                user_dict = self.request.recv(1024).decode('utf-8')
                msg, tag, config, db_path, name = login(user_dict)  # 返回认证状态及数据
                self.config = config  # 为对象创建属性
                self.db_path = db_path  # 对象家目录
                self.now_path = db_path  # 对象当前路径
                self.name = name
                state = ('%s:%s' % (msg, tag))  # 认证状态
                if msg == False:
                    self.request.send(state.encode('utf-8'))
                    continue
                self.request.send(state.encode('utf-8'))
                while True:
                    '''交互开始'''
                    cmd = self.request.recv(1024).decode('utf-8')
                    if len(cmd) == 0: break
                    cmd_cmd = cmd.split()[0]  # 接收cmd命令按空格切分得到第一个值
                    if hasattr(self, cmd_cmd):  # 判断cmd命令存不存在类中
                        func = getattr(self, cmd_cmd)  # 字符串调用类方法
                        func(cmd)
        except Exception as f:  # 针对Windows
            print(f)

    def help(self, cmd):
        cmd_dict = '''
            -------------------------帮助文档----------------------------------
              命令                     说明                    示例
               cd             切换目录(public公共目录)     cd dirname(目录名称)
               ls                 查看当前目录下所有文件          ls
               pwd                   查看当前路径               pwd
               get                    下载文件              get filename(文件名)
               put                    上传文件              put filename(文件名)
              mkdir               创建目录(当前路径下)       mkdir dirname(目录名)
        '''
        if len(cmd.split()) > 1:
            res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd
            self.request.sendall(res.encode('utf-8'))
        else:
            self.request.sendall(cmd_dict.encode('utf-8'))

    def ls(self, cmd):
        if len(cmd.split()) > 1:
            res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd
            self.request.sendall(res.encode('utf-8'))
        else:
            obj = subprocess.Popen('dir %s' % self.now_path,
                                   shell=True,
                                   stdout=subprocess.PIPE
                                   )
            res = obj.stdout.read()
            self.request.sendall(res)

    def pwd(self, cmd):
        if len(cmd.split()) > 1:
            res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd
        else:
            res = self.now_path
        self.request.sendall(res.encode('utf-8'))

    def mkdir(self, cmd):
        if len(cmd.split()) == 2:
            dir = cmd.split()[1]
            dir_path = self.now_path + r'\%s' % dir
            if not os.path.isdir(dir_path):  # 目录不存在
                os.mkdir(dir_path)
                res = '< %s >目录创建成功!!!' % dir
            else:
                res = '< %s >目录已存在!' % dir
        else:
            res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd
        self.request.sendall(res.encode('utf-8'))

    def cd(self, cmd):
        if len(cmd.split()) == 2:
            dir = cmd.split()[1]
            if dir != self.name and dir in self.config.sections():
                res = '权限不足,不要瞎搞'

            elif dir == 'public':
                self.now_path = settings.PUBLIC
                res = '< pulic >共享目录切换成功!!!'

            elif os.path.isdir(self.now_path + r'\%s' % dir):
                self.now_path += r'\%s' % dir
                res = '< %s >目录切换成功!!!' % dir

            elif dir == '..' and len(self.now_path) > len(self.db_path):
                self.now_path = os.path.dirname(self.now_path)
                res = '< 上一级 >目录切换成功!!!'

            else:
                res = '权限不足,或路径不正确'
            self.request.sendall(res.encode('utf-8'))
        else:
            res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd
            self.request.sendall(res.encode('utf-8'))

    def get(self, cmd):
        '''下载'''
        if len(cmd.split()) == 2:
            filename = cmd.split()[1]
            file_path = self.now_path + r'\%s' % filename
            if os.path.isfile(file_path):
                self.request.sendall('exist'.encode('utf-8'))  # 交互2发送文件存在的信号
                file_size = os.stat(file_path).st_size  # 计算文件大小
                res = self.request.recv(1024).decode('utf-8')  # 交互3接收状态信息
                if res.split(':')[0] == 'exist':  # 客户端文件存在
                    client_size = int(res.split(':')[1])

                    if client_size < file_size:  # 客户端文件支持续传
                        self.request.sendall('yes'.encode('utf-8'))  # 分支交互1
                        file_size -= client_size

                    else:
                        self.request.sendall('no'.encode('utf-8'))
                        return

                else:  # 文件不存在时
                    client_size = 0

                with open(file_path, 'rb') as f:
                    self.request.sendall(str(file_size).encode('utf-8'))  # 交互4发送文件大小
                    self.request.recv(1024)  # 交互5接收一次  其实多余 怕粘包
                    f.seek(client_size)  # 文件指针移动到客户端文件大小位置
                    m = hashlib.md5()
                    for line in f:
                        m.update(line)
                        self.request.sendall(line)  # 交互6for循环发送循环数据
                self.request.sendall(m.hexdigest().encode('utf-8'))  # 交互7发送服务端文件MD5值
            else:
                self.request.sendall('文件不存在哦'.encode('utf-8'))
        else:
            res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd
            self.request.sendall(res.encode('utf-8'))

    def put(self, cmd):
        '''上传'''
        file_name = cmd.split()[1]
        file_path = self.now_path + r'\%s' % file_name
        self.request.sendall('已准备好上传服务'.encode('utf-8'))  # 交互2发送确认通知
        file_size = int(self.request.recv(1024).decode('utf-8'))  # 交互3接收文件大小
        quota_size = int(self.config.get(self.name, 'quota'))  # 拿到用户的磁盘配额
        used_size = self.__getdirsize(self.db_path)  # 计算得到用户已使用的空间
        remain_size = quota_size - used_size  # 得到用户剩余空间
        if file_size + used_size <= quota_size:
            self.request.sendall(('yes:%s' % remain_size).encode('utf-8'))  # 交互4发送可以接收通知和用户剩余空间
            with open(file_path, 'wb') as f:
                receive_size = 0
                m = hashlib.md5()
                while receive_size < file_size:
                    real_size = file_size - int(receive_size)  # 计算剩余大小
                    if real_size > 1024:
                        size = 1024
                    else:
                        size = real_size
                    data = self.request.recv(size)  # 交互5循环接收数据
                    receive_size += len(data)
                    f.write(data)
                    m.update(data)
                server_md5 = m.hexdigest()
                client_md5 = self.request.recv(1024).decode('utf-8')  # 交互6接收客户端文件MD5值
                if server_md5 == client_md5:
                    self.request.sendall('\nmd5值相同,文件具有一致性,文件上传完成'.encode('utf-8'))  # 交互7发送完成信息
        else:
            self.request.sendall(('no:%s' % remain_size).encode('utf-8'))  # 分支交互4

    def __getdirsize(self, db_path):
        '''计算已使用的用户家目录大小'''
        size = 0
        for root, dirs, files in os.walk(db_path):
            size += sum([os.path.getsize(os.path.join(root, name)) for name in files])
        return size
server.py
from conf import settings
import os, configparser, hashlib


def login(user_dict):
    userlist = user_dict.split(':')
    name = userlist[0]
    password = userlist[1]
    config = query_db()  # 查询本地用户数据库
    if config.has_section(name):  # 判断有没有name
        true_name = config.get(name, 'name')  # 取出本地数据库对应name的名字
        true_pwd = hash(config.get(name, 'password'))  # 取出本地数据库对应name的加密过的密码
        if name == true_name and password == true_pwd:  # 比对
            db_path = os.path.join(settings.DATABASE + r'\%s' % name)
            return True, '恭喜%s,认证成功!!!' % name, config, db_path, name  # 额 返回了五个值。。。。

        else:
            return False, '用户名或密码错误', None, None, None

    else:
        return False, '用户名或密码错误', None, None, None


def query_db():
    '''查询本地数据库'''
    config = configparser.ConfigParser()
    config.read(settings.USER_DB + r'\user.ini')

    return config


def hash(password):
    s = hashlib.md5()
    s.update(password.encode('utf-8'))
    return s.hexdigest()
user.py
[mogu]
name=mogu
password=123
quota=10240000

[xiaoming]
name=xiaoming
password=123
quota=10240000

[zhangsan]
name=zhangsan
password=123
quota=10240000
user.ini
#:coding:utf-8
import socketserver, configparser, os
from modules import server
from conf import settings


def create_dir():
    '''初始化生成本地数据库用户属主目录'''
    config = configparser.ConfigParser()  # configpasrser模块
    config.read(settings.USER_DB + r'\user.ini')  # 用户数据文件路径
    for user_name in config.sections():  # 循环取值
        user_path = settings.DATABASE + r'\%s' % user_name
        if not os.path.isdir(user_path):  # 文件夹不存在则创建
            os.mkdir(user_path)


if __name__ == '__main__':
    create_dir()
    server = socketserver.ThreadingTCPServer(('127.0.0.1', 6666), server.MyTcphandler)
    server.serve_forever()
start.py

使用说明:
              命令                      说明                    示例
               cd             切换目录(public公共目录)     cd dirname(目录名称)
               ls                 查看当前目录下所有文件          ls
               pwd                   查看当前路径               pwd
               get                    下载文件              get filename(文件名)
               put                    上传文件              put filename(文件名)
              mkdir               创建目录(当前路径下)       mkdir dirname(目录名)
所有用户信息都在 user_db里的user.ini  文件里

客户端
 |-------database(客户端数据库)本来想不建得,然后东西太多有点乱
|
|-------start.py 客户端程序的启动文件

客户端:

# :coding:utf-8
import socket, hashlib, os, sys

BASE_DIR = os.path.dirname(__file__)
db_path = os.path.join(BASE_DIR, 'database')


def hash(password):
    s = hashlib.md5()
    s.update(password.encode('utf-8'))
    return s.hexdigest()


class FtpClient:
    '''ftp客户端'''

    def __init__(self, ip_port):
        self.ip_port = ip_port

    def __connect(self):
        '''连接服务器'''
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect(self.ip_port)

    def __start(self):
        '''程序开始'''
        self.__connect()
        while True:
            '''认证'''
            name = input('用户名:').strip()
            pwd = input('密码:').strip()
            pwd = hash(pwd)
            user_dict = ('%s:%s' % (name, pwd))
            self.client.sendall(user_dict.encode('utf-8'))
            state = self.client.recv(1024).decode('utf-8')
            if state.split(':')[0] == 'True':
                print(state.split(':')[1])
                self.__interaction(name)
            else:
                print(state.split(':')[1])

    def __interaction(self, name):
        '''交互开始'''
        while True:
            cmd = input('[%s]>>:' % name).strip()
            if len(cmd) == 0: continue
            cmd_cmd = cmd.split()[0]  # 按照空格切分输入的命令
            if hasattr(self, cmd_cmd):  # 如果类中存在对应方法则执行
                func = getattr(self, cmd_cmd)
                func(cmd)
            else:
                print('< %s >不是内部或外部命令,也不是可运行的程序或批处理文件。'
                      '可查看帮助文档(help)' % cmd)

    def help(self, cmd):
        '''帮助命令'''
        self.client.sendall(cmd.encode('utf-8'))
        print(self.client.recv(1024).decode('utf-8'))

    def ls(self, cmd):
        '''查看当前路径文件命令'''
        self.client.sendall(cmd.encode('utf-8'))
        print(self.client.recv(2048).decode('gbk'))

    def pwd(self, cmd):
        '''显示当前路径命令'''
        self.client.sendall(cmd.encode('utf-8'))
        print(self.client.recv(1024).decode('utf-8'))

    def mkdir(self, cmd):
        '''创建目录'''
        self.client.sendall(cmd.encode('utf-8'))
        print(self.client.recv(1024).decode('utf-8'))

    def cd(self, cmd):
        self.client.sendall(cmd.encode('utf-8'))
        print(self.client.recv(1024).decode('utf-8'))

    def get(self, cmd):
        '''下载'''
        self.client.sendall(cmd.encode('utf-8'))  # 1 交互
        res = self.client.recv(1024).decode('utf-8')  # 2 交互
        if res == 'exist':
            filename = cmd.split()[1]
            if os.path.isfile(db_path + r'\%s' % filename):  # 如果文件存在
                receive_size = os.stat(db_path + r'\%s' % filename).st_size  # 已接收文件大小
                self.client.sendall(('exist:%s' % receive_size).encode('utf-8'))  # 交互3发送状态和大小
                state = self.client.recv(1024).decode('utf-8')  # 分支交互1
                if state == 'yes':
                    print('文件续传成功,正在下载')
                else:
                    print('文件完整,无法进行下载')
                    return
            else:
                receive_size = 0  # 文件不存在时为0
                self.client.sendall('no:0'.encode('utf-8'))  # 交互3发送状态

            file_size = int(self.client.recv(1024).decode('utf-8'))  # 交互4接收文件大小
            self.client.sendall('receive'.encode('utf-8'))  # 交互5此交互其实多余,但是怕粘包
            with open(db_path + r'\%s' % filename, 'ab') as f:
                file_size += int(receive_size)  # 计算总文件大小
                m = hashlib.md5()  # MD5
                while receive_size < file_size:  # 接收文件大小 < 总文件大小
                    real_size = file_size - int(receive_size)  # 计算剩余大小
                    if real_size > 1024:
                        size = 1024
                    else:
                        size = real_size
                    data = self.client.recv(size)  # 交互6  开始循环接收文件
                    receive_size += len(data)
                    f.write(data)  # 追加写入
                    m.update(data)
                    self.__progress(receive_size, file_size)  # 进度条啦
                client_md5 = m.hexdigest()  # 客户端新文件MD5值
                server_md5 = self.client.recv(1024).decode('utf-8')  # 交互7  接收服务端文件MD5值
                if client_md5 == server_md5: print('\nmd5值相同,文件具有一致性,文件下载完成')
        else:
            print(res)

    def put(self, cmd):
        '''上传'''
        if len(cmd.split()) == 2:
            file_name = cmd.split()[1]
            file_path = db_path + r'\%s' % file_name
            if os.path.isfile(file_path):  # 客户端本地是否有文件
                self.client.sendall(cmd.encode('utf-8'))  # 交互1
                print(self.client.recv(1024).decode('utf-8'))  # 交互2收到确认通知
                file_size = os.stat(file_path).st_size  # 计算本地文件大小
                self.client.sendall(str(file_size).encode('utf-8'))  # 交互3发送文件大小
                res = self.client.recv(1024).decode('utf-8')  # 交互4接收确认信息和可用空间
                remain_size = int(res.split(':')[1])
                if res.split(':')[0] == 'yes':
                    print('开始上传,当前剩余空间%sM' % (round(remain_size / 1024000)))  # 四舍五入
                    with open(file_path, 'rb') as f:
                        m = hashlib.md5()
                        for line in f:
                            m.update(line)
                            send_size = f.tell()  # 返回文件的当前位置
                            self.client.sendall(line)  # 交互5for循环发送文件数据
                            self.__progress(send_size, file_size)
                    self.client.sendall(m.hexdigest().encode('utf-8'))  # 交互6发送本地文件MD5值
                    print(self.client.recv(1024).decode('utf-8'))  # 交互7 接收完成信息
                else:
                    print('空间不足哦,无法上传,当前剩余空间%sM' % (round(remain_size / 1024000)))
            else:
                print('< %s > 文件不存在哦' % file_name)
        else:
            print('< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd)

    def __progress(self, recv_size, data_size, width=70):
        ''' =========进度条啦没整明白==========
    # data_size = 9292
    # recv_size = 0
    # while recv_size < data_size:
    #     time.sleep(0.1)  # 模拟数据的传输延迟
    #     recv_size += 1024  # 每次收1024
    #
    #     percent = recv_size / data_size  # 接收的比例
    #     progress(percent, width=70)  # 进度条的宽度70'''
        percent = float(recv_size) / float(data_size)
        if percent >= 1:
            percent = 1
        show_str = ('[%%-%ds]' % width) % (int(width * percent) * '>')
        print('\r%s %d%%' % (show_str, int(100 * percent)), file=sys.stdout, flush=True, end='')


# print(FtpClient.__dict__)
if __name__ == '__main__':
    ip_port = ('127.0.0.1', 6666)
    client = FtpClient(ip_port)
    client._FtpClient__start()

目前此程序还有些问题,例如cd 命令   以及如何在客户端显示家目录为根目录  等问题

 

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:思路——根据网站链接爬取整个图片网站

下一篇:多线程爬虫介绍