【问题标题】:Django uploads: Discard uploaded duplicates, use existing file (md5 based check)Django 上传:丢弃上传的重复文件,使用现有文件(基于 md5 的检查)
【发布时间】:2013-03-30 21:28:53
【问题描述】:

我有一个带有FileField 的模型,它保存用户上传的文件。由于我想节省空间,所以我想避免重复。

我想要达到的目标:

  1. 计算上传的文件md5校验和
  2. 使用 基于其 md5sum 的文件名存储文件
  3. 如果同名文件已经存在(新文件是重复文件),放弃上传的文件并改用现有文件

12 已经在工作了,但是我怎么会忘记上传的副本而使用现有文件呢?

请注意,我想保留现有文件并且覆盖它(主要是为了保持修改时间相同 - 更适合备份)。

注意事项:

  • 我使用的是 Django 1.5
  • 上传处理程序是django.core.files.uploadhandler.TemporaryFileUploadHandler

代码:

def media_file_name(instance, filename):
    h = instance.md5sum
    basename, ext = os.path.splitext(filename)
    return os.path.join('mediafiles', h[0:1], h[1:2], h + ext.lower())

class Media(models.Model):
    orig_file = models.FileField(upload_to=media_file_name)
    md5sum = models.CharField(max_length=36)
    ...

    def save(self, *args, **kwargs):
            if not self.pk:  # file is new
                md5 = hashlib.md5()
                for chunk in self.orig_file.chunks():
                    md5.update(chunk)
                self.md5sum = md5.hexdigest()
            super(Media, self).save(*args, **kwargs)

感谢任何帮助!

【问题讨论】:

  • 您打算获得多少流量?如果是小型项目或私人项目,您可以每月支付 0.50 美元购买 Amazon S3、Rackspace Cloudfiles 或任何其他便宜的文件存储。

标签: python django django-models django-file-upload


【解决方案1】:

AFAIK 您无法使用保存/删除方法轻松实现这一点,因为文件被专门处理。

但你可以试试这样。

首先,我的简单md5文件哈希函数:

def md5_for_file(chunks):
    md5 = hashlib.md5()
    for data in chunks:
        md5.update(data)
    return md5.hexdigest()

下一个simple_upload_to 就像你的 media_file_name 函数一样。 你应该这样使用它:

def simple_upload_to(field_name, path='files'):
    def upload_to(instance, filename):
        name = md5_for_file(getattr(instance, field_name).chunks())
        dot_pos = filename.rfind('.')
        ext = filename[dot_pos:][:10].lower() if dot_pos > -1 else '.unknown'
        name += ext
        return os.path.join(path, name[:2], name)
    return upload_to

class Media(models.Model):
    # see info about storage below
    orig_file = models.FileField(upload_to=simple_upload_to('orig_file'), storage=MyCustomStorage())

当然,这只是一个示例,因此路径生成逻辑可以多种多样。

还有最重要的部分:

from django.core.files.storage import FileSystemStorage

class MyCustomStorage(FileSystemStorage):
    def get_available_name(self, name):
        return name

    def _save(self, name, content):
        if self.exists(name):
            self.delete(name)
        return super(MyCustomStorage, self)._save(name, content)

如您所见,此自定义存储在保存之前会删除文件,然后以相同的名称保存新文件。 因此,如果不删除(并因此更新)文件很重要,您可以在这里实现您的逻辑。

您可以在此处找到有关存储的更多信息:https://docs.djangoproject.com/en/1.5/ref/files/storage/

【讨论】:

    【解决方案2】:

    感谢 alTus 的回答,我发现写一个 custom storage class 是关键,而且比预期的要容易。

    • 如果文件已经存在,我只是省略了调用超类_save 方法来写入文件,我只是返回名称。
    • 我覆盖get_available_name,以避免在同名文件已经存在时将数字附加到文件名中

    我不知道这是否是正确的方法,但到目前为止效果很好。

    希望这有用!

    这是完整的示例代码:

    import hashlib
    import os
    
    from django.core.files.storage import FileSystemStorage
    from django.db import models
    
    class MediaFileSystemStorage(FileSystemStorage):
        def get_available_name(self, name, max_length=None):
            if max_length and len(name) > max_length:
                raise(Exception("name's length is greater than max_length"))
            return name
    
        def _save(self, name, content):
            if self.exists(name):
                # if the file exists, do not call the superclasses _save method
                return name
            # if the file is new, DO call it
            return super(MediaFileSystemStorage, self)._save(name, content)
    
    
    def media_file_name(instance, filename):
        h = instance.md5sum
        basename, ext = os.path.splitext(filename)
        return os.path.join('mediafiles', h[0:1], h[1:2], h + ext.lower())
    
    
    class Media(models.Model):
        # use the custom storage class fo the FileField
        orig_file = models.FileField(
            upload_to=media_file_name, storage=MediaFileSystemStorage())
        md5sum = models.CharField(max_length=36)
        # ...
    
        def save(self, *args, **kwargs):
            if not self.pk:  # file is new
                md5 = hashlib.md5()
                for chunk in self.orig_file.chunks():
                    md5.update(chunk)
                self.md5sum = md5.hexdigest()
            super(Media, self).save(*args, **kwargs)
    

    【讨论】:

    • 很不错的代码:h[0:1], h[1:2] 在路径中有什么用?
    • 哦,这只是为了分发到不同的目录/0/0/ - /f/f/,我不想将所有文件都存储在一个目录中。
    • 这仍然会在数据库中创建一个具有新 pk 但文件名相同的条目 - 你是如何处理的?
    • 我不确定你的意思 - 如果文件已经存在,我不会试图阻止文件上传,我只是将同一个文件重新用于另一个模型实例。如果你想阻止一起上传副本,你可以使用验证器。
    【解决方案3】:

    这个答案帮助我解决了我想在上传的文件已经存在时引发异常的问题。如果上传位置中已存在同名文件,则此版本会引发异常。

    from django.core.files.storage import FileSystemStorage
    
    class FailOnDuplicateFileSystemStorage(FileSystemStorage):
        def get_available_name(self, name):
            return name
    
        def _save(self, name, content):
            if self.exists(name):
                raise ValidationError('File already exists: %s' % name)
    
            return super(
                FailOnDuplicateFileSystemStorage, self)._save(name, content)
    

    【讨论】:

      【解决方案4】:

      我遇到了同样的问题,发现了这个 SO question。因为这并不少见,所以我在网上搜索并找到了以下 Python 包,它可以完全满足您的要求:

      https://pypi.python.org/pypi/django-hashedfilenamestorage

      如果 SHA1 哈希没有问题,我认为添加 MD5 哈希支持的拉取请求将是一个好主意。

      【讨论】:

        【解决方案5】:

        数据来自模板 -> 表单 -> 视图 -> db(model)。在最早的步骤中停止重复是有意义的。在这种情况下,forms.py。

        # scripts.py
        import hashlib
        from .models import *
        def generate_sha(file):
            sha = hashlib.sha1()
            file.seek(0)
            while True:
                buf = file.read(104857600)
                if not buf:
                    break
                sha.update(buf)
            sha1 = sha.hexdigest()
            file.seek(0)
            return sha1
        
        # models.py
        class images(models.Model):
            label = models.CharField(max_length=21, blank=False, null=False)
            image = models.ImageField(upload_to='images/')
            image_sha1 = models.CharField(max_length=40, blank=False, null=False)
            create_time = models.DateTimeField(auto_now=True)
        
        # forms.py
        class imageForm(forms.Form):
            Label = forms.CharField(max_length=21, required=True)
            Image = forms.ImageField(required=True)
        
            def clean(self):
                cleaned_data = super(imageForm, self).clean()
                Label = cleaned_data.get('Label')
                Image = cleaned_data.get('Image')
                sha1 = generate_sha(Image)
                if images.objects.filter(image_sha1=sha1).exists():
                    raise forms.ValidationError('This already exists')
                if not Label:
                    raise forms.ValidationError('No Label')
                if not Image:
                    raise forms.ValidationError('No Image')
        
        # views.py
        from .scripts import *
        from .models import *
        from .forms import *
        
        def image(request):
            if request.method == 'POST':
                form = imageForm(request.POST, request.FILES)
                if form.is_valid():
                    photo = images (
                        payee=request.user,
                        image=request.FILES['Image'],
                        image_sha1=generate_sha(request.FILES['Image'],),
                        label=form.cleaned_data.get('Label'),
                        )
                    photo.save()
                    return render(request, 'stars/image_form.html', {'form' : form})
            else:
                form = imageForm()
            context = {'form': form,}
            return render(request, 'stars/image_form.html', context)
        
        # image_form.html
        {% extends "base.html" %}
        {% load static %}
        {% load staticfiles %}
        
        {% block content %}
        
         <div class="card mx-auto shadow p-3 mb-5 bg-white rounded text-left" style="max-width: 50rem;">
            <div class="container">
                <form action="{% url 'wallet' %}" method="post" enctype="multipart/form-data">
                    {% csrf_token %}
                    {{ form  }}
                    <input type="submit" value="Upload" class="btn btn-outlined-primary">
                </form>
        
                {% if form.errors %}
                    {% for field in form %}
                        {% for error in field.errors %}
                            <p> {{ error }} </p>
                        {% endfor %}
                    {% endfor %}
                {% endif %}
        
            </div>
        </div>
        
        {% endblock content %}  
        
        

        参考:http://josephmosby.com/2015/05/13/preventing-file-dupes-in-django.html

        【讨论】:

        • 是的,但这是针对不同的用例:防止重复文件被上传和存储;我希望有单独的模型实例引用一个文件,而不是浪费磁盘空间在磁盘上多次保存相同的文件数据。
        猜你喜欢
        • 1970-01-01
        • 2022-08-06
        • 1970-01-01
        • 2018-02-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-25
        相关资源
        最近更新 更多