django

Django 1.6 文件上传下载 (raw edition)

今天项目做到文件上传的一步了,本来 Django 考虑得很周到,有一堆很优美很成系统的方法来处理文件,但是由于没时间继续学了,只有退而求其次,用一种裸(raw)的办法,按照以往的架构来处理上传文件。

下面先简述一下我以往的文件处理办法:

  1. 所有上传的文件都以 sha1 重命名,不带任何修饰,全部存放在一个指定的上传文件目录中;
  2. 用一个表 file_base 指向这些文件,只存放sha1(主键)和文件大小,这样一来如果有几个文件名不同但是内容一样的文件,就可以重复存储;
  3. 对于每个文件引用的地方,创建一个 file_referece 表添加到 file_base 的外键, 然后通过这个就可以实现 file_base 的引用计数和垃圾清理了;

以上的逻辑,我就先创建了这么两个模型:

class FileBase(models.Model):
    """
    文件基类,实际文件存放在 Option.get('file_path') 目录
    """
    hash = models.CharField(max_length=40, primary_key=True)  # sha1 code
    size = models.IntegerField()


class FileReference(models.Model):
    """
    文件引用,实际引用文件通过一对一引用到此对象
    用以计算 FileBase 的引用计数
    """
    file = models.ForeignKey(FileBase, related_name='refs', on_delete=models.PROTECT)
    name = models.CharField(max_length=260)
    mime = models.CharField(max_length=100)
    timestamp = models.DateTimeField(auto_now_add=True)

    def get_path(self):
        path = Option.get('file_path') or 'd:/data'
        return os.path.join(path, self.file.hash)

    @classmethod
    def from_upload(cls, file_uploaded, **kwargs):
        path = Option.get('file_path') or 'd:/data'
        tmp_name = tempfile.mktemp()
        with open(tmp_name, 'wb') as tar:
            sha1 = hashlib.sha1()
            for chunk in file_uploaded.chunks():
                sha1.update(chunk)
                tar.write(chunk)
            tar.close()
            hash = sha1.hexdigest()
            tar_path = os.path.join(path, hash)
            shutil.move(tmp_name, tar_path)
        (fb, created) = FileBase.objects.get_or_create(
            pk=hash, defaults={'size': file_uploaded._size})
        return cls.objects.create(
            file=fb, name=file_uploaded._name,
            mime=file_uploaded.content_type, **kwargs
        )

    def __str__(self):
        return self.name

上面有一个 from_upload 方法处理比较巧妙,但是需要我们先往下看才能知道内里乾坤。

视图:

首当其冲的问题是如何在 view 中处理上传的文件,我们用 enctype="multipart/form-data 的表单提交了一个文件字段的时候,可以在视图的 request.FILES 里面找到这些文件:

def my_view(request):
    for key in request.FILES:
        file = request.FILES[key]
        print(file._name)  # 原始文件名不带路径
        print(file.content_type)  # MIME 类型字符串
        print(file._size)  # 文件大小字节数
        bin_all = file.read()  # 一次过读取文件内容(会占很多内存)
        for chunk in file.chuncks():
            fout.write(chunk)  # 逐块读取文件内容

这样子获取到的 file 对象是一个 UploadFile 对象(但事实上根据不同的文件大小可能是这个类的不同的派生类,但我们不去深究),通过上面的这一小段代码就已经可以看到如何操作上传过来的文件了,只需要用 python 基本的 open 打开一个文件写入即可完成操作,第一段代码里面还包含了文件 hash 的计算。

模型封装:

这里,我想要达到一个目的,因为使用上传文件的场合是很多的,可能是不同的对象在引用,我想确保 FileReference 是唯一的,那么原理上说如果我想在 Node 对象里面附加多个附件属性,我需要在 Node 和 FileReference 中间加插一个 “ManyToOne” 的关系;

但是我又希望在 Node 被删除的时候自动把对应的 FileReference 删除掉,那么显然如果我们再加一个 ManyToManyField 表来做关联会带来很多附加的代码。

于是我就对每一个像 Node 的对象创建一个 FileReference 的派生类 NodeFileReference,里面包含一个对 Node 的 ForeignKey,这样就可以达到效果了:Node 被删除,NodeFileReference 由于外键行被删除自己被级联删除,然后它通过一个 OneToOneField 继承于 FileReference,对应的 FileReference 也被删掉了(ok 太完美了)。

class NodeFileReference(FileReference):

    fref = models.OneToOneField(FileReference, parent_link=True)
    node = models.ForeignKey(Node, related_name='files')
方法的封装:

我们最后应该用一种什么接口去调用文件的接收和持久化呢?看回第一段代码的 from_upload 方法,它是一个 classmethod,因此在派生的时候会继承给 NodeFileReference,这个方法接收一个 UploadFile 对象,以及一个附加的 **kwargs,然后会自动完成文件的存储、保存在数据库,最后返回一个 NodeFileReference 对象。

这样的话我们就可以在视图里面直接通过这样的方式来为我们的 Node 对象上传一个文件:

def node_file_upload(request, pk):
    node = Node.objects.get(pk=pk)
    for fn in request.FILES:
        fref = NodeFileReference.from_upload(request.FILES[fn], node=node)
        node.files.add(fref)

这么少的代码完成了这么多优美的工作,而且还是用裸法炮制。越来越对 Django 有信心了!可惜项目太紧,三成功力也暂时没时间继续学习了。

下载文件:

至于文件下载,说白了不过是从一个 request 返回一个 response 罢了。但是至于在 django 里面,结合上面的架构,再考虑分块下载(而不是一个大文件塞下来)这样的考虑,就按下面的处理:

分块下载参考这篇文章:https://djangosnippets.org/snippets/365/

代码需要改动一下,我们这里的话,事实上要下载一个文件(包含文件名等信息,只需要给出一个 FileReference 对象即可)

于是用这一个视图函数解决战斗:

from django.utils.http import urlquote
from django.core.servers.basehttp import FileWrapper

def send_file(request, pk):
    """
    Send a file through Django without loading the whole file into
    memory at once. The FileWrapper will turn the file object into an
    iterator for chunks of 8KB.
    """
    fref = get_object_or_404(FileReference, pk=pk)
    filename = fref.get_path()  # Select your file here.
    wrapper = FileWrapper(open(filename, 'rb'))
    response = HttpResponse(wrapper, content_type=fref.mime)
    response['Content-Length'] = os.path.getsize(filename)
    response['Content-Disposition'] = 'attachment; filename='+urlquote(fref.name)
    return response

【转载请附】愿以此功德,回向 >>

原文链接:https://www.huangwenchao.com.cn/2014/04/django-upload.html【Django 1.6 文件上传下载 (raw edition)】

发表评论

电子邮件地址不会被公开。 必填项已用*标注