Python模拟FTP服务

项目结构:

  server
    bin
      ftp_start.py                 # 启动程序
    conf
      setting.py                   # 主配置文件
      user_info.conf               # 用户信息文件
    core
      server.p                     # 代码核心文件
    HOME                            # 用户家目录
      ftp1                         # 用户目录
        file
      ftp2                         # 用户目录
        file
    lib
      check_file_exist.py          # 检查文件是否存在
      encrypt.py                   # 信息加密
      get_use_quota.py             # 检查用户上传磁盘空间
      make_header.py               # 自制报头

 

FTP: Server端

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import os

PACKAGE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PACKAGE_PATH)

from core import server

if __name__ == '__main__':
    server.ftp_server.user_auth()
bin/ftp_start.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

# config path
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# user file
USER_FILE_PATH = '%s/conf/user_info.conf' % BASE_PATH

# home dir
HOME_DIR = '%s\\HOME' % BASE_PATH

# server bind address
BIND_ADDRESS = '127.0.0.1'

# listen
PORT = 8888
conf/setting.py
[ftp1]
PASSWORD = 123456
QUOTA = 100

[ftp2]
PASSWORD = abcdef
QUOTA = 200
conf/user_info.conf
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from conf import setting
from lib import get_use_quota,encrypt,check_file_exist,make_header
import configparser
import socket,struct,pickle,os,hashlib

class MyService:
    # 地址家族
    address_family = socket.AF_INET
    # 传输协议
    socket_type = socket.SOCK_STREAM
    # 字符编码
    code = 'utf-8'
    # 请求队列数
    request_queue = 5
    # 请求包的大小
    request_packet_size = 1024
    # 用户文件路径
    user_file_path = setting.USER_FILE_PATH

    # socket初始化
    def socket_init(self,):
        self.server = socket.socket(self.address_family,self.socket_type)
        self.server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
        self.server.bind((setting.BIND_ADDRESS,setting.PORT))
        self.server.listen(self.request_queue)

    # 用户认证
    def user_auth(self):
        self.socket_init()
        tag = True
        while tag:
            # 拿到客户端监听地址和监听端口
            self.conn,self.address = self.server.accept()

            tag2 = True
            while tag2:
                # 接收用户信息字典
                bytes_header = self.conn.recv(self.request_packet_size)
                if not bytes_header:break

                # 序列化加载报头
                header_dic = pickle.loads(bytes_header)

                # 客户端登录用户密码
                client_user = header_dic['user']
                client_passwd = header_dic['passwd']

                # 拿到文件对象
                conf_obj = configparser.ConfigParser()
                # 读取文件内容
                conf_obj.read(self.user_file_path)
                # 文件所有标题(section)
                db_all_user = conf_obj.sections()

                # 用户不存在文件中
                if client_user not in db_all_user:
                    self.conn.send('False'.encode(self.code))
                # 用户存在
                else:
                    # 拿到用户密码(options)
                    db_passwd = conf_obj.get(client_user,'PASSWORD')
                    # 密码(options)加密
                    db_encrypt_passswd = encrypt.encrypt_passwd(db_passwd)

                    # 密码对比
                    if client_passwd != db_encrypt_passswd:
                        self.conn.send('False'.encode(self.code))
                    else:
                        self.conn.send('True'.encode(self.code))

                        while tag2:
                            # 接收客户端发送的命令 get file,put file
                            bytes_cmd = self.conn.recv(self.request_packet_size)
                            str_command = bytes_cmd.decode(self.code)

                            # 命令
                            command = str_command.split()[0]
                            # 文件名
                            file_name = str_command.split()[1]

                            # quit 退出
                            if command == 'quit':
                                tag2 = False
                            elif command == 'get':
                                self.get(client_user,file_name)
                            elif command == 'put':
                                self.put(client_user)

            self.conn.close()
        self.server.close()

    def get(self,client_user,file_name):
        # 获取文件路径,文件不存在则返回False
        file_path = check_file_exist.check_exist(client_user,file_name)
        
        # 下载文件的大小
        get_file_size = os.path.getsize(file_path)
        # 转换为Mb
        mb_size = get_file_size / (1024*1024)

        # 文件不存在
        if not file_path:
            self.conn.send('False'.encode(self.code))
            return
        # 文件存在
        else:
            self.conn.send('True'.encode(self.code))

        # 序列化报头,报头长度
        pick_header,header_length = make_header.my_header(get_file_size,file_path)

        # 发送报头长度
        self.conn.send(header_length)
        # 发送报头
        self.conn.send(pick_header)

        # 发送文件
        with open(file_path,'rb') as read_f:
            for line in read_f:
                self.conn.send(line)

    def put(self,client_user):
        # 拿到配置文件对象
        conf_obj =     configparser.ConfigParser()
        # 读取配置文件内容
        conf_obj.read(self.user_file_path)
        # 用户磁盘最大配额
        max_quota = conf_obj.get(client_user,'QUOTA')
        # 已用配额
        use_quota_mb = get_use_quota.get_quota(client_user)
        # 接受报头长度(struct格式)
        struct_header_length = self.conn.recv(4)
        # 将struct格式转换为整数
        header_length = struct.unpack('i',struct_header_length)[0]    # (int,)元组格式

        # 接受报头
        pick_header = self.conn.recv(header_length)
        header_dic = pickle.loads(pick_header)

        # 获取上传文件名称
        put_file_name = header_dic['filename']
        # 获取上传文件大小
        put_file_size = header_dic['filesize']
        # 转换成mb大小
        put_file_size_mb = put_file_size / (1024*1024)
        # 获取上传文件md5校验值
        put_file_md5 = header_dic['md5']

        # 已用配额+本次文件大小 > 最大用户配额
        if use_quota_mb + put_file_size_mb > int(max_quota):
            self.conn.send('False'.encode(self.code))
            return
        else:
            self.conn.send('True'.encode(self.code))

        # 接受上传文件到用户的家目录
        HOME_DIR = os.path.join(setting.HOME_DIR,client_user)
        file_abs_path = os.path.join(HOME_DIR,put_file_name)

        with open(file_abs_path,'wb') as write_file:
            put_size = 0
            while put_size < put_file_size:
                # 接收文件一行内容
                recv_line = self.conn.recv(self.request_packet_size)
                # 写入文件
                write_file.write(recv_line)
                write_file.flush()

                # 接受内容大小
                put_size += len(recv_line)

        # 文件MD5校验
        with open(file_abs_path,'rb') as read_file:
            for line in read_file:
                md5 = hashlib.md5('小鸡炖蘑菇'.encode('utf-8'))
                md5.update(line)
            local_md5_data = md5.hexdigest()
        if local_md5_data != put_file_md5:
            self.conn.send('False'.encode(self.code))
        else:
            self.conn.send('True'.encode(self.code))

ftp_server = MyService()
core/server.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from conf import setting
import os

def check_exist(client_user,file):
    # 家目录
    home = os.path.join(setting.HOME_DIR,client_user)
    # 如果文件名不带目录,则为当前目录
    if file[0] != '/' or file[0] != '\\':
        file = os.path.join(home,file)
    # 文件绝对路径列表
    file_list = []
    # 遍历目录、文件
    for root,dirs,files in os.walk(home):
        for f in files:
            # 文件名字不为空
            if f:
                # 添加到文件列表
                file_list.append(os.path.join(root,f))
    if file not in file_list:return False
    # 文件存在
    return file
lib/check_file_exist.py
#!/usr/bin/env python

import hashlib

# 加密密码
def encrypt_passwd(passwd):
    # md5加密
    my_md5 = hashlib.md5('猪肉炖粉条'.encode('utf-8'))
    my_md5.update(passwd.encode('utf-8'))
    # 获取加密值
    encrypt_var = my_md5.hexdigest()
    # 返回加密后的值
    return encrypt_var
lib/encrypt.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from conf import setting
import os

# 获取已用磁盘配额
def get_quota(user):
    # 家目录
    home_dir = os.path.join(setting.HOME_DIR,user)

    # 遍历所有文件
    for root,dirs,files in os.walk(home_dir):
        # 获取所有文件大小总和,并换算为MB
        total_size = sum([os.path.getsize(os.path.join(root,filename)) for filename in files]) / (1024*1024)
    return total_size
lib/get_use_quota.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import struct,hashlib,os,pickle

def my_header(file_size,file_path):
    # md5校验文件
    with open(file_path,'rb') as read_file:
        for line in read_file:
            md5 = hashlib.md5('小鸡炖蘑菇'.encode('utf-8'))
            md5.update(line)
        md5_data = md5.hexdigest()

    # 报头
    header_dic = {
        'filename': os.path.basename(file_path),
        'filesize': file_size,
        'md5': md5_data
    }

    # 序列化报头
    pick_header = pickle.dumps(header_dic)
    # 报头长度
    header_length = struct.pack('i',len(pick_header))
    return pick_header,header_length
lib/make_header.py

FTP: Client端

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket,struct,pickle
import hashlib,sys,os
import getpass

class Myclient:
    # 地址家族
    address_family = socket.AF_INET
    # 协议类型
    socket_type = socket.SOCK_STREAM
    # 字符编码
    code = 'utf-8'
    # 请求包的大小
    request_packet_size = 1024

    def __init__(self,bind_address,bind_port):
        self.bind_address = bind_address
        self.bind_port = bind_port
        
    # socket初始化连接
    def socket_init(self):
        self.client = socket.socket(self.address_family,self.socket_type)
        self.client.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
        self.client.connect((self.bind_address,self.bind_port))

    # 登录
    def login(self):
        self.socket_init()
        count = 0
        tag = True
        while tag:
            user = input('user >>: ').strip()
            passwd = getpass.getpass('password >>: ').strip()

            # 用户或密码有一个为空
            if not user or not passwd:
                count += 1
                continue

            # 密码加密
            md5_passwd = hashlib.md5('猪肉炖粉条'.encode(self.code))
            md5_passwd.update(passwd.encode(self.code))
            passwd_str = md5_passwd.hexdigest()

            # 用户信息字典
            header_dic = {
                'user': user,
                'passwd': passwd_str
            }

            # 序列化信息字典
            pick_dic = pickle.dumps(header_dic)
            # 发送用户信息字典
            self.client.send(pick_dic)

            # 接收登录信息
            log_info_byets = self.client.recv(self.request_packet_size)
            log_info = log_info_byets.decode(self.code)

            if log_info == 'True':
                print('login success')

                while tag:
                    # 输入命令
                    self.client_cmd = input('>>: ').strip()
                    # 命令列表
                    cmd_list = self.client_cmd.split()

                    # 退出
                    if self.client_cmd == 'quit':
                        # 发送退出信号
                        self.client.send('quit server'.encode(self.code))
                        tag = False
                        break

                    # 命令语法错误
                    if len(cmd_list) != 2:
                        print('Syntax error')
                        continue

                    # 命令
                    cmd = cmd_list[0]
                    file_name = cmd_list[1]

                    # 执行命令
                    if cmd == 'get':
                        self.client.send(self.client_cmd.encode(self.code))
                        self.get()
                    elif cmd == 'put':
                        # 上传的文件是否存在
                        file_exist = self.check_file_exist(file_name)

                        # 存在则发送命令
                        if file_exist:
                            self.client.send(self.client_cmd.encode(self.code))
                            self.put(file_name)
                        else:
                            print('file not does exist')
            else:
                print('user or passwd error')
                count += 1
                if count == 3:
                    tag = False
        self.client.close()

    # 检查上传文件是否存在
    def check_file_exist(self,file_path):
        # 如果发送文件不带目录,则为当前目录
        if file_path[0] != '/' or file_path[0] != '\\':
            file_path = os.path.join(os.getcwd(),file_path)

        if os.path.exists(file_path):
            return True
        return False

    # md5校验
    def md5_check(self,file_path):
        with open(file_path, 'rb') as read_file:
            for line in read_file:
                md5 = hashlib.md5('小鸡炖蘑菇'.encode('utf-8'))
                md5.update(line)
            md5_data = md5.hexdigest()
        return md5_data

    # get命令
    def get(self):
        # 接受文件是否存在信号
        bytes_file_exist = self.client.recv(self.request_packet_size)
        file_exist = bytes_file_exist.decode(self.code)

        # 文件存在
        if file_exist == 'True':
            # 报头长度(struct格式)
            struct_header_length = self.client.recv(4)
            # 将struct格式转换为整数
            header_length = struct.unpack('i',struct_header_length)[0]    # (int,)元组格式

            # 接受报头信息
            pick_header = self.client.recv(header_length)
            header_dic = pickle.loads(pick_header)

            # 获取下载文件名                
            get_file_name = header_dic['filename']
            # 获取下载文件大小
            get_file_size = header_dic['filesize']
            # 获取文件下载MD5校验值
            get_file_md5 = header_dic['md5']

            # 写入文件
            with open(get_file_name,'wb') as write_file:
                down_size = 0
                while down_size < get_file_size:
                    # 接收文件内容
                    recv_line = self.client.recv(self.request_packet_size)
                    # 写入文件
                    write_file.write(recv_line)

                    # 接受内容大小
                    down_size += len(recv_line)

                    # 下载文件进度百分比
                    down_percent = down_size / get_file_size

                    # 进度条输出格式
                    my_str = ('[%%-%ds]' % 50) % (int(down_percent*50)*'#')
                    # 打印进度条
                    print('\r%s %s%%' % (my_str,int(down_percent*100)),end='',file=sys.stdout,flush=True)
                print('')

            # 文件MD5校验
            md5_data = self.md5_check(get_file_name)

            if md5_data != get_file_md5:
                print('File md5 check failed')

        # 文件不存在
        else:
            print('file not does exist')


    # put命令
    def put(self,file_path):
        # 发送文件大小
        file_size = os.path.getsize(file_path)

        # md5校验文件
        md5_data = self.md5_check(file_path)

        # 报头信息
        header_dic = {
            'filename': os.path.basename(file_path),
            'filesize': file_size,
            'md5': md5_data,
        }

        # 序列化报头
        pick_header = pickle.dumps(header_dic)
        # 报头长度
        header_length = struct.pack('i',len(pick_header))

        # 发送报头长度
        self.client.send(header_length)
        # 发送报头
        self.client.send(pick_header)

        # 接受磁盘配额是否足够信息
        quota_info = self.client.recv(1024)

        if quota_info.decode(self.code) == 'False':
            print('insufficient disk quota')
            return

        # 上传文件
        send_total_size = 0
        with open(file_path,'rb') as read_file:
            for line in read_file:
                # 发送文件一行内容
                self.client.send(line)
                # 发送总内容
                send_total_size += len(line)
                
                # 发送文件内容百分比
                put_percnet = send_total_size / file_size
                # 进度条输出格式
                my_str = ('[%%-%ds]' % 50) % (int(put_percnet*50)*'#')
                # 打印进度条
                print('\r%s %s%%' % (my_str,int(put_percnet*100)),end='',file=sys.stdout,flush=True)
            print('')

            # 接收文件MD5校验信息
            md5_data_byets = self.client.recv(self.request_packet_size)
            md5_data = md5_data_byets.decode(self.code)

            # 校验失败
            if md5_data == 'False':
                print('File md5 check failed')

if __name__ == '__main__':
    ftp_client = Myclient('127.0.0.1',8888)
    ftp_client.login()
client.py

 

模拟结果:

PUT

 

 

GET

posted @ 2018-12-20 14:45  浩小白  Views(358)  Comments(0Edit  收藏  举报