欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

python开发多用户在线FTP

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

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

服务端:
python开发多用户在线FTP
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
python开发多用户在线FTP
#: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
python开发多用户在线FTP
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
python开发多用户在线FTP
[mogu]
name=mogu
password=123
quota=10240000

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

[zhangsan]
name=zhangsan
password=123
quota=10240000
user.ini
python开发多用户在线FTP
#: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 命令   以及如何在客户端显示家目录为根目录  等问题