Django文件存储 自己定制存储系统解析


Posted in Python onAugust 02, 2019

要自己写一个存储系统,可以依照以下步骤:

1.写一个继承自django.core.files.storage.Storage的子类。

from django.core.files.storage import Storage
class MyStorage(Storage):
  ...

2.Django必须可以在无任何参数的情况下实例化MyStorage,所以任何环境设置必须来自django.conf.settings。

from django.conf import settings
from django.core.files.storage import Storage
 
class MyStorage(Storage):
  def __init__(self, option=None):
    if not option:
      option = settings.CUSTOM_STORAGE_OPTIONS
    ...

3.根据Storage的open和save方法源码:

def open(self, name, mode='rb'):
  """
  Retrieves the specified file from storage.
  """
  return self._open(name, mode)
 
 
def save(self, name, content, max_length=None):
  """
  Saves new content to the file specified by name. The content should be
  a proper File object or any python file-like object, ready to be read
  from the beginning.
  """
  # Get the proper name for the file, as it will actually be saved.
  if name is None:
    name = content.name
 
  if not hasattr(content, 'chunks'):
    content = File(content, name)
 
  name = self.get_available_name(name, max_length=max_length)
  return self._save(name, content)

MyStorage需要实现_open和_save方法。

如果写的是个本地存储系统,还要重写path方法。

4.使用django.utils.deconstruct.deconstructible装饰器,以便在migration可以序列化。

还有,Storage.delete()、Storage.exists()、Storage.listdir()、Storage.size()、Storage.url()方法都会报NotImplementedError,也需要重写。

Django Qiniu Storage

七牛云有自己的django storage系统,可以看下是怎么运作的,地址 https://github.com/glasslion/django-qiniu-storage 。

先在环境变量或者settings中配置QINIU_ACCESS_KEY、QINIU_SECRET_KEY、QINIU_BUCKET_NAME、QINIU_BUCKET_DOMAIN、QINIU_SECURE_URL。

使用七牛云托管用户上传的文件,在 settings.py 里设置DEFAULT_FILE_STORAGE:

DEFAULT_FILE_STORAGE = 'qiniustorage.backends.QiniuStorage'

使用七牛托管动态生成的文件以及站点自身的静态文件,设置:

STATICFILES_STORAGE = 'qiniustorage.backends.QiniuStaticStorage'

运行python manage.py collectstatic,静态文件就会被统一上传到七牛。

QiniuStorage代码如下:

@deconstructible
class QiniuStorage(Storage):
  """
  Qiniu Storage Service
  """
  location = ""
 
  def __init__(
      self,
      access_key=QINIU_ACCESS_KEY,
      secret_key=QINIU_SECRET_KEY,
      bucket_name=QINIU_BUCKET_NAME,
      bucket_domain=QINIU_BUCKET_DOMAIN,
      secure_url=QINIU_SECURE_URL):
 
    self.auth = Auth(access_key, secret_key)
    self.bucket_name = bucket_name
    self.bucket_domain = bucket_domain
    self.bucket_manager = BucketManager(self.auth)
    self.secure_url = secure_url
 
  def _clean_name(self, name):
    """
    Cleans the name so that Windows style paths work
    """
    # Normalize Windows style paths
    clean_name = posixpath.normpath(name).replace('\\', '/')
 
    # os.path.normpath() can strip trailing slashes so we implement
    # a workaround here.
    if name.endswith('/') and not clean_name.endswith('/'):
      # Add a trailing slash as it was stripped.
      return clean_name + '/'
    else:
      return clean_name
 
  def _normalize_name(self, name):
    """
    Normalizes the name so that paths like /path/to/ignored/../foo.txt
    work. We check to make sure that the path pointed to is not outside
    the directory specified by the LOCATION setting.
    """
 
    base_path = force_text(self.location)
    base_path = base_path.rstrip('/')
 
    final_path = urljoin(base_path.rstrip('/') + "/", name)
 
    base_path_len = len(base_path)
    if (not final_path.startswith(base_path) or
        final_path[base_path_len:base_path_len + 1] not in ('', '/')):
      raise SuspiciousOperation("Attempted access to '%s' denied." %
                   name)
    return final_path.lstrip('/')
 
  def _open(self, name, mode='rb'):
    return QiniuFile(name, self, mode)
 
  def _save(self, name, content):
    cleaned_name = self._clean_name(name)
    name = self._normalize_name(cleaned_name)
 
    if hasattr(content, 'chunks'):
      content_str = b''.join(chunk for chunk in content.chunks())
    else:
      content_str = content.read()
 
    self._put_file(name, content_str)
    return cleaned_name
 
  def _put_file(self, name, content):
    token = self.auth.upload_token(self.bucket_name)
    ret, info = put_data(token, name, content)
    if ret is None or ret['key'] != name:
      raise QiniuError(info)
 
  def _read(self, name):
    return requests.get(self.url(name)).content
 
  def delete(self, name):
    name = self._normalize_name(self._clean_name(name))
    if six.PY2:
      name = name.encode('utf-8')
    ret, info = self.bucket_manager.delete(self.bucket_name, name)
 
    if ret is None or info.status_code == 612:
      raise QiniuError(info)
 
  def _file_stat(self, name, silent=False):
    name = self._normalize_name(self._clean_name(name))
    if six.PY2:
      name = name.encode('utf-8')
    ret, info = self.bucket_manager.stat(self.bucket_name, name)
    if ret is None and not silent:
      raise QiniuError(info)
    return ret
 
  def exists(self, name):
    stats = self._file_stat(name, silent=True)
    return True if stats else False
 
  def size(self, name):
    stats = self._file_stat(name)
    return stats['fsize']
 
  def modified_time(self, name):
    stats = self._file_stat(name)
    time_stamp = float(stats['putTime']) / 10000000
    return datetime.datetime.fromtimestamp(time_stamp)
 
  def listdir(self, name):
    name = self._normalize_name(self._clean_name(name))
    if name and not name.endswith('/'):
      name += '/'
 
    dirlist = bucket_lister(self.bucket_manager, self.bucket_name,
                prefix=name)
    files = []
    dirs = set()
    base_parts = name.split("/")[:-1]
    for item in dirlist:
      parts = item['key'].split("/")
      parts = parts[len(base_parts):]
      if len(parts) == 1:
        # File
        files.append(parts[0])
      elif len(parts) > 1:
        # Directory
        dirs.add(parts[0])
    return list(dirs), files
 
  def url(self, name):
    name = self._normalize_name(self._clean_name(name))
    name = filepath_to_uri(name)
    protocol = u'https://' if self.secure_url else u'http://'
    return urljoin(protocol + self.bucket_domain, name)

配置是从环境变量或者settings.py中获得的:

def get_qiniu_config(name, default=None):
  """
  Get configuration variable from environment variable
  or django setting.py
  """
  config = os.environ.get(name, getattr(settings, name, default))
  if config is not None:
    if isinstance(config, six.string_types):
      return config.strip()
    else:
      return config
  else:
    raise ImproperlyConfigured(
      "Can't find config for '%s' either in environment"
      "variable or in setting.py" % name) 
QINIU_ACCESS_KEY = get_qiniu_config('QINIU_ACCESS_KEY')
QINIU_SECRET_KEY = get_qiniu_config('QINIU_SECRET_KEY')
QINIU_BUCKET_NAME = get_qiniu_config('QINIU_BUCKET_NAME')
QINIU_BUCKET_DOMAIN = get_qiniu_config('QINIU_BUCKET_DOMAIN', '').rstrip('/')
QINIU_SECURE_URL = get_qiniu_config('QINIU_SECURE_URL', 'False')

重写了_open和_save方法:

def _open(self, name, mode='rb'):
  return QiniuFile(name, self, mode) 
def _save(self, name, content):
  cleaned_name = self._clean_name(name)
  name = self._normalize_name(cleaned_name) 
  if hasattr(content, 'chunks'):
    content_str = b''.join(chunk for chunk in content.chunks())
  else:
    content_str = content.read() 
  self._put_file(name, content_str)
  return cleaned_name

使用的put_data方法上传文件,相关代码如下:

def put_data(
    up_token, key, data, params=None, mime_type='application/octet-stream', check_crc=False, progress_handler=None,
    fname=None):
  """上传二进制流到七牛 
  Args:
    up_token:     上传凭证
    key:       上传文件名
    data:       上传二进制流
    params:      自定义变量,规格参考 http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
    mime_type:    上传数据的mimeType
    check_crc:    是否校验crc32
    progress_handler: 上传进度
 
  Returns:
    一个dict变量,类似 {"hash": "<Hash string>", "key": "<Key string>"}
    一个ResponseInfo对象
  """
  crc = crc32(data) if check_crc else None
  return _form_put(up_token, key, data, params, mime_type, crc, progress_handler, fname)
 
def _form_put(up_token, key, data, params, mime_type, crc, progress_handler=None, file_name=None):
  fields = {}
  if params:
    for k, v in params.items():
      fields[k] = str(v)
  if crc:
    fields['crc32'] = crc
  if key is not None:
    fields['key'] = key 
  fields['token'] = up_token
  url = config.get_default('default_zone').get_up_host_by_token(up_token) + '/'
  # name = key if key else file_name
 
  fname = file_name
  if not fname or not fname.strip():
    fname = 'file_name'
 
  r, info = http._post_file(url, data=fields, files={'file': (fname, data, mime_type)})
  if r is None and info.need_retry():
    if info.connect_failed:
      url = config.get_default('default_zone').get_up_host_backup_by_token(up_token) + '/'
    if hasattr(data, 'read') is False:
      pass
    elif hasattr(data, 'seek') and (not hasattr(data, 'seekable') or data.seekable()):
      data.seek(0)
    else:
      return r, info
    r, info = http._post_file(url, data=fields, files={'file': (fname, data, mime_type)})
 
  return r, info 
def _post_file(url, data, files):
  return _post(url, data, files, None) 
def _post(url, data, files, auth, headers=None):
  if _session is None:
    _init()
  try:
    post_headers = _headers.copy()
    if headers is not None:
      for k, v in headers.items():
        post_headers.update({k: v})
    r = _session.post(
      url, data=data, files=files, auth=auth, headers=post_headers,
      timeout=config.get_default('connection_timeout'))
  except Exception as e:
    return None, ResponseInfo(None, e)
  return __return_wrapper(r) 
def _init():
  session = requests.Session()
  adapter = requests.adapters.HTTPAdapter(
    pool_connections=config.get_default('connection_pool'), pool_maxsize=config.get_default('connection_pool'),
    max_retries=config.get_default('connection_retries'))
  session.mount('http://', adapter)
  global _session
  _session = session

最终使用的是requests库上传文件的,统一适配了链接池个数、链接重试次数。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python中去空格函数的用法
Aug 21 Python
Python中使用ConfigParser解析ini配置文件实例
Aug 30 Python
Python实现抓取网页生成Excel文件的方法示例
Aug 05 Python
python按综合、销量排序抓取100页的淘宝商品列表信息
Feb 24 Python
python的中异常处理机制
Aug 30 Python
Python创建字典的八种方式
Feb 27 Python
Pycharm 2019 破解激活方法图文详解
Oct 11 Python
py-charm延长试用期限实例
Dec 22 Python
python生成大写32位uuid代码
Mar 03 Python
使用 prometheus python 库编写自定义指标的方法(完整代码)
Jun 29 Python
浅谈Python3中print函数的换行
Aug 05 Python
Python如何让字典保持有序排列
Apr 29 Python
使用pycharm在本地开发并实时同步到服务器
Aug 02 #Python
Django文件存储 默认存储系统解析
Aug 02 #Python
Django 迁移、操作数据库的方法
Aug 02 #Python
Django用户认证系统 组与权限解析
Aug 02 #Python
python3中eval函数用法使用简介
Aug 02 #Python
Django用户认证系统 Web请求中的认证解析
Aug 02 #Python
Django用户认证系统 User对象解析
Aug 02 #Python
You might like
索尼ICF-SW100收音机评测
2021/03/02 无线电
PHP生成UTF8文件的方法
2010/05/15 PHP
PHP高自定义性安全验证码代码
2011/11/27 PHP
介绍一些PHP判断变量的函数
2012/04/24 PHP
mysql,mysqli,PDO的各自不同介绍
2012/09/19 PHP
简单谈谈php中ob_flush和flush的区别
2014/11/27 PHP
PHP页面跳转操作实例分析(header方法)
2016/09/28 PHP
JavaScript DOM学习第八章 表单错误提示
2010/02/19 Javascript
jQuery-ui中自动完成实现方法
2010/06/10 Javascript
js URL参数的拼接方法比较
2012/02/15 Javascript
jquery js 获取时间差、时间格式具体代码
2013/06/05 Javascript
在服务端(Page.Write)调用自定义的JS方法详解
2013/08/09 Javascript
node.js中的fs.readlink方法使用说明
2014/12/17 Javascript
深入理解JavaScript系列(50):Function模式(下篇)
2015/03/04 Javascript
JS实现仿FLASH效果的竖排导航代码
2015/09/15 Javascript
JS实现点击事件统计的简单实例
2016/07/10 Javascript
AngularJS 中文API参考手册
2016/07/28 Javascript
jQuery扩展实现text提示还能输入多少字节的方法
2016/11/28 Javascript
微信小程序如何播放腾讯视频的实现
2019/09/20 Javascript
vue实现一个矩形标记区域(rectangle marker)的方法
2020/10/28 Javascript
vue+Element-ui实现分页效果
2020/11/15 Javascript
在Python中使用sort()方法进行排序的简单教程
2015/05/21 Python
在Mac OS上搭建Python的开发环境
2015/12/24 Python
Python实现简单遗传算法(SGA)
2018/01/29 Python
Django自定义过滤器定义与用法示例
2018/03/22 Python
用Python读取几十万行文本数据
2018/12/24 Python
Python实现最大子序和的方法示例
2019/07/05 Python
解决python中导入win32com.client出错的问题
2019/07/26 Python
用canvas显示验证码的实现
2020/04/10 HTML / CSS
毕业生求职的求职信
2013/12/05 职场文书
农村婚礼证婚词
2014/01/08 职场文书
红旗团支部事迹材料
2014/01/27 职场文书
领导班子整改措施
2014/10/24 职场文书
2014年餐厅服务员工作总结
2014/11/18 职场文书
领导干部学习十八届五中全会精神心得体会
2016/01/05 职场文书
《社戏》教学反思
2016/02/22 职场文书