Como configurar um projeto Django com django-storages e Amazon S3, mas com pastas diferentes para arquivos estáticos e arquivos de mídia?

92

Estou configurando um projeto Django que estava usando o sistema de arquivos do servidor para armazenar os arquivos estáticos dos aplicativos (STATIC_ROOT ) e os arquivos carregados pelo usuário ( MEDIA_ROOT).

Eu preciso agora hospedar todo esse conteúdo no S3 da Amazon, então criei um bucket para isso. Usando django-storageso botoback-end de armazenamento, consegui fazer upload das estatísticas coletadas para o intervalo S3:

MEDIA_ROOT = '/media/'
STATIC_ROOT = '/static/'

DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
AWS_ACCESS_KEY_ID = 'KEY_ID...'
AWS_SECRET_ACCESS_KEY = 'ACCESS_KEY...'
AWS_STORAGE_BUCKET_NAME = 'bucket-name'
STATICFILES_STORAGE = 'storages.backends.s3boto.S3BotoStorage'

Então, eu tenho um problema: o MEDIA_ROOT e STATIC_ROOTnão são usados ​​dentro do intervalo, portanto, a raiz do intervalo contém os arquivos estáticos e os caminhos carregados pelo usuário.

Então eu poderia definir:

S3_URL = 'http://s3.amazonaws.com/%s' % AWS_STORAGE_BUCKET_NAME
STATIC_URL = S3_URL + STATIC_ROOT
MEDIA_URL = 'S3_URL + MEDIA_ROOT

E use essas configurações nos modelos, mas não há distinção de arquivos estáticos / de mídia ao armazenar no S3 com django-storages.

Como isso pode ser feito?

Obrigado!

Armando Pérez Marqués
fonte
8
Porque há apenas uma configuração para especificar o nome do intervalo ( AWS_STORAGE_BUCKET_NAME), e é aquela usada quando uma instância da classe especificada em STATICFILES_STORAGEé instanciada.
Armando Pérez Marqués

Respostas:

126

Acho que o seguinte deve funcionar e ser mais simples do que o método de Mandx, embora seja muito semelhante:

Crie um s3utils.pyarquivo:

from storages.backends.s3boto import S3BotoStorage

StaticRootS3BotoStorage = lambda: S3BotoStorage(location='static')
MediaRootS3BotoStorage  = lambda: S3BotoStorage(location='media')

Então em seu settings.py:

DEFAULT_FILE_STORAGE = 'myproject.s3utils.MediaRootS3BotoStorage'
STATICFILES_STORAGE = 'myproject.s3utils.StaticRootS3BotoStorage'

Um exemplo diferente, mas relacionado (que eu realmente testei) pode ser visto nos dois example_arquivos aqui .

Bradenm
fonte
1
Definitivamente mais simples e melhor do que a minha versão. Embora eu não tenha testado isso, também acho que vai funcionar. Obrigado! Também estou verificando seu repo django-s3storage , parece uma solução muito leve se o projeto usa S3 exclusivamente.
Armando Pérez Marqués
1
E, se você gosta mais de embalagens, uma olhada no django-s3-folder-storage . Acabei de encontrar, não posso dizer se é a mesma solução, mas pré-embalada.
Armando Pérez Marqués
4
Isso não funciona comigo, os arquivos de mídia são carregados para o / do intervalo s3. Parece que a configuração de localização não está sendo respeitada. django-storages == 1.1.6, django-extensions == 1.1.1, django = 1.4
Nathan Keller
3
Para mim, fazia mais sentido ter baldes separados e não gosto de ter a configuração fora do meu módulo de configurações, então minha solução acabou parecendo com esta gist.github.com/antonagestam/6075199
antonagestam
1
Essa solução não funciona, pelo que posso dizer. Esta deve ser a abordagem: gist.github.com/defrex/82680e858281d3d3e6e4
defrex
8

Atualmente, estou usando este código em um s3utilsmódulo separado :

from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_unicode

from storages.backends.s3boto import S3BotoStorage


def safe_join(base, *paths):
    """
    A version of django.utils._os.safe_join for S3 paths.

    Joins one or more path components to the base path component intelligently.
    Returns a normalized version of the final path.

    The final path must be located inside of the base path component (otherwise
    a ValueError is raised).

    Paths outside the base path indicate a possible security sensitive operation.
    """
    from urlparse import urljoin
    base_path = force_unicode(base)
    paths = map(lambda p: force_unicode(p), paths)
    final_path = urljoin(base_path + ("/" if not base_path.endswith("/") else ""), *paths)
    # Ensure final_path starts with base_path and that the next character after
    # the final path is '/' (or nothing, in which case final_path must be
    # equal to base_path).
    base_path_len = len(base_path) - 1
    if not final_path.startswith(base_path) \
       or final_path[base_path_len:base_path_len + 1] not in ('', '/'):
        raise ValueError('the joined path is located outside of the base path'
                         ' component')
    return final_path


class StaticRootS3BotoStorage(S3BotoStorage):
    def __init__(self, *args, **kwargs):
        super(StaticRootS3BotoStorage, self).__init__(*args, **kwargs)
        self.location = kwargs.get('location', '')
        self.location = 'static/' + self.location.lstrip('/')

    def _normalize_name(self, name):
        try:
            return safe_join(self.location, name).lstrip('/')
        except ValueError:
            raise SuspiciousOperation("Attempted access to '%s' denied." % name)


class MediaRootS3BotoStorage(S3BotoStorage):
    def __init__(self, *args, **kwargs):
        super(MediaRootS3BotoStorage, self).__init__(*args, **kwargs)
        self.location = kwargs.get('location', '')
        self.location = 'media/' + self.location.lstrip('/')

    def _normalize_name(self, name):
        try:
            return safe_join(self.location, name).lstrip('/')
        except ValueError:
            raise SuspiciousOperation("Attempted access to '%s' denied." % name)

Então, no meu módulo de configurações:

DEFAULT_FILE_STORAGE = 'myproyect.s3utils.MediaRootS3BotoStorage'
STATICFILES_STORAGE = 'myproyect.s3utils.StaticRootS3BotoStorage'

Eu tive que redefinir o _normalize_name()método privado para usar uma versão "fixa" da safe_join()função, já que o código original está me dandoSuspiciousOperation exceções para caminhos legais.

Estou postando isso para consideração, se alguém puder dar uma resposta melhor ou melhorar esta, será muito bem-vindo.

Armando Pérez Marqués
fonte
7

Arquivo: PROJECT_NAME / custom_storages.py

from django.conf import settings
from storages.backends.s3boto import S3BotoStorage

class StaticStorage(S3BotoStorage):
    location = settings.STATICFILES_LOCATION

class MediaStorage(S3BotoStorage):
    location = settings.MEDIAFILES_LOCATION

Arquivo: PROJECT_NAME / settings.py

STATICFILES_LOCATION = 'static'
MEDIAFILES_LOCATION = 'media'

if not DEBUG:
    STATICFILES_STORAGE = 'PROJECT_NAME.custom_storages.StaticStorage'
    DEFAULT_FILE_STORAGE = 'PROJECT_NAME.custom_storages.MediaStorage'
    AWS_ACCESS_KEY_ID = 'KEY_XXXXXXX'
    AWS_SECRET_ACCESS_KEY = 'SECRET_XXXXXXXXX'
    AWS_STORAGE_BUCKET_NAME = 'BUCKET_NAME'
    AWS_HEADERS = {'Cache-Control': 'max-age=86400',}
    AWS_QUERYSTRING_AUTH = False

E corra: python manage.py collectstatic

Oscar Enrique Lotero S
fonte
Se acontecer de você nomear este arquivo em storages.pyvez de custom_storages.pyVocê deseja usarfrom __future__ import absolute_import
Aaron McMillin
2

Acho que a resposta é muito simples e feita por padrão. Isso está funcionando para mim no AWS Elastic Beanstalk com Django 1.6.5 e Boto 2.28.0:

STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.Loader',
    'django.template.loaders.app_directories.Loader',
)

DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
STATICFILES_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID']
AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_KEY']

As chaves da AWS são passadas do arquivo de configuração do contêiner e eu não as tenho STATIC_ROOTou as STATIC_URLdefini. Além disso, não há necessidade de s3utils.pyarquivo. Esses detalhes são tratados pelo sistema de armazenamento automaticamente. O truque aqui é que eu precisava fazer referência a esse caminho desconhecido em meus modelos de maneira correta e dinâmica. Por exemplo:

<link rel="icon" href="{% static "img/favicon.ico" %}">

É assim que me dirijo ao meu favicon que reside localmente (pré-implantação) em ~/Projects/my_app/project/my_app/static/img/favicon.ico.

Claro que tenho um local_settings.pyarquivo separado para acessar essas coisas localmente no ambiente dev e ele tem configurações ESTÁTICAS e MÍDIA. Tive de experimentar e ler muito para encontrar esta solução e funciona de forma consistente e sem erros.

Eu entendo que você precisa da separação estática e raiz e considerando que você só pode fornecer um intervalo, gostaria de salientar que esse método pega todas as pastas em meu ambiente local ~/Projects/my_app/project/my_app/static/e cria uma pasta na raiz do intervalo (ou seja: S3bucket / img / como no exemplo acima). Então você consegue separação de arquivos. Por exemplo, você poderia ter ummedia pasta na staticpasta e acessá-la por meio de modelos com isto:

{% static "media/" %}

Eu espero que isso ajude. Vim aqui em busca de uma resposta e me esforcei um pouco mais para encontrar uma solução mais simples do que estender o sistema de armazenamento. Em vez disso, li a documentação sobre o uso pretendido do Boto e descobri que muito do que eu precisava estava embutido por padrão. Felicidades!

e.thompsia
fonte