Django + Amazon S3
S3 позволяет хранить статические файлы и медиа в облаке для лучшей производительности, масштабируемости и надежности. Это особенно важно для проектов с большими объемами файлов или высокой нагрузкой.
Установка и настройка
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, укажи разрешенные домены и методы.