Django + Amazon S3

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

Установка и настройка

1# Установка необходимых пакетов
2pip install django-storages
3pip install boto3
4pip install botocore
5
6# Для дополнительной функциональности
7pip install django-s3-storage
8pip install django-s3direct
 1# settings.py
 2import os
 3from django.conf import settings
 4
 5# AWS S3 настройки
 6AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID', 'your-access-key')
 7AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY', 'your-secret-key')
 8AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME', 'your-bucket-name')
 9AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1')
10AWS_S3_CUSTOM_DOMAIN = os.environ.get('AWS_S3_CUSTOM_DOMAIN', None)
11
12# Дополнительные настройки S3
13AWS_S3_OBJECT_PARAMETERS = {
14    'CacheControl': 'max-age=86400',  # 24 часа кэширования
15}
16AWS_S3_FILE_OVERWRITE = False  # Не перезаписывать файлы с одинаковыми именами
17AWS_DEFAULT_ACL = 'public-read'  # Публичный доступ для статических файлов
18AWS_QUERYSTRING_AUTH = False  # Не добавлять аутентификацию в URL
19AWS_S3_SIGNATURE_VERSION = 's3v4'  # Использовать новую версию подписи
20
21# Настройки для медиа файлов
22AWS_S3_MEDIA_BUCKET_NAME = os.environ.get('AWS_S3_MEDIA_BUCKET_NAME', AWS_STORAGE_BUCKET_NAME)
23AWS_S3_MEDIA_CUSTOM_DOMAIN = os.environ.get('AWS_S3_MEDIA_CUSTOM_DOMAIN', None)
24
25# Настройки для статических файлов
26AWS_S3_STATIC_BUCKET_NAME = os.environ.get('AWS_S3_STATIC_BUCKET_NAME', AWS_STORAGE_BUCKET_NAME)
27AWS_S3_STATIC_CUSTOM_DOMAIN = os.environ.get('AWS_S3_STATIC_CUSTOM_DOMAIN', None)
28
29# Настройки хранилищ
30if not settings.DEBUG:
31    # Продакшн: использовать S3
32    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
33    STATICFILES_STORAGE = 'storages.backends.s3boto3.StaticS3Boto3Storage'
34
35    # URL для медиа и статических файлов
36    if AWS_S3_CUSTOM_DOMAIN:
37        MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'
38        STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
39    else:
40        MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/media/'
41        STATIC_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/static/'
42else:
43    # Разработка: использовать локальные файлы
44    MEDIA_URL = '/media/'
45    STATIC_URL = '/static/'
46
47# Настройки для django-storages
48STORAGES = {
49    "default": {
50        "BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
51        "OPTIONS": {
52            "bucket_name": AWS_STORAGE_BUCKET_NAME,
53            "access_key": AWS_ACCESS_KEY_ID,
54            "secret_key": AWS_SECRET_ACCESS_KEY,
55            "region_name": AWS_S3_REGION_NAME,
56            "file_overwrite": False,
57            "default_acl": "public-read",
58            "querystring_auth": False,
59            "signature_version": "s3v4",
60        }
61    },
62    "staticfiles": {
63        "BACKEND": "storages.backends.s3boto3.StaticS3Boto3Storage",
64        "OPTIONS": {
65            "bucket_name": AWS_S3_STATIC_BUCKET_NAME,
66            "access_key": AWS_ACCESS_KEY_ID,
67            "secret_key": AWS_SECRET_ACCESS_KEY,
68            "region_name": AWS_S3_REGION_NAME,
69            "file_overwrite": True,  # Статические файлы можно перезаписывать
70            "default_acl": "public-read",
71            "querystring_auth": False,
72            "signature_version": "s3v4",
73        }
74    }
75}

Создание кастомных хранилищ

 1# your_app/storage.py
 2from storages.backends.s3boto3 import S3Boto3Storage
 3from django.conf import settings
 4
 5class MediaStorage(S3Boto3Storage):
 6    """Хранилище для медиа файлов"""
 7    location = 'media'
 8    file_overwrite = False
 9    default_acl = 'public-read'
10    querystring_auth = False
11
12    def __init__(self, *args, **kwargs):
13        super().__init__(*args, **kwargs)
14        self.bucket_name = getattr(settings, 'AWS_S3_MEDIA_BUCKET_NAME', settings.AWS_STORAGE_BUCKET_NAME)
15        if hasattr(settings, 'AWS_S3_MEDIA_CUSTOM_DOMAIN'):
16            self.custom_domain = settings.AWS_S3_MEDIA_CUSTOM_DOMAIN
17
18class StaticStorage(S3Boto3Storage):
19    """Хранилище для статических файлов"""
20    location = 'static'
21    file_overwrite = True
22    default_acl = 'public-read'
23    querystring_auth = False
24
25    def __init__(self, *args, **kwargs):
26        super().__init__(*args, **kwargs)
27        self.bucket_name = getattr(settings, 'AWS_S3_STATIC_BUCKET_NAME', settings.AWS_STORAGE_BUCKET_NAME)
28        if hasattr(settings, 'AWS_S3_STATIC_CUSTOM_DOMAIN'):
29            self.custom_domain = settings.AWS_S3_STATIC_CUSTOM_DOMAIN
30
31class PrivateStorage(S3Boto3Storage):
32    """Приватное хранилище для конфиденциальных файлов"""
33    location = 'private'
34    file_overwrite = False
35    default_acl = 'private'
36    querystring_auth = True  # Требует аутентификацию для доступа
37
38    def url(self, name, parameters=None, expire=None, http_method=None):
39        """Генерирует временный URL для приватных файлов"""
40        if expire is None:
41            expire = 3600  # 1 час по умолчанию
42        return super().url(name, parameters, expire, http_method)
43
44# Обновляем settings.py
45if not settings.DEBUG:
46    DEFAULT_FILE_STORAGE = 'your_app.storage.MediaStorage'
47    STATICFILES_STORAGE = 'your_app.storage.StaticStorage'
48
49    # Для приватных файлов
50    PRIVATE_FILE_STORAGE = 'your_app.storage.PrivateStorage'

Настройка CloudFront CDN

 1# settings.py
 2# CloudFront настройки
 3AWS_S3_CUSTOM_DOMAIN = 'your-cloudfront-domain.cloudfront.net'
 4AWS_CLOUDFRONT_DOMAIN = 'your-cloudfront-domain.cloudfront.net'
 5
 6# Настройки для разных типов файлов
 7AWS_S3_STATIC_CUSTOM_DOMAIN = 'static.yourdomain.com'
 8AWS_S3_MEDIA_CUSTOM_DOMAIN = 'media.yourdomain.com'
 9
10# Кэширование для CloudFront
11AWS_S3_OBJECT_PARAMETERS = {
12    'CacheControl': 'max-age=31536000',  # 1 год для статических файлов
13}
14
15# Настройки для медиа файлов через CloudFront
16AWS_S3_MEDIA_OBJECT_PARAMETERS = {
17    'CacheControl': 'max-age=86400',  # 1 день для медиа файлов
18}
19
20# Обновляем URL
21if AWS_S3_CUSTOM_DOMAIN:
22    MEDIA_URL = f'https://{AWS_S3_MEDIA_CUSTOM_DOMAIN}/'
23    STATIC_URL = f'https://{AWS_S3_STATIC_CUSTOM_DOMAIN}/'
24else:
25    MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/media/'
26    STATIC_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/static/'

Модели с файлами

 1# your_app/models.py
 2from django.db import models
 3from django.conf import settings
 4from django.core.files.storage import default_storage
 5
 6class Document(models.Model):
 7    title = models.CharField(max_length=200)
 8    file = models.FileField(
 9        upload_to='documents/%Y/%m/%d/',
10        storage=default_storage,
11        help_text='Загрузите документ'
12    )
13    uploaded_at = models.DateTimeField(auto_now_add=True)
14    file_size = models.PositiveIntegerField(editable=False)
15
16    def save(self, *args, **kwargs):
17        if self.file:
18            self.file_size = self.file.size
19        super().save(*args, **kwargs)
20
21    def get_file_url(self):
22        """Получить URL файла"""
23        if self.file:
24            return self.file.url
25        return None
26
27    def get_file_name(self):
28        """Получить имя файла"""
29        if self.file:
30            return self.file.name.split('/')[-1]
31        return None
32
33class Image(models.Model):
34    title = models.CharField(max_length=200)
35    image = models.ImageField(
36        upload_to='images/%Y/%m/%d/',
37        storage=default_storage,
38        help_text='Загрузите изображение'
39    )
40    thumbnail = models.ImageField(
41        upload_to='thumbnails/%Y/%m/%d/',
42        storage=default_storage,
43        blank=True,
44        null=True
45    )
46    uploaded_at = models.DateTimeField(auto_now_add=True)
47
48    def save(self, *args, **kwargs):
49        super().save(*args, **kwargs)
50        if self.image and not self.thumbnail:
51            self.create_thumbnail()
52
53    def create_thumbnail(self):
54        """Создать миниатюру изображения"""
55        from PIL import Image as PILImage
56        from io import BytesIO
57        from django.core.files import File
58
59        if self.image:
60            img = PILImage.open(self.image)
61            img.thumbnail((300, 300), PILImage.Resampling.LANCZOS)
62
63            thumb_io = BytesIO()
64            img.save(thumb_io, format=img.format or 'JPEG', quality=85)
65
66            self.thumbnail.save(
67                f'thumb_{self.image.name.split("/")[-1]}',
68                File(thumb_io),
69                save=False
70            )
71
72class PrivateDocument(models.Model):
73    """Приватный документ с временным доступом"""
74    title = models.CharField(max_length=200)
75    file = models.FileField(
76        upload_to='private/%Y/%m/%d/',
77        storage=settings.PRIVATE_FILE_STORAGE,
78        help_text='Приватный документ'
79    )
80    uploaded_at = models.DateTimeField(auto_now_add=True)
81    expires_at = models.DateTimeField(help_text='Дата истечения доступа')
82
83    def get_temporary_url(self, expire_hours=1):
84        """Получить временный URL для доступа к файлу"""
85        from datetime import timedelta
86        from django.utils import timezone
87
88        if timezone.now() > self.expires_at:
89            return None
90
91        expire_seconds = expire_hours * 3600
92        return self.file.storage.url(self.file.name, expire=expire_seconds)

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

 1# your_app/forms.py
 2from django import forms
 3from django.core.files.uploadedfile import UploadedFile
 4from .models import Document, Image, PrivateDocument
 5
 6class DocumentUploadForm(forms.ModelForm):
 7    class Meta:
 8        model = Document
 9        fields = ['title', 'file']
10
11    def clean_file(self):
12        file = self.cleaned_data.get('file')
13        if file:
14            # Проверяем размер файла (50MB максимум)
15            if file.size > 50 * 1024 * 1024:
16                raise forms.ValidationError('Файл слишком большой. Максимальный размер: 50MB.')
17
18            # Проверяем расширение файла
19            allowed_extensions = ['.pdf', '.doc', '.docx', '.txt', '.rtf']
20            file_extension = file.name.lower()[-4:]
21            if file_extension not in allowed_extensions:
22                raise forms.ValidationError(
23                    f'Неподдерживаемый тип файла. Разрешены: {", ".join(allowed_extensions)}'
24                )
25
26            # Проверяем MIME тип
27            if hasattr(file, 'content_type'):
28                allowed_mimes = [
29                    'application/pdf',
30                    'application/msword',
31                    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
32                    'text/plain',
33                    'application/rtf'
34                ]
35                if file.content_type not in allowed_mimes:
36                    raise forms.ValidationError('Неподдерживаемый MIME тип файла.')
37
38        return file
39
40class ImageUploadForm(forms.ModelForm):
41    class Meta:
42        model = Image
43        fields = ['title', 'image']
44
45    def clean_image(self):
46        image = self.cleaned_data.get('image')
47        if image:
48            # Проверяем размер файла (10MB максимум)
49            if image.size > 10 * 1024 * 1024:
50                raise forms.ValidationError('Изображение слишком большое. Максимальный размер: 10MB.')
51
52            # Проверяем формат изображения
53            from PIL import Image as PILImage
54            try:
55                img = PILImage.open(image)
56                img.verify()
57            except Exception:
58                raise forms.ValidationError('Неподдерживаемый формат изображения.')
59
60        return image
61
62class PrivateDocumentForm(forms.ModelForm):
63    class Meta:
64        model = PrivateDocument
65        fields = ['title', 'file', 'expires_at']
66        widgets = {
67            'expires_at': forms.DateTimeInput(
68                attrs={'type': 'datetime-local'},
69                format='%Y-%m-%dT%H:%M'
70            )
71        }
72
73    def clean_expires_at(self):
74        expires_at = self.cleaned_data.get('expires_at')
75        from django.utils import timezone
76
77        if expires_at and expires_at <= timezone.now():
78            raise forms.ValidationError('Дата истечения должна быть в будущем.')
79
80        return expires_at

Views для работы с файлами

  1# your_app/views.py
  2from django.shortcuts import render, get_object_or_404, redirect
  3from django.contrib.auth.decorators import login_required
  4from django.contrib import messages
  5from django.http import HttpResponse, Http404
  6from django.core.files.storage import default_storage
  7from django.conf import settings
  8from .models import Document, Image, PrivateDocument
  9from .forms import DocumentUploadForm, ImageUploadForm, PrivateDocumentForm
 10
 11@login_required
 12def upload_document(request):
 13    if request.method == 'POST':
 14        form = DocumentUploadForm(request.POST, request.FILES)
 15        if form.is_valid():
 16            document = form.save()
 17            messages.success(request, f'Документ "{document.title}" успешно загружен.')
 18            return redirect('document_detail', pk=document.pk)
 19    else:
 20        form = DocumentUploadForm()
 21
 22    return render(request, 'upload_document.html', {'form': form})
 23
 24@login_required
 25def upload_image(request):
 26    if request.method == 'POST':
 27        form = ImageUploadForm(request.POST, request.FILES)
 28        if form.is_valid():
 29            image = form.save()
 30            messages.success(request, f'Изображение "{image.title}" успешно загружено.')
 31            return redirect('image_detail', pk=image.pk)
 32    else:
 33        form = ImageUploadForm()
 34
 35    return render(request, 'upload_image.html', {'form': form})
 36
 37@login_required
 38def upload_private_document(request):
 39    if request.method == 'POST':
 40        form = PrivateDocumentForm(request.POST, request.FILES)
 41        if form.is_valid():
 42            document = form.save()
 43            messages.success(request, f'Приватный документ "{document.title}" успешно загружен.')
 44            return redirect('private_document_detail', pk=document.pk)
 45    else:
 46        form = PrivateDocumentForm()
 47
 48    return render(request, 'upload_private_document.html', {'form': form})
 49
 50def download_document(request, pk):
 51    document = get_object_or_404(Document, pk=pk)
 52
 53    if document.file:
 54        # Для приватных файлов проверяем права доступа
 55        if hasattr(document, 'private_document'):
 56            if not request.user.is_authenticated:
 57                raise Http404
 58
 59            # Генерируем временный URL
 60            download_url = document.private_document.get_temporary_url()
 61            if download_url:
 62                return redirect(download_url)
 63            else:
 64                raise Http404("Срок действия ссылки истек")
 65        else:
 66            # Публичный файл
 67            return redirect(document.file.url)
 68
 69    raise Http404("Файл не найден")
 70
 71def delete_file(request, pk):
 72    if not request.user.is_staff:
 73        raise Http404
 74
 75    document = get_object_or_404(Document, pk=pk)
 76
 77    if request.method == 'POST':
 78        # Удаляем файл из S3
 79        if document.file:
 80            default_storage.delete(document.file.name)
 81
 82        # Удаляем запись из БД
 83        document.delete()
 84        messages.success(request, 'Файл успешно удален.')
 85        return redirect('document_list')
 86
 87    return render(request, 'confirm_delete.html', {'document': document})
 88
 89# API view для получения информации о файле
 90from django.http import JsonResponse
 91from django.views.decorators.csrf import csrf_exempt
 92import json
 93
 94@csrf_exempt
 95def file_info(request, pk):
 96    if request.method == 'GET':
 97        try:
 98            document = Document.objects.get(pk=pk)
 99            info = {
100                'id': document.pk,
101                'title': document.title,
102                'file_name': document.get_file_name(),
103                'file_size': document.file_size,
104                'file_url': document.get_file_url(),
105                'uploaded_at': document.uploaded_at.isoformat(),
106            }
107            return JsonResponse(info)
108        except Document.DoesNotExist:
109            return JsonResponse({'error': 'Документ не найден'}, status=404)
110
111    return JsonResponse({'error': 'Метод не поддерживается'}, status=405)

HTML шаблоны

 1# templates/upload_document.html
 2{% extends 'base.html' %}
 3{% load static %}
 4
 5{% block content %}
 6<div class="container mt-4">
 7    <h2>Загрузка документа</h2>
 8
 9    <div class="card">
10        <div class="card-body">
11            <form method="post" enctype="multipart/form-data">
12                {% csrf_token %}
13
14                <div class="mb-3">
15                    <label for="{{ form.title.id_for_label }}" class="form-label">Название документа</label>
16                    {{ form.title }}
17                    {% if form.title.errors %}
18                        <div class="alert alert-danger">
19                            {{ form.title.errors }}
20                        </div>
21                    {% endif %}
22                </div>
23
24                <div class="mb-3">
25                    <label for="{{ form.file.id_for_label }}" class="form-label">Файл</label>
26                    {{ form.file }}
27                    {% if form.file.errors %}
28                        <div class="alert alert-danger">
29                            {{ form.file.errors }}
30                        </div>
31                    {% endif %}
32                    <div class="form-text">
33                        Поддерживаемые форматы: PDF, DOC, DOCX, TXT, RTF. Максимальный размер: 50MB.
34                    </div>
35                </div>
36
37                <button type="submit" class="btn btn-primary">Загрузить документ</button>
38                <a href="{% url 'document_list' %}" class="btn btn-secondary">Отмена</a>
39            </form>
40        </div>
41    </div>
42</div>
43{% endblock %}
44
45# templates/document_list.html
46{% extends 'base.html' %}
47{% load static %}
48
49{% block content %}
50<div class="container mt-4">
51    <div class="d-flex justify-content-between align-items-center mb-4">
52        <h2>Документы</h2>
53        <a href="{% url 'upload_document' %}" class="btn btn-primary">Загрузить документ</a>
54    </div>
55
56    {% if documents %}
57        <div class="row">
58            {% for document in documents %}
59                <div class="col-md-6 col-lg-4 mb-3">
60                    <div class="card h-100">
61                        <div class="card-body">
62                            <h5 class="card-title">{{ document.title }}</h5>
63                            <p class="card-text">
64                                <small class="text-muted">
65                                    Размер: {{ document.file_size|filesizeformat }}<br>
66                                    Загружен: {{ document.uploaded_at|date:"d.m.Y H:i" }}
67                                </small>
68                            </p>
69                            <div class="btn-group w-100">
70                                <a href="{% url 'download_document' document.pk %}"
71                                   class="btn btn-outline-primary btn-sm">Скачать</a>
72                                {% if user.is_staff %}
73                                    <a href="{% url 'delete_file' document.pk %}"
74                                       class="btn btn-outline-danger btn-sm">Удалить</a>
75                                {% endif %}
76                            </div>
77                        </div>
78                    </div>
79                </div>
80            {% endfor %}
81        </div>
82    {% else %}
83        <div class="alert alert-info">
84            Документы не найдены. <a href="{% url 'upload_document' %}">Загрузить первый документ</a>
85        </div>
86    {% endif %}
87</div>
88{% endblock %}

Команды управления

 1# your_app/management/commands/sync_s3.py
 2from django.core.management.base import BaseCommand
 3from django.core.files.storage import default_storage
 4from django.conf import settings
 5import os
 6
 7class Command(BaseCommand):
 8    help = 'Синхронизация локальных файлов с S3'
 9
10    def add_arguments(self, parser):
11        parser.add_argument(
12            '--upload',
13            action='store_true',
14            help='Загрузить локальные файлы в S3',
15        )
16        parser.add_argument(
17            '--download',
18            action='store_true',
19            help='Скачать файлы из S3 локально',
20        )
21        parser.add_argument(
22            '--path',
23            type=str,
24            help='Путь к директории для синхронизации',
25        )
26
27    def handle(self, *args, **options):
28        if options['upload']:
29            self.upload_to_s3(options['path'])
30        elif options['download']:
31            self.download_from_s3(options['path'])
32        else:
33            self.stdout.write(
34                self.style.ERROR('Укажите --upload или --download')
35            )
36
37    def upload_to_s3(self, path):
38        if not path:
39            path = settings.MEDIA_ROOT
40
41        self.stdout.write(f'Загрузка файлов из {path} в S3...')
42
43        for root, dirs, files in os.walk(path):
44            for file in files:
45                local_path = os.path.join(root, file)
46                relative_path = os.path.relpath(local_path, path)
47
48                with open(local_path, 'rb') as f:
49                    default_storage.save(relative_path, f)
50
51                self.stdout.write(f'Загружен: {relative_path}')
52
53        self.stdout.write(
54            self.style.SUCCESS('Загрузка завершена')
55        )
56
57    def download_from_s3(self, path):
58        if not path:
59            path = settings.MEDIA_ROOT
60
61        self.stdout.write(f'Скачивание файлов из S3 в {path}...')
62
63        # Получаем список файлов из S3
64        files = default_storage.listdir('')
65
66        for file in files[1]:  # files[1] содержит файлы
67            local_path = os.path.join(path, file)
68            os.makedirs(os.path.dirname(local_path), exist_ok=True)
69
70            with default_storage.open(file, 'rb') as s3_file:
71                with open(local_path, 'wb') as local_file:
72                    local_file.write(s3_file.read())
73
74            self.stdout.write(f'Скачан: {file}')
75
76        self.stdout.write(
77            self.style.SUCCESS('Скачивание завершено')
78        )

Тестирование S3 интеграции

 1# your_app/tests/test_s3.py
 2from django.test import TestCase, override_settings
 3from django.core.files.storage import default_storage
 4from django.core.files.base import ContentFile
 5from django.conf import settings
 6from .models import Document, Image
 7import tempfile
 8import os
 9
10@override_settings(
11    DEFAULT_FILE_STORAGE='django.core.files.storage.FileSystemStorage',
12    MEDIA_ROOT=tempfile.mkdtemp()
13)
14class S3StorageTest(TestCase):
15    def setUp(self):
16        self.test_file_content = b'Test file content'
17        self.test_file_name = 'test_file.txt'
18
19    def test_file_upload(self):
20        """Тест загрузки файла"""
21        # Создаем тестовый файл
22        test_file = ContentFile(self.test_file_content, self.test_file_name)
23
24        # Сохраняем файл
25        saved_name = default_storage.save(self.test_file_name, test_file)
26
27        # Проверяем, что файл сохранен
28        self.assertTrue(default_storage.exists(saved_name))
29
30        # Проверяем содержимое файла
31        with default_storage.open(saved_name, 'rb') as f:
32            content = f.read()
33            self.assertEqual(content, self.test_file_content)
34
35    def test_file_delete(self):
36        """Тест удаления файла"""
37        # Создаем и сохраняем файл
38        test_file = ContentFile(self.test_file_content, self.test_file_name)
39        saved_name = default_storage.save(self.test_file_name, test_file)
40
41        # Удаляем файл
42        default_storage.delete(saved_name)
43
44        # Проверяем, что файл удален
45        self.assertFalse(default_storage.exists(saved_name))
46
47    def test_document_model(self):
48        """Тест модели Document с файлом"""
49        # Создаем документ
50        document = Document.objects.create(
51            title='Test Document',
52            file=ContentFile(self.test_file_content, self.test_file_name)
53        )
54
55        # Проверяем, что файл сохранен
56        self.assertTrue(document.file)
57        self.assertEqual(document.file_size, len(self.test_file_content))
58
59        # Проверяем методы модели
60        self.assertIsNotNone(document.get_file_url())
61        self.assertEqual(document.get_file_name(), self.test_file_name)
62
63    def tearDown(self):
64        # Очищаем временные файлы
65        import shutil
66        shutil.rmtree(settings.MEDIA_ROOT)
67
68# Тест для продакшн настроек
69@override_settings(
70    DEFAULT_FILE_STORAGE='storages.backends.s3boto3.S3Boto3Storage',
71    AWS_ACCESS_KEY_ID='test-key',
72    AWS_SECRET_ACCESS_KEY='test-secret',
73    AWS_STORAGE_BUCKET_NAME='test-bucket',
74    AWS_S3_REGION_NAME='us-east-1'
75)
76class S3ProductionTest(TestCase):
77    def test_s3_storage_backend(self):
78        """Тест, что используется S3 backend"""
79        from storages.backends.s3boto3 import S3Boto3Storage
80        self.assertIsInstance(default_storage, S3Boto3Storage)

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

  • Используй переменные окружения для хранения AWS ключей
  • Настрой CloudFront CDN для улучшения производительности
  • Используй разные bucket'и для статических и медиа файлов
  • Настрой CORS для S3 bucket если нужно
  • Используй IAM роли вместо access keys в продакшне
  • Настрой lifecycle policies для автоматического удаления старых файлов
  • Мониторь использование S3 через CloudWatch
  • Используй версионирование для важных файлов
  • Настрой backup стратегию для критических данных
  • Тестируй интеграцию в различных окружениях

FAQ

Q: Как настроить CDN для S3?
A: Используй CloudFront и укажи его URL в MEDIA_URL и STATIC_URL. Настрой origin для S3 bucket и кэширование.

Q: Как обеспечить безопасность файлов в S3?
A: Используй IAM роли, настрой bucket policies, применяй шифрование, используй приватные bucket'ы для конфиденциальных данных.

Q: Как оптимизировать стоимость S3?
A: Используй правильные классы хранения, настрой lifecycle policies, применяйте сжатие, используй CloudFront для кэширования.

Q: Как мигрировать существующие файлы в S3?
A: Используй django-storages команды, AWS CLI, или создай кастомную команду для синхронизации.

Q: Как обрабатывать ошибки загрузки в S3?
A: Используй try-catch блоки, логируй ошибки, настрой retry логику, проверяй права доступа.

Q: Как настроить CORS для S3?
A: Добавь CORS конфигурацию в S3 bucket settings, укажи разрешенные домены и методы.