Загрузка файлов в 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 блоки, валидируй файлы перед сохранением, показывай понятные сообщения об ошибках пользователю.