svthalia/concrexit

View on GitHub
website/thaliawebsite/storage/backend.py

Summary

Maintainability
A
0 mins
Test Coverage
import os

from django.conf import settings
from django.core import signing
from django.core.files.storage import FileSystemStorage

from storages.backends.s3boto3 import S3Boto3Storage, S3ManifestStaticStorage


class S3RenameMixin:
    def rename(self, old_name, new_name):
        self.bucket.Object(new_name).copy_from(
            CopySource={"Bucket": self.bucket.name, "Key": old_name}
        )
        self.delete(old_name)


class PublicS3Storage(S3RenameMixin, S3Boto3Storage):
    """Storage on S3 for public media files.

    This storage is used for files that are publicly accessible. We use CloudFront to
    serve files from S3. CloudFront is configured with a behavior to serve files in the
    PUBLIC_MEDIA_LOCATION publicly, despite the objects in the bucket having a "private"
    ACL. Hence, we can still use the default "private" ACL to prevent people from using
    the S3 bucket directly.

    To support the "ResponseContentDisposition" parameter for the `attachment` argument
    to `PublicS3Storage.url` we do still need to sign the url, but in other cases we can
    strip the signing parameters from urls.
    """

    location = settings.PUBLIC_MEDIA_LOCATION
    file_overwrite = False

    # Cloudfront will have a behavior to serve files in PUBLIC_MEDIA_LOCATION publicly,
    # despite the objects in the bucket having a private ACL. Hence, we can use the
    # default "private" ACL to prevent people from reading from the bucket directly.

    def url(self, name, attachment=False, expire_seconds=None):
        params = {}
        if attachment:
            params[
                "ResponseContentDisposition"
            ] = f'attachment; filename="{attachment}"'

        url = super().url(name, params, expire=expire_seconds)

        if not attachment:
            # The signature is required even for public media in order
            # to support the "ResponseContentDisposition" parameter.
            return self._strip_signing_parameters(url)

        return url


class PrivateS3Storage(S3RenameMixin, S3Boto3Storage):
    location = settings.PRIVATE_MEDIA_LOCATION
    file_overwrite = False

    def url(self, name, attachment=False, expire_seconds=None):
        params = {}
        if attachment:
            params[
                "ResponseContentDisposition"
            ] = f'attachment; filename="{attachment}"'

        return super().url(name, params, expire=expire_seconds)


class StaticS3Storage(S3ManifestStaticStorage):
    location = settings.STATICFILES_LOCATION
    object_parameters = {"CacheControl": "max-age=31536000"}

    # Clear the signing information as we don't need it for static files.
    # Loading the cloudfront key would waste some time for no reason.
    cloudfront_key_id = None
    cloudfront_key = None


class FileSystemRenameMixin:
    def rename(self, old_name, new_name):
        old_name = self.path(old_name)
        new_name = self.path(new_name)
        os.rename(old_name, new_name)


class PublicFileSystemStorage(FileSystemRenameMixin, FileSystemStorage):
    location = os.path.join(settings.MEDIA_ROOT, settings.PUBLIC_MEDIA_LOCATION)
    base_url = settings.PUBLIC_MEDIA_URL

    def url(self, name, attachment=False, expire_seconds=None):
        return super().url(name)


class PrivateFileSystemStorage(FileSystemRenameMixin, FileSystemStorage):
    location = os.path.join(settings.MEDIA_ROOT, settings.PRIVATE_MEDIA_LOCATION)

    def url(
        self,
        name,
        attachment=False,
        expire_seconds=None,
    ):
        sig_info = {
            "name": name,
            "serve_path": name,
            "storage": f"{self.__class__.__module__}.{self.__class__.__name__}",
            "attachment": attachment,
        }
        return f"{super().url(name)}?sig={signing.dumps(sig_info)}"