From 49b3bb8ffe6311b877409886ecfdb90d8b4beb15 Mon Sep 17 00:00:00 2001 From: rguptar <69279773+rguptar@users.noreply.github.com> Date: Wed, 30 Nov 2022 10:26:58 -0800 Subject: [PATCH] Add Azure Storage as an option to serve static and media files (#3967) Fix https://github.com/cookiecutter/cookiecutter-django/issues/2301 --- README.md | 4 ++-- cookiecutter.json | 1 + docs/project-generation-options.rst | 4 +++- docs/settings.rst | 3 +++ hooks/pre_gen_project.py | 7 ++----- tests/test_cookiecutter_generation.py | 14 +++++++++++++- .../.envs/.production/.django | 6 ++++++ .../config/settings/production.py | 12 +++++++++++- .../requirements/production.txt | 2 ++ .../utils/storages.py | 11 +++++++++++ 10 files changed, 54 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9f2af6d3..27fc6e0e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ production-ready Django projects quickly. - Optional basic ASGI setup for Websockets - Optional custom static build using Gulp and livereload - Send emails via [Anymail](https://github.com/anymail/django-anymail) (using [Mailgun](http://www.mailgun.com/) by default or Amazon SES if AWS is selected cloud provider, but switchable) -- Media storage using Amazon S3 or Google Cloud Storage +- Media storage using Amazon S3, Google Cloud Storage or Azure Storage - Docker support using [docker-compose](https://github.com/docker/compose) for development and production (using [Traefik](https://traefik.io/) with [LetsEncrypt](https://letsencrypt.org/) support) - [Procfile](https://devcenter.heroku.com/articles/procfile) for deploying to Heroku - Instructions for deploying to [PythonAnywhere](https://www.pythonanywhere.com/) @@ -41,7 +41,7 @@ production-ready Django projects quickly. *These features can be enabled during initial project setup.* -- Serve static files from Amazon S3, Google Cloud Storage or [Whitenoise](https://whitenoise.readthedocs.io/) +- Serve static files from Amazon S3, Google Cloud Storage, Azure Storage or [Whitenoise](https://whitenoise.readthedocs.io/) - Configuration for [Celery](https://docs.celeryq.dev) and [Flower](https://github.com/mher/flower) (the latter in Docker setup only) - Integration with [MailHog](https://github.com/mailhog/MailHog) for local email testing - Integration with [Sentry](https://sentry.io/welcome/) for error logging diff --git a/cookiecutter.json b/cookiecutter.json index 90d084b3..97041101 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -27,6 +27,7 @@ "cloud_provider": [ "AWS", "GCP", + "Azure", "None" ], "mail_service": [ diff --git a/docs/project-generation-options.rst b/docs/project-generation-options.rst index 2d8103cf..0560badd 100644 --- a/docs/project-generation-options.rst +++ b/docs/project-generation-options.rst @@ -66,7 +66,8 @@ cloud_provider: 1. AWS_ 2. GCP_ - 3. None + 3. Azure_ + 4. None Note that if you choose no cloud provider, media files won't work. @@ -147,6 +148,7 @@ debug: .. _AWS: https://aws.amazon.com/s3/ .. _GCP: https://cloud.google.com/storage/ +.. _Azure: https://azure.microsoft.com/en-us/products/storage/blobs/ .. _Amazon SES: https://aws.amazon.com/ses/ .. _Mailgun: https://www.mailgun.com diff --git a/docs/settings.rst b/docs/settings.rst index b8c6d448..4691adbb 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -49,6 +49,9 @@ DJANGO_AWS_S3_CUSTOM_DOMAIN AWS_S3_CUSTOM_DOMAIN n/a DJANGO_AWS_S3_MAX_MEMORY_SIZE AWS_S3_MAX_MEMORY_SIZE n/a 100_000_000 DJANGO_GCP_STORAGE_BUCKET_NAME GS_BUCKET_NAME n/a raises error GOOGLE_APPLICATION_CREDENTIALS n/a n/a raises error +DJANGO_AZURE_ACCOUNT_KEY AZURE_ACCOUNT_KEY n/a raises error +DJANGO_AZURE_ACCOUNT_NAME AZURE_ACCOUNT_NAME n/a raises error +DJANGO_AZURE_CONTAINER_NAME AZURE_CONTAINER n/a raises error SENTRY_DSN SENTRY_DSN n/a raises error SENTRY_ENVIRONMENT n/a n/a production SENTRY_TRACES_SAMPLE_RATE n/a n/a 0.0 diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 2845f012..c3eef1e4 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -72,11 +72,8 @@ if ( sys.exit(1) if ( - "{{ cookiecutter.cloud_provider }}" == "GCP" - and "{{ cookiecutter.mail_service }}" == "Amazon SES" -) or ( - "{{ cookiecutter.cloud_provider }}" == "None" - and "{{ cookiecutter.mail_service }}" == "Amazon SES" + "{{ cookiecutter.mail_service }}" == "Amazon SES" + and "{{ cookiecutter.cloud_provider }}" != "AWS" ): print( "You should either use AWS or select a different " diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 3a757fcb..6219f75e 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -56,6 +56,8 @@ SUPPORTED_COMBINATIONS = [ {"cloud_provider": "AWS", "use_whitenoise": "n"}, {"cloud_provider": "GCP", "use_whitenoise": "y"}, {"cloud_provider": "GCP", "use_whitenoise": "n"}, + {"cloud_provider": "Azure", "use_whitenoise": "y"}, + {"cloud_provider": "Azure", "use_whitenoise": "n"}, {"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailgun"}, {"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailjet"}, {"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mandrill"}, @@ -82,7 +84,16 @@ SUPPORTED_COMBINATIONS = [ {"cloud_provider": "GCP", "mail_service": "SendinBlue"}, {"cloud_provider": "GCP", "mail_service": "SparkPost"}, {"cloud_provider": "GCP", "mail_service": "Other SMTP"}, - # Note: cloud_providers GCP and None with mail_service Amazon SES is not supported + {"cloud_provider": "Azure", "mail_service": "Mailgun"}, + {"cloud_provider": "Azure", "mail_service": "Mailjet"}, + {"cloud_provider": "Azure", "mail_service": "Mandrill"}, + {"cloud_provider": "Azure", "mail_service": "Postmark"}, + {"cloud_provider": "Azure", "mail_service": "Sendgrid"}, + {"cloud_provider": "Azure", "mail_service": "SendinBlue"}, + {"cloud_provider": "Azure", "mail_service": "SparkPost"}, + {"cloud_provider": "Azure", "mail_service": "Other SMTP"}, + # Note: cloud_providers GCP, Azure, and None + # with mail_service Amazon SES is not supported {"use_async": "y"}, {"use_async": "n"}, {"use_drf": "y"}, @@ -113,6 +124,7 @@ SUPPORTED_COMBINATIONS = [ UNSUPPORTED_COMBINATIONS = [ {"cloud_provider": "None", "use_whitenoise": "n"}, {"cloud_provider": "GCP", "mail_service": "Amazon SES"}, + {"cloud_provider": "Azure", "mail_service": "Amazon SES"}, {"cloud_provider": "None", "mail_service": "Amazon SES"}, ] diff --git a/{{cookiecutter.project_slug}}/.envs/.production/.django b/{{cookiecutter.project_slug}}/.envs/.production/.django index e7e8461c..ad652c9a 100644 --- a/{{cookiecutter.project_slug}}/.envs/.production/.django +++ b/{{cookiecutter.project_slug}}/.envs/.production/.django @@ -44,6 +44,12 @@ DJANGO_AWS_STORAGE_BUCKET_NAME= # ------------------------------------------------------------------------------ GOOGLE_APPLICATION_CREDENTIALS= DJANGO_GCP_STORAGE_BUCKET_NAME= +{% elif cookiecutter.cloud_provider == 'Azure' %} +# Azure +# ------------------------------------------------------------------------------ +DJANGO_AZURE_ACCOUNT_KEY= +DJANGO_AZURE_ACCOUNT_NAME= +DJANGO_AZURE_CONTAINER_NAME= {% endif %} # django-allauth # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index 8eed5a80..40d3f19c 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -100,6 +100,10 @@ aws_s3_domain = AWS_S3_CUSTOM_DOMAIN or f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws {% elif cookiecutter.cloud_provider == 'GCP' %} GS_BUCKET_NAME = env("DJANGO_GCP_STORAGE_BUCKET_NAME") GS_DEFAULT_ACL = "publicRead" +{% elif cookiecutter.cloud_provider == 'Azure' %} +AZURE_ACCOUNT_KEY = env("DJANGO_AZURE_ACCOUNT_KEY") +AZURE_ACCOUNT_NAME = env("DJANGO_AZURE_ACCOUNT_NAME") +AZURE_CONTAINER = env("DJANGO_AZURE_CONTAINER_NAME") {% endif -%} {% if cookiecutter.cloud_provider != 'None' or cookiecutter.use_whitenoise == 'y' -%} @@ -116,6 +120,9 @@ STATIC_URL = f"https://{aws_s3_domain}/static/" STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticRootGoogleCloudStorage" COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy" STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/" +{% elif cookiecutter.cloud_provider == 'Azure' -%} +STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticRootAzureStorage" +STATIC_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/static/" {% endif -%} # MEDIA @@ -126,6 +133,9 @@ MEDIA_URL = f"https://{aws_s3_domain}/media/" {%- elif cookiecutter.cloud_provider == 'GCP' %} DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootGoogleCloudStorage" MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" +{%- elif cookiecutter.cloud_provider == 'Azure' %} +DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootAzureStorage" +MEDIA_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/media/" {%- endif %} # EMAIL @@ -228,7 +238,7 @@ COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True) {%- if cookiecutter.cloud_provider == 'None' %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" -{%- elif cookiecutter.cloud_provider in ('AWS', 'GCP') and cookiecutter.use_whitenoise == 'n' %} +{%- elif cookiecutter.cloud_provider in ('AWS', 'GCP', 'Azure') and cookiecutter.use_whitenoise == 'n' %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE COMPRESS_STORAGE = STATICFILES_STORAGE {%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index 275633dd..c9b5b88a 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -20,6 +20,8 @@ hiredis==2.0.0 # https://github.com/redis/hiredis-py django-storages[boto3]==1.13.1 # https://github.com/jschneier/django-storages {%- elif cookiecutter.cloud_provider == 'GCP' %} django-storages[google]==1.13.1 # https://github.com/jschneier/django-storages +{%- elif cookiecutter.cloud_provider == 'Azure' %} +django-storages[azure]==1.13.1 # https://github.com/jschneier/django-storages {%- endif %} {%- if cookiecutter.mail_service == 'Mailgun' %} django-anymail[mailgun]==8.6 # https://github.com/anymail/django-anymail diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py index b712d323..27595ad1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py @@ -22,4 +22,15 @@ class StaticRootGoogleCloudStorage(GoogleCloudStorage): class MediaRootGoogleCloudStorage(GoogleCloudStorage): location = "media" file_overwrite = False +{%- elif cookiecutter.cloud_provider == 'Azure' -%} +from storages.backends.azure_storage import AzureStorage + + +class StaticRootAzureStorage(AzureStorage): + location = "static" + + +class MediaRootAzureStorage(AzureStorage): + location = "media" + file_overwrite = False {%- endif %}