Загрузка Django: удалять загруженные дубликаты, использовать существующий файл (проверка на основе md5)
У меня есть модель с FileField
, которая содержит загруженные пользователем файлы. Поскольку я хочу сэкономить место, я бы хотел избежать дубликатов.
Что я хотел бы достичь:
- Вычислить загруженные файлы контрольная сумма md5
- Сохраните файл с именем файла на основе его md5sum
- Если файл с таким именем уже существует (новый файл дубликат), отбросить загруженный файл и вместо него использовать существующий файл
1 и 2 уже работает, но как бы я забыл о загруженном дубликате и вместо этого использовал существующий файл?
Обратите внимание, что я хотел бы сохранить существующий файл и не перезаписывать его (в основном, чтобы сохранить измененное время одинаковым - лучше для резервного копирования).
Примечания:
- Я использую 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)
Любая помощь приветствуется!
Ответы
Ответ 1
Благодаря ответу alTus я смог выяснить, что написание пользовательского класса хранилища является ключом, и это оказалось проще, чем ожидалось.
- Я просто опускаю вызов метода суперкласса
_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 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)
Ответ 2
AFAIK вы не можете легко реализовать это, используя методы сохранения/удаления. Файлы coz обрабатываются совершенно определенно.
Но вы могли бы попробовать что-то подобное.
Во-первых, моя простая хеш-функция md5 файла:
def md5_for_file(chunks):
md5 = hashlib.md5()
for data in chunks:
md5.update(data)
return md5.hexdigest()
Далее simple_upload_to
- это smth, как ваша функция 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)
Как вы можете видеть, это пользовательское хранилище удаляет файл перед сохранением, а затем сохраняет новый с тем же именем.
Таким образом, здесь вы можете реализовать свою логику, если важно не удалять (и, следовательно, обновлять) файлы.
Подробнее о хранилищах ou можно найти здесь: https://docs.djangoproject.com/en/1.5/ref/files/storage/
Ответ 3
У меня была такая же проблема, и я нашел этот вопрос. Поскольку это ничего необычного, я искал в Интернете и нашел следующий пакет Python, который швы делает именно то, что вы хотите:
https://pypi.python.org/pypi/django-hashedfilenamestorage
Если хэши SHA1 не под вопросом, я думаю, что запрос на добавление для добавления поддержки хеширования MD5 будет отличной идеей.
Ответ 4
Этот ответ помог мне решить проблему, когда я хотел создать исключение, если уже загруженный файл уже существует. Эта версия вызывает исключение, если файл с тем же именем уже существует в месте загрузки.
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)
Ответ 5
Данные идут из шаблона → формы → представления → дБ. Имеет смысл останавливать дубликаты на самом раннем этапе. В этом случае 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