İçeriğe geç →

Django AWS S3 Büyük Dosya Yükleme

Büyük dosyaları yüklemek ve yönetmek zor bir iş. Bu yükü omuzlamayı tercih edebilir ve statik dosya yönetimini kendiniz geliştirebilirsiniz fakat emin olun ki bu hiç de kolay olmayacaktır. Amazon Web Services servisi olan S3 tam da burada imdadınıza koşuyor. Django AWS S3 entegrasyonuyla statik dosyaları cüzi ve öngörülebilir fiyatlarla teoride sonsuz boyuttaki bir diskte saklıyorsunuz.

Nedir Bu S3?

Amazon S3

Amazon S3 (Simple Storage Service) isminden de anlaşılacağı üzere bir depolama servisidir. Udemy, Netflix, Dropbox gibi firmaların referansını almış güçlü bir altyapı olduğunu düşünecek olursak oldukça kullanışlı hızlı ve güçlü. CloudFront servisiyle performansı daha da ötelere taşıyabiliriz ama üzülerek söylüyorum: Şimdilik onu anlatmayacağız.

Siz statik dosyaları S3 üzerine yüklüyorsunuz, hem güvenlik hem de hız anlamında kaygınız kalmıyor diyebilirim. Hazırsanız S3 Django nasıl entegre edilir, görelim.

Django ve S3

Tahmin edeceğiniz üzere S3, Django uygulamanızda media depolama ve statik dosya yönetiminde size yardımcı oluyor. Django modellerinizdeki FileField alanlarını doğrudan S3 ile entegre edebiliyorsunuz. Bunun için benim şahsi olarak kullandığım eklenti django-storages. Bu eklentiyle kullanıcınızın form elementi olarak gönderdiği dosya içeriği doğrudan S3 üzerine taşınıyor. Size standart FileField fonksiyonlarıyla dosyayı yönetmek kalıyor.

Süreç ise şöyle işliyor. DRF(Django Rest Framework) kullanıyorsanız endpoint değilse form action’da belirtilen view dosyayı karşılıyor. Eğer dosya 2.5 mb değerinden küçükse hafıza üzerinden, değilse temp klasörüne yazılmış binary halinden S3 üzerine yükleniyor.

Dosya Boyutu Büyükse?…

Burada işin rengi biraz değişiyor. Büyük bir dosya yüklemek için uçbirimlerinizi açmak maliyetli sonuçlar üretir. Multipart upload gibi yöntemleri kullanacağınız için de hataya açık bir mimari kurmuş oluyorsunuz. Bir diğer sorun ise kaynaklarınızı optimize kullanmamış oluyorsunuz. Çünkü söz konusu büyük dosya önce sizin sunucularınıza yüklenecek sonrasındaysa S3 üzerine taşınacak.

S3 üzerine doğrudan dosya göndermek için bir post policy oluşturmalı ve imzalamalıyız. Böylelikle kullanıcı tarafından yüklenecek içeriğin geçici bir post policy aracılığıyla yüklenmesini sağlar ve sunucumuzun olmadığı bir mimariyle statik dosyaları teslim alabiliriz.

Öncelikle drf ve boto3 yüklü olmalıdır. Bunun için aşağıdaki komutları çalıştıralım.


pip install django
pip install djangorestframework
pip install boto3
pip install django-environ

Mesela bir video paylaşım uygulaması yapıyor olalım. Kullanıcıdan alınan bilgiyle bir yükleme yapacağız ve yüklenen video doğrudan S3 üzerine yüklenecek. Aşağıdaki komutları çalıştırın.


python -m django startproject Tutorial
cd Tutorial
python manage.py startapp video

Django uygulamamızı oluşturduk. video klasörünün altına urls.py dosyası ekleyelim ve settings.py dosyasında INSTALLED_APPS aşağıdaki gibi olmalı.


..........
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'video'
]
........

S3 Bucket Konfigürasyonları

https://aws.amazon.com/tr/ adresine gidin ve hesap oluşturun. Bu hesabımızda bir s3 bucket oluşturacağız ve programatic access için yetkilendirmeler yapacağız.

  • Hesabınıza giriş yapın
  • Services altındaki S3 servisine gidin
  • Create Bucket ‘a tıkladıktan sonra bucket-name ve region için karar verebilirsiniz. Bu aşamada region ayarınızı not almalısınız.
  • Oluşturduğunuz bucket’a girin.
  • sonrasında Permissions altından CORS Configuration a tıklayın. Aşağıdaki kodu yapıştırarak cross-origin poliçesini belirlemiş olacaksınız.
<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

AWS Konfigürasyonları

  • Services altından IAM servisine gitmelisiniz. Bu servisle AWS servislerinizin erişim poliçelerini belirlersiniz.
  • Policies altından Create Policy ile poliçe oluşturun ve JSON editor altına aşağıdaki kodu yapıştırın. Bucket ismi değerini değiştirmelisiniz.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucketMultipartUploads",
                "s3:ListBucketVersions",
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::<bucket-ismi>"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:*Object*",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "s3:ListAllMyBuckets",
            "Resource": "*"
        }
    ]
}
  • Services > IAM > Groups altına gidin .
  • Create new group butonuna tıklayın
  • Grup ismi ve önceki adımda oluşturduğumuz poliçeyi ekleyin
  • Grup oluşturduktan sonra Services > IAM > Users  altına gidin
  • Create User’ a tıklayın ve kullanıcı ismi girişi yaptıktan sonra Access Type değerini Programatic Access olarak değiştirin.
  • Permissions altına giderek yarattığımız kullanıcıyı oluşturduğunuz gruba ekleyin.
  • Create User dedikten sonra csv dosyasını indirmelisiniz. Böylelikle AWS Access Key Id ve AWS Secret Key değerlerini elde etmiş olacağız.

Django Üzerinde Uçbirim Oluşturma

AWS konfigürasyonlarımızı oluşturduk ve Django üzerinde geliştirmemizi yapabiliriz. Ben daha önce bir api uçbirimi olarak oluşturduğum için şimdi de aynı şekilde yapabiliriz.

Uygulamalarda versiyon kontrol araçları içerisine veri tabanı ve aws key gibi kritik bilgileri dahil etmemek oldukça önemli. Bu sayede hem 12 factor uygulama geliştirme standartlarını uygulamış oluyorsunuz hem de uygulamanın güvenliğini sağlamış oluyorsunuz.

Aşağıdaki kodu settings.py altına ekleyin.


import environ
env = environ.Env()
environ.Env.read_env(env_file=os.path.join(BASE_DIR, ".env"))
#####AWS Configuration start
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = '<bucket ismi>'
AWS_REGION = '<region>'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400',
}

#Video properties
VIDEO_ALLOWED_EXTENSIONS = ['mp4', 'mov']
VIDEO_ALLOWED_UPLOAD_MARGIN = 60*30
VIDEO_MAX_FILE_SIZE = 512*1024*1024

Bucket ismi ve region değerlerini kendi belirlediğiniz değerle değiştirmelisiniz.

Proje ana klasöründe .env isminde bir dosya oluşturun ve ortam değişkenlerinizi aşağıdaki formatta ekleyin.

Önemli: .env dosyasını versiyon kontrol uygulamanızdan çıkarmalısınız.

AWS_ACCESS_KEY_ID=<access key id>
AWS_SECRET_ACCESS_KEY=<secret access key>

Video altındaki models.py dosyası altına aşağıdaki kodu ekleyerek bir model oluşturun.


class Video(models.Model):
    class Meta:
        db_table = 'dim_videos'
    video_key = models.CharField(max_length=500, null=False, unique=True)
    video_key_mp4_720p=models.CharField(max_length=500, null=True)
    video_key_mp4_480p = models.CharField(max_length=500, null=True)
    video_key_mp4_360p = models.CharField(max_length=500, null=True)
    video_key_webm_720p = models.CharField(max_length=500, null=True)
    video_key_webm_480p = models.CharField(max_length=500, null=True)
    video_key_webm_360p = models.CharField(max_length=500, null=True)
    video_length_seconds = models.PositiveIntegerField( null=True)
    video_encoded = models.BooleanField(db_index=True, null=True)
    video_encoding_date = models.DateTimeField(null=True)
    video_encoding_percentage=models.PositiveSmallIntegerField(null=True)

ardından aşağıdaki kodu çalıştırarak migration yapabilirsiniz. Db ayarlarını değiştirmediğimiz için ayarlar sqllite veri tabanı üzerine oluşturulacak.


python manage.py makemigrations
python manage.py migrate

Singleton implementasyonuyla uygulama içerisinde geçerli bir S3Client oluşturmamız gerekmekte. Söz konusu S3Client nesnesi uygulamamızdaki tüm s3 operasyonlarını yönetecek. settings.py dosyasının bulunduğu uygulama klasörüne klasöre aws_bean.py dosyasını oluşturun ve aşağıdaki kodu kullanın.

Aşağıdaki S3Client sınıfındaki staticmethod uygulama üzerinde singleton olarak yaşayacak S3 istemci nesnesi oluşturacak. Daha önce yüklediğimiz boto3 kütüphanesiyle birlikte bir s3 istemcisi oluşturuyoruz ve v4 imzalama algoritmasını kullanması için konfigüre ediyoruz.


import boto3
from botocore.client import Config
from django.conf import settings


class S3Client:
    __instance__ = None
    @staticmethod
    def getInstance():
        if not S3Client.__instance__:
            S3Client.__instance__=boto3.client('s3', region_name=settings.AWS_REGION, aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
                  aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
                  config=Config(s3={'addressing_style': 'path'},
                                signature_version='s3v4'))
        return S3Client.__instance__

Django rest framework üzerinden uygulama geliştirdiğimiz için serializer ile doğrulama yaparak rest uçbirimleri oluşturmak daha kolay olacaktır.

video klasörünün altında serializers.py dosyası oluşturup aşağıdaki kodu ekleyelim.


from rest_framework import serializers
from rest_framework.exceptions import *
from django.conf import settings
from .models import Video

class VideoPutSerializer(serializers.Serializer):
    filename = serializers.CharField()
    def validate_filename(self, p_filename):
        filename_sp = p_filename.split('.')
        if len(filename_sp)<2:
            raise ValidationError('Not a valid filename. You should define extension.')
        if filename_sp[-1] not in settings.VIDEO_ALLOWED_EXTENSIONS:
            raise ValidationError(
                f'Not a valid file type. Allowed one of these : {".".join(settings.VIDEO_ALLOWED_EXTENSIONS)}')
        return p_filename

class VideoPostSerializer(serializers.Serializer):
    video_key = serializers.CharField()
    def validate_video_key(self, video_key):
        try:
            video_obj = Video.objects.get(video_key=video_key, video_encoding_percentage=None)
        except Video.DoesNotExist as e:
            raise ValidationError('No uploaded video with provided key.')
        return video_obj

Yukarıda da kullandığımız serializer yapısı form serializer işlemlerinde oluduğu gibi rest uçbiriminin kurallarını kolayca tasarlamamızı sağlar.

Aşağıdaki kodu video klasörü altındaki views.py klasörüne yapıştırıyoruz.


from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import VideoPostSerializer, VideoPutSerializer
from django.db import transaction
from .models import Video
from django.conf import settings
import mimetypes
from Tutorial.aws_bean import S3Client
import uuid

# Create your views here.
class VideoView(APIView):
    def get(self, request):
        success_token = request.query_params.get("success")
        if success_token == "true":
            return Response({"detail": "Upload was successful"}, status.HTTP_200_OK)
        else:
            return Response({"detail": "Method not allowed"}, status.HTTP_405_METHOD_NOT_ALLOWED)

    def post(self, request):
        serializer = VideoPostSerializer(data=request.data)
        if serializer.is_valid():
            video_obj = serializer.validated_data['video_key']
            video_obj.video_encoding_percentage = 0
            with transaction.atomic():
                video_obj.save()
                transaction.on_commit(lambda: print('ok')) ##Burada encoding queue ile video işlenmeli
                return Response({"detail": "Video is pushed into encoding queue."})
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

    def put(self, request):
        """
        Checking if input filename is true
        """
        serializer = VideoPutSerializer(data=request.data)
        if serializer.is_valid():
            #Creating video object
            filename = serializer.validated_data.get('filename')
            extension = filename.split(".")[-1]
            with transaction.atomic():
                video_obj = Video(video_encoded=False)
                video_obj.save()

            #Creating upload key
            upload_key = f"media/video/{video_obj.id}-{uuid.uuid4().hex}/{video_obj.id}.{extension.lower()}"
            with transaction.atomic():
                video_obj.video_key=upload_key
                video_obj.save()

            mimetype = mimetypes.MimeTypes().guess_type(filename)[0]

            url = S3Client.getInstance().generate_presigned_post(
                Bucket=settings.AWS_STORAGE_BUCKET_NAME,
                Key=upload_key,
                Fields={
                    "acl": "public-read",
                    "bucket": settings.AWS_STORAGE_BUCKET_NAME,
                    "success_action_redirect": request.get_host() + "/api/views/video/?success=true",
                    "content-type": mimetype
                },
                Conditions=[
                    {"acl": "public-read"},
                    ["starts-with", "$content-type", "video/"],
                    {"bucket": settings.AWS_STORAGE_BUCKET_NAME},
                    {"success_action_redirect": request.get_host() + "/api/views/video/?success=true"},
                    ["content-length-range", 0, settings.VIDEO_MAX_FILE_SIZE]
                ],
                ExpiresIn=settings.VIDEO_ALLOWED_UPLOAD_MARGIN
            )
            #TODO: Remove before production
            for key in url['fields']:
                print(f"{key}:{url['fields'][key]}")
            return Response(url)
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

Yukarıdaki işlemler karmaşık gelebilir. put işleminde yeni bir video kaydı oluşturuyoruz ve ilgili videonun yüklenmesi için istemciye geçici bir poliçe veriyoruz. Bu poliçe istemciye verilen süre dolmadan kulanılması gerekmekte. Ancak o zaman kullanıcı bizim oluşturduğumuz kural setiyle kontrol edilecek dosyayı yükleyebilir.

Conditions altındaki şartlar ise dökümantasyonda belirtildiği şekilde tanımlanmalıdır. Yukarıdaki şartları açıklayacak olursak access control list açık okuma olarak tanımlanmalı, içerik mimetype “video/” ile başlamalı, bucket settings altındaki bucket olmalı, yükleme başarılıysa “/api/views/video/?success=true” adresine yönlenmeli ve içerik boyutu en fazla settings altında belirtilen miktar kadar olmalıdır.

post işlemiyse dosya yüklendikten sonra çağrılır. Henüz implementasyonu yok fakat celery benzeri bir asenkron işlem kütüphanesine göndererek video encoding işlemi yapmalısınız.

Post işlemi altında DB üzerine çift sorgu yapılmasın diye serializer üzerinden gelen nesne doğrudan kullanıldı.

Son olarak ise url konfigürasyonlarını yaparak uygulamayı çalıştırabiliriz.

Tutorial klasörü altındaki urls.py aşağıdaki gibi olmalıdır.


from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('video.urls'))
]

Ve son olarak video klasörü altındaki urls.py aşağıdaki gibi olmalıdır.


from django.contrib import admin
from django.urls import path
from .views import VideoView
urlpatterns = [
 path('video/', VideoView.as_view())
]

Projenin devamında video nasıl encode edilmelidir sorusu karşımıza çıkıyor. Bunun için aws üzerindeki transcoder servisi alternatifi olarak ffmpeg üzerinden encode etme işlemi yapabiliriz.

Projenin tamamına ulaşmak için:

https://github.com/mehmetozturk4705/django-s3-video-encoding-pipeline

Kategori: Cloud Django

Yorumlar

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir