使用py3fdfs - 踩坑实录 __str__ return

2019-07-30  本文已影响0人  花括弧
django上传图片 和 用户获得html页面后请求图片 流程

fastdfs的优点:

在pycharm中导入py3fdfs

# 根据pypi中py3fdfs的示例: 在python3命令行执行下面两句
>>> from fdfs_client.client import *
>>> client = Fdfs_client('/etc/fdfs/client.conf')

在执行client = Fdfs_client('/etc/fdfs/client.conf')时,会报错:TypeError: type object argument after ** must be a mapping, not str
解决方法:
1)根据报错位置,定位到PycharmProjects/dailyfresh/venv/lib/python3.6/site-packages/fdfs_client/client.py
观察到 如下的代码:

class Fdfs_client(object):
    '''
    Class Fdfs_client implemented Fastdfs client protol ver 3.08.

    It's useful upload, download, delete file to or from fdfs server, etc. It's uses
    connection pool to manage connection to server.
    '''

    def __init__(self, trackers, poolclass=ConnectionPool):
        self.trackers = trackers
        self.tracker_pool = poolclass(**self.trackers)
        self.timeout = self.trackers['timeout']
        return None

发觉Fdfs_client的初始化要传递trackers, 而不是'/etc/fdfs/client.conf'字符串
接着观察到 文件顶部 有如下代码:

def get_tracker_conf(conf_path='client.conf'):
    cf = Fdfs_ConfigParser()
    tracker = {}
    try:
        cf.read(conf_path)
        timeout = cf.getint('__config__', 'connect_timeout')
        tracker_list = cf.get('__config__', 'tracker_server')
        if isinstance(tracker_list, str):
            tracker_list = [tracker_list]
        tracker_ip_list = []
        for tr in tracker_list:
            tracker_ip, tracker_port = tr.split(':')
            tracker_ip_list.append(tracker_ip)
        tracker['host_tuple'] = tuple(tracker_ip_list)
        tracker['port'] = int(tracker_port)
        tracker['timeout'] = timeout
        tracker['name'] = 'Tracker Pool'
    except:
        raise
    return tracker

def get_tracker_conf(conf_path='client.conf'):不就是 返回一个tracker么,而且其接收的参数是client.conf配置文件的路径
def get_tracker_conf(conf_path='client.conf'):函数的作用是:把配置文件client.conf中信息,提取到一个字典tracker中,并返回 该字典tracker

那么我们可以使用如下的代码 来代替(同时也是正确的):

# 由配置文件中的信息 得到 字典trackers 
trackers = get_tracker_conf('/Users/leesam/PycharmProjects/dailyfresh/utils/fdfs/client.conf')
# 把
client = Fdfs_client(trackers)

别急,填完这个坑,路的前方 还有好多坑呢。哈哈哈哈啊哈哈。

自定义了 文件存储类 用来将admin管理页面 添加的一条记录 保存到远端fdfs,点击保存按钮时,出现了 如下错误

__str__ return non-string (type bytes)
报错的意思大概是:返回了非字符串的bytes类型
由于之前 在项目中 只添加了如下的代码,而且 只有2个方法(只有2个返回值)。一个明确返回False,那么 错误 大概是出在 return filename这行。
况且, filename = res.get('Remote file_id')的确是 返回的bytes类型。那么,我们要把其从字节类型转换到字符串类型
使用decode()函数,把字节类型的 filename转换到字符串类型
return filename修改为return filename.decode()即可。 str(value), the type of value is bytes

原因分析:
自己写的文件存储类,返回的是字节型类型的文件名。执行的时候,在django内部的get_prep_value模块 接收到了 该文件名参数,并使用了str(value)进行了封装。所以, 才会报错__str__ returned non-string (type bytes).
由于,报错位置 跟 实际问题的位置 不在一个地方,所以 问题藏得比较隐蔽。

文件存储类的代码如下(注意_save的返回值: 返回字符串类型):

from django.core.files.storage import Storage

from fdfs_client.client import *

class FDFSStorage(Storage):
    '''fastdfs文件存储类'''

    def _open(self, name, mode='rb'):
        '''打开文件时 调用该函数'''
        pass

    # 通过后台管理页面,选文件 并 上传时
    # django会调用_save方法(并给_save方法传递2个参数:name: 所要上传文件的名字,content: (包含文件内容的)File类的实例对象)
    def _save(self, name, content):
        '''保存文件时 调用该函数'''
        # name: 所要上传文件的名字
        # content: File类的实例(包含上传文件内容的File实例对象)

        # 创建一个Fdfs_client对象
        # client = Fdfs_client('./utils/fdfs/client.conf')    #会根据./utils/fdfs/client.conf文件的配置,传给远端的tracker
        trackers = get_tracker_conf('/Users/leesam/PycharmProjects/dailyfresh/utils/fdfs/client.conf')
        client = Fdfs_client(trackers)

        # 上传文件到 fastdfs文件系统 中
        # content.read() 可以从File的实例对象content中 读取 文件内容
        # upload_by_buffer返回内容为 字典。格式如下 注释部分
        res = client.upload_by_buffer(content.read()) # upload_by_buffer 根据文件内容 上传文件

        # dict {
        #
        #     'Group name': group_name,
        #     'Remote file_id': remote_file_id,
        #     'Status': 'Upload successed.',
        #     'Local file name': '',
        #     'Uploaded size': upload_size,
        #     'Storage IP': storage_ip
        #
        # }

        if res.get('Status') != 'Upload successed.':
            # 上传失败
            raise Exception('upload file to fastdfs failed.')

        # 获取 返回的 文件id
        filename = res.get('Remote file_id')

        return filename.decode()

    # django在调用_save之前,会先调用_exists
    # _exists 根据 文件的name,判断 文件 是否存在于 文件系统中。存在:返回True,不存在:返回False
    def exists(self, name):
        '''django 判断 文件名 是否可用'''
        # 因为 文件是存储在 fastdfs文件系统中的,所以 对于django来说:不存在 文件名不可用 的情况
        return False

改进方法:

在setting.py增加以下内容

# 设置django的文件存储类,上传文件时 django会调用 该文件存储类的相关方法
DEFAULT_FILE_STORAGE = 'utils.fdfs.storage.FDFSStorage'

# 设置 fastdfs文件系统 使用的 client.conf文件路径
FDFS_CLIENT_CONF = './utils/fdfs/client.conf'
# 设置 fastdfs存储服务器上 nginx使用的IP和端口号
FDFS_STORAGE_URL = 'http://10.211.55.15:8888/'

定义自己的storage类:

from django.core.files.storage import Storage

from fdfs_client.client import *

from django.conf import settings

class FDFSStorage(Storage):
    '''fastdfs文件存储类'''
    def  __init__(self, client_conf=None, base_url=None):
        if client_conf == None:
            client_conf = settings.FDFS_CLIENT_CONF
        self.client_conf = client_conf

        if base_url == None:
            base_url = settings.FDFS_STORAGE_URL
        self.base_url = base_url


    def _open(self, name, mode='rb'):
        '''打开文件时 调用该函数'''
        # 用不到 打开文件,所以省略
        pass

    # 通过后台管理页面,选文件 并 上传时
    # django会调用_save方法(并给_save方法传递2个参数:name: 所要上传文件的名字,content: (包含文件内容的)File类的实例对象)
    def _save(self, name, content):
        '''保存文件时 调用该函数'''
        # name: 所要上传文件的名字
        # content: File类的实例(包含上传文件内容的File实例对象)
        # 返回值: fastdfs中 存储文件时 使用的文件名(被保存到 数据库的表 中)

        # 创建一个Fdfs_client对象
        # client = Fdfs_client('./utils/fdfs/client.conf')    #会根据./utils/fdfs/client.conf文件的配置,传给远端的tracker
        # trackers = get_tracker_conf('/Users/leesam/PycharmProjects/dailyfresh/utils/fdfs/client.conf')
        trackers = get_tracker_conf(self.client_conf)
        client = Fdfs_client(trackers)

        # 上传文件到 fastdfs文件系统 中
        # content.read() 可以从File的实例对象content中 读取 文件内容
        # upload_by_buffer返回内容为 字典。格式如下 注释部分
        res = client.upload_by_buffer(content.read()) # upload_by_buffer 根据文件内容 上传文件

        # dict {
        #
        #     'Group name': group_name,
        #     'Remote file_id': remote_file_id,
        #     'Status': 'Upload successed.',
        #     'Local file name': '',
        #     'Uploaded size': upload_size,
        #     'Storage IP': storage_ip
        #
        # }

        if res.get('Status') != 'Upload successed.':
            # 上传失败
            raise Exception('upload file to fastdfs failed.')

        # 获取 返回的 文件id
        filename = res.get('Remote file_id')
        # 只能返回str类型, filename为bytes类型(需要转换类型,不然会报错)
        return filename.decode()

    # django在调用_save之前,会先调用_exists
    # _exists 根据 文件的name,判断 文件 是否存在于 文件系统中。存在:返回True,不存在:返回False
    def exists(self, name):
        '''django 判断 文件名 是否可用'''
        # 因为 文件是存储在 fastdfs文件系统中的,所以 对于django来说:不存在 文件名不可用 的情况
        # 因为 fastdfs是根据文件内容 得到文件名的(不存在文件名相同 文件内容不同,因而 无法存储的问题)
        return False

    def url(self, name):
        '''返回 访问文件name 所需的url路径'''
        # django调用url方法时,所传递的 name参数:数据库 表中所存的 文件名字符串(即是,fastdfs中存储文件时 使用的文件名)
        return self.base_url + name

compare:

# 存储类必须是:deconstructible,以便在迁移中的字段上使用它时可以序列化。 
# 只要你的字段有自己的参数:serializable,
#你可以使用django.utils.deconstruct.deconstructible类装饰器(这是Django在FileSystemStorage上使用的)
@deconstructible
class FdfsStorage(Storage):
    def __init__(self, option=None):
        if not option:
            self.option = settings.CUSTOM_STORAGE_OPTIONS
        else:
            self.option = option

上一篇 下一篇

猜你喜欢

热点阅读