Загрузка файлов в Django

Django предоставляет удобные инструменты для работы с загруженными файлами. Это мощная система для обработки загрузки, валидации и хранения различных типов файлов.

Модель с файловым полем

Базовая модель для работы с файлами:

 1from django.db import models
 2from django.core.validators import FileExtensionValidator
 3
 4class Book(models.Model):
 5    title = models.CharField(max_length=200)
 6    description = models.TextField()
 7    cover_image = models.ImageField(
 8        upload_to='covers/',
 9        validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif'])],
10        help_text='Загрузите обложку книги (JPG, PNG, GIF)'
11    )
12    pdf_file = models.FileField(
13        upload_to='books/',
14        validators=[FileExtensionValidator(allowed_extensions=['pdf'])],
15        help_text='PDF файл книги'
16    )
17    created_at = models.DateTimeField(auto_now_add=True)
18    updated_at = models.DateTimeField(auto_now=True)
19
20    def __str__(self):
21        return self.title
22
23    def get_file_size(self):
24        if self.pdf_file:
25            return f"{self.pdf_file.size / 1024:.1f} KB"
26        return "0 KB"

Форма для загрузки файлов

Создание формы с поддержкой файлов:

 1from django import forms
 2from .models import Book
 3
 4class BookForm(forms.ModelForm):
 5    class Meta:
 6        model = Book
 7        fields = ['title', 'description', 'cover_image', 'pdf_file']
 8        widgets = {
 9            'title': forms.TextInput(attrs={'class': 'form-control'}),
10            'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
11            'cover_image': forms.FileInput(attrs={'class': 'form-control-file'}),
12            'pdf_file': forms.FileInput(attrs={'class': 'form-control-file'}),
13        }
14
15    def clean_cover_image(self):
16        image = self.cleaned_data.get('cover_image')
17        if image:
18            # Проверяем размер файла (максимум 5MB)
19            if image.size > 5 * 1024 * 1024:
20                raise forms.ValidationError('Размер изображения не должен превышать 5MB')
21
22            # Проверяем формат
23            if not image.content_type.startswith('image/'):
24                raise forms.ValidationError('Файл должен быть изображением')
25
26        return image
27
28    def clean_pdf_file(self):
29        pdf = self.cleaned_data.get('pdf_file')
30        if pdf:
31            # Проверяем размер файла (максимум 50MB)
32            if pdf.size > 50 * 1024 * 1024:
33                raise forms.ValidationError('Размер PDF файла не должен превышать 50MB')
34
35            # Проверяем формат
36            if not pdf.content_type == 'application/pdf':
37                raise forms.ValidationError('Файл должен быть в формате PDF')
38
39        return pdf

Представление для обработки загрузки

View для обработки загрузки файлов:

 1from django.shortcuts import render, redirect, get_object_or_404
 2from django.contrib import messages
 3from django.contrib.auth.decorators import login_required
 4from django.core.files.storage import default_storage
 5from django.conf import settings
 6import os
 7from .forms import BookForm
 8from .models import Book
 9
10@login_required
11def upload_book(request):
12    if request.method == 'POST':
13        form = BookForm(request.POST, request.FILES)
14        if form.is_valid():
15            book = form.save(commit=False)
16            book.user = request.user
17            book.save()
18
19            messages.success(request, f'Книга "{book.title}" успешно загружена!')
20            return redirect('book_detail', pk=book.pk)
21        else:
22            messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
23    else:
24        form = BookForm()
25
26    return render(request, 'books/upload_book.html', {'form': form})
27
28@login_required
29def update_book(request, pk):
30    book = get_object_or_404(Book, pk=pk, user=request.user)
31
32    if request.method == 'POST':
33        form = BookForm(request.POST, request.FILES, instance=book)
34        if form.is_valid():
35            # Удаляем старые файлы при обновлении
36            if 'cover_image' in request.FILES:
37                if book.cover_image:
38                    old_image_path = book.cover_image.path
39                    if os.path.exists(old_image_path):
40                        os.remove(old_image_path)
41
42            if 'pdf_file' in request.FILES:
43                if book.pdf_file:
44                    old_pdf_path = book.pdf_file.path
45                    if os.path.exists(old_pdf_path):
46                        os.remove(old_pdf_path)
47
48            form.save()
49            messages.success(request, 'Книга успешно обновлена!')
50            return redirect('book_detail', pk=book.pk)
51    else:
52        form = BookForm(instance=book)
53
54    return render(request, 'books/update_book.html', {'form': form, 'book': book})

HTML шаблон для загрузки

Создай файл templates/books/upload_book.html:

 1{% extends 'base.html' %}
 2{% load static %}
 3
 4{% block content %}
 5<div class="container mt-4">
 6    <h2>Загрузить новую книгу</h2>
 7
 8    <form method="post" enctype="multipart/form-data" class="mt-3">
 9        {% csrf_token %}
10
11        <div class="mb-3">
12            <label for="{{ form.title.id_for_label }}" class="form-label">Название книги</label>
13            {{ form.title }}
14            {% if form.title.errors %}
15                <div class="alert alert-danger mt-1">
16                    {{ form.title.errors }}
17                </div>
18            {% endif %}
19        </div>
20
21        <div class="mb-3">
22            <label for="{{ form.description.id_for_label }}" class="form-label">Описание</label>
23            {{ form.description }}
24            {% if form.description.errors %}
25                <div class="alert alert-danger mt-1">
26                    {{ form.description.errors }}
27                </div>
28            {% endif %}
29        </div>
30
31        <div class="mb-3">
32            <label for="{{ form.cover_image.id_for_label }}" class="form-label">Обложка книги</label>
33            {{ form.cover_image }}
34            <div class="form-text">{{ form.cover_image.help_text }}</div>
35            {% if form.cover_image.errors %}
36                <div class="alert alert-danger mt-1">
37                    {{ form.cover_image.errors }}
38                </div>
39            {% endif %}
40        </div>
41
42        <div class="mb-3">
43            <label for="{{ form.pdf_file.id_for_label }}" class="form-label">PDF файл</label>
44            {{ form.pdf_file }}
45            <div class="form-text">{{ form.pdf_file.help_text }}</div>
46            {% if form.pdf_file.errors %}
47                <div class="alert alert-danger mt-1">
48                    {{ form.pdf_file.errors }}
49                </div>
50            {% endif %}
51        </div>
52
53        <button type="submit" class="btn btn-primary">Загрузить книгу</button>
54    </form>
55</div>
56{% endblock %}

Настройка медиа файлов

Конфигурация в settings.py:

 1import os
 2from pathlib import Path
 3
 4# Базовые пути
 5BASE_DIR = Path(__file__).resolve().parent.parent
 6
 7# Медиа файлы
 8MEDIA_URL = '/media/'
 9MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
10
11# Максимальный размер загружаемого файла (в байтах)
12DATA_UPLOAD_MAX_MEMORY_SIZE = 52428800  # 50MB
13
14# Разрешенные типы файлов
15ALLOWED_UPLOAD_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx']
16MAX_UPLOAD_SIZE = 52428800  # 50MB

Настройка URL для медиа файлов

Добавь в urls.py:

 1from django.conf import settings
 2from django.conf.urls.static import static
 3from django.contrib import admin
 4from django.urls import path, include
 5
 6urlpatterns = [
 7    path('admin/', admin.site.urls),
 8    path('books/', include('books.urls')),
 9    # Другие URL паттерны...
10]
11
12# Добавляем URL для медиа файлов в режиме разработки
13if settings.DEBUG:
14    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Кастомные валидаторы

Создание собственных валидаторов файлов:

 1from django.core.exceptions import ValidationError
 2from django.core.files.uploadedfile import UploadedFile
 3import magic
 4import os
 5
 6def validate_file_type(uploaded_file: UploadedFile, allowed_types: list):
 7    """Валидация типа файла по MIME типу"""
 8    # Читаем первые 2048 байт для определения MIME типа
 9    uploaded_file.seek(0)
10    mime_type = magic.from_buffer(uploaded_file.read(2048), mime=True)
11    uploaded_file.seek(0)
12
13    if mime_type not in allowed_types:
14        raise ValidationError(f'Неподдерживаемый тип файла. Разрешены: {", ".join(allowed_types)}')
15
16def validate_file_size(uploaded_file: UploadedFile, max_size_mb: int):
17    """Валидация размера файла"""
18    max_size_bytes = max_size_mb * 1024 * 1024
19
20    if uploaded_file.size > max_size_bytes:
21        raise ValidationError(f'Размер файла не должен превышать {max_size_mb}MB')
22
23def validate_image_dimensions(uploaded_file: UploadedFile, min_width: int, min_height: int):
24    """Валидация размеров изображения"""
25    from PIL import Image
26
27    try:
28        with Image.open(uploaded_file) as img:
29            width, height = img.size
30
31            if width < min_width or height < min_height:
32                raise ValidationError(
33                    f'Минимальные размеры изображения: {min_width}x{min_height}px'
34                )
35    except Exception as e:
36        raise ValidationError('Не удалось прочитать изображение')
37
38# Использование в модели
39class Document(models.Model):
40    title = models.CharField(max_length=200)
41    file = models.FileField(
42        upload_to='documents/',
43        validators=[
44            lambda value: validate_file_type(value, ['application/pdf', 'application/msword']),
45            lambda value: validate_file_size(value, 10),  # 10MB
46        ]
47    )
48    image = models.ImageField(
49        upload_to='images/',
50        validators=[
51            lambda value: validate_file_size(value, 5),  # 5MB
52            lambda value: validate_image_dimensions(value, 800, 600),  # 800x600px
53        ]
54    )

Обработка множественной загрузки

Загрузка нескольких файлов одновременно:

 1from django import forms
 2from django.core.files.uploadedfile import UploadedFile
 3
 4class MultipleFileForm(forms.Form):
 5    files = forms.FileField(
 6        widget=forms.ClearableFileInput(attrs={'multiple': True}),
 7        label='Выберите файлы',
 8        help_text='Можно выбрать несколько файлов'
 9    )
10
11def handle_multiple_files(request):
12    if request.method == 'POST':
13        form = MultipleFileForm(request.POST, request.FILES)
14        if form.is_valid():
15            files = request.FILES.getlist('files')
16
17            for uploaded_file in files:
18                # Обрабатываем каждый файл
19                if uploaded_file.size > 0:
20                    # Сохраняем файл
21                    file_path = default_storage.save(
22                        f'uploads/{uploaded_file.name}',
23                        uploaded_file
24                    )
25
26                    # Создаем запись в базе данных
27                    FileRecord.objects.create(
28                        name=uploaded_file.name,
29                        file=file_path,
30                        size=uploaded_file.size,
31                        content_type=uploaded_file.content_type
32                    )
33
34            messages.success(request, f'Успешно загружено {len(files)} файлов')
35            return redirect('file_list')
36    else:
37        form = MultipleFileForm()
38
39    return render(request, 'upload_multiple.html', {'form': form})

Асинхронная загрузка с AJAX

JavaScript для загрузки файлов без перезагрузки страницы:

 1// upload.js
 2document.addEventListener('DOMContentLoaded', function() {
 3    const uploadForm = document.getElementById('upload-form');
 4    const fileInput = document.getElementById('file-input');
 5    const progressBar = document.getElementById('progress-bar');
 6    const statusDiv = document.getElementById('status');
 7
 8    uploadForm.addEventListener('submit', function(e) {
 9        e.preventDefault();
10
11        const formData = new FormData();
12        const files = fileInput.files;
13
14        for (let i = 0; i < files.length; i++) {
15            formData.append('files', files[i]);
16        }
17
18        // Добавляем CSRF токен
19        const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
20        formData.append('csrfmiddlewaretoken', csrfToken);
21
22        // Отправляем файлы
23        const xhr = new XMLHttpRequest();
24
25        xhr.upload.addEventListener('progress', function(e) {
26            if (e.lengthComputable) {
27                const percentComplete = (e.loaded / e.total) * 100;
28                progressBar.style.width = percentComplete + '%';
29                progressBar.textContent = Math.round(percentComplete) + '%';
30            }
31        });
32
33        xhr.addEventListener('load', function() {
34            if (xhr.status === 200) {
35                const response = JSON.parse(xhr.responseText);
36                statusDiv.innerHTML = `<div class="alert alert-success">${response.message}</div>`;
37                progressBar.style.width = '0%';
38                progressBar.textContent = '0%';
39            } else {
40                statusDiv.innerHTML = '<div class="alert alert-danger">Ошибка загрузки</div>';
41            }
42        });
43
44        xhr.addEventListener('error', function() {
45            statusDiv.innerHTML = '<div class="alert alert-danger">Ошибка сети</div>';
46        });
47
48        xhr.open('POST', '/upload/');
49        xhr.send(formData);
50    });
51});

Безопасность загрузки файлов

Важные меры безопасности:

 1import os
 2from django.core.exceptions import ValidationError
 3from django.core.files.uploadedfile import UploadedFile
 4
 5def secure_file_upload(uploaded_file: UploadedFile):
 6    """Безопасная загрузка файлов"""
 7
 8    # Проверяем расширение файла
 9    file_name = uploaded_file.name
10    file_ext = os.path.splitext(file_name)[1].lower()
11
12    # Список разрешенных расширений
13    allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx']
14
15    if file_ext not in allowed_extensions:
16        raise ValidationError(f'Расширение {file_ext} не разрешено')
17
18    # Проверяем MIME тип
19    allowed_mime_types = [
20        'image/jpeg', 'image/png', 'image/gif',
21        'application/pdf', 'application/msword',
22        'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
23    ]
24
25    if uploaded_file.content_type not in allowed_mime_types:
26        raise ValidationError('Неподдерживаемый тип файла')
27
28    # Проверяем размер файла
29    max_size = 50 * 1024 * 1024  # 50MB
30    if uploaded_file.size > max_size:
31        raise ValidationError('Файл слишком большой')
32
33    # Генерируем безопасное имя файла
34    import uuid
35    safe_name = f"{uuid.uuid4()}{file_ext}"
36
37    return safe_name
38
39# Использование в модели
40class SecureDocument(models.Model):
41    original_name = models.CharField(max_length=255)
42    file = models.FileField(upload_to='secure/')
43    uploaded_at = models.DateTimeField(auto_now_add=True)
44
45    def save(self, *args, **kwargs):
46        if self.file:
47            # Генерируем безопасное имя
48            safe_name = secure_file_upload(self.file)
49            self.file.name = safe_name
50        super().save(*args, **kwargs)

Лучшие практики

  • Всегда валидируй файлы - проверяй тип, размер и содержимое
  • Используй безопасные имена - генерируй уникальные имена файлов
  • Ограничивай размер файлов - настройте максимальный размер загрузки
  • Проверяй MIME типы - не доверяйте расширениям файлов
  • Используй CDN для статики - для быстрой доставки файлов
  • Создавай резервные копии - важные файлы должны дублироваться
  • Логируй загрузки - отслеживайте все операции с файлами
  • Тестируй загрузку - проверяйте функциональность на разных устройствах

Тестирование загрузки файлов

Создание тестов для проверки функциональности:

 1from django.test import TestCase, Client
 2from django.core.files.uploadedfile import SimpleUploadedFile
 3from django.contrib.auth.models import User
 4from django.urls import reverse
 5import tempfile
 6import os
 7from .models import Book
 8
 9class FileUploadTestCase(TestCase):
10    def setUp(self):
11        self.client = Client()
12        self.user = User.objects.create_user(
13            username='testuser',
14            password='testpass123'
15        )
16        self.client.login(username='testuser', password='testpass123')
17
18        # Создаем временный файл для тестирования
19        self.temp_file = tempfile.NamedTemporaryFile(delete=False)
20        self.temp_file.write(b'Test PDF content')
21        self.temp_file.close()
22
23    def tearDown(self):
24        # Удаляем временный файл
25        os.unlink(self.temp_file.name)
26
27    def test_file_upload(self):
28        with open(self.temp_file.name, 'rb') as f:
29            response = self.client.post(reverse('upload_book'), {
30                'title': 'Test Book',
31                'description': 'Test Description',
32                'pdf_file': SimpleUploadedFile(
33                    'test.pdf',
34                    f.read(),
35                    content_type='application/pdf'
36                )
37            })
38
39        self.assertEqual(response.status_code, 302)  # Redirect after success
40        self.assertTrue(Book.objects.filter(title='Test Book').exists())
41
42    def test_invalid_file_type(self):
43        with open(self.temp_file.name, 'rb') as f:
44            response = self.client.post(reverse('upload_book'), {
45                'title': 'Test Book',
46                'description': 'Test Description',
47                'pdf_file': SimpleUploadedFile(
48                    'test.txt',
49                    f.read(),
50                    content_type='text/plain'
51                )
52            })
53
54        self.assertEqual(response.status_code, 200)  # Form errors
55        self.assertFalse(Book.objects.filter(title='Test Book').exists())

FAQ

Q: Как валидировать загруженные файлы?
A: Используй валидаторы в полях модели или в формах через clean методы. Проверяй тип, размер и содержимое файлов.

Q: Как ограничить размер загружаемых файлов?
A: Настрой DATA_UPLOAD_MAX_MEMORY_SIZE в settings.py и добавь валидаторы размера в формы.

Q: Можно ли загружать несколько файлов одновременно?
A: Да, используй атрибут multiple в input и обрабатывай request.FILES.getlist().

Q: Как безопасно хранить имена файлов?
A: Генерируй уникальные имена с помощью UUID, проверяй расширения и MIME типы, не доверяй пользовательскому вводу.

Q: Как настроить загрузку для production?
A: Используй CDN для статических файлов, настройте бэкапы, ограничьте типы и размеры файлов, логируйте все операции.

Q: Как обрабатывать ошибки загрузки?
A: Используй try-except блоки, валидируй файлы перед сохранением, показывай понятные сообщения об ошибках пользователю.