Comment mettre en place un projet Django avec django-storages et Amazon S3, mais avec des dossiers différents pour les fichiers statiques et les fichiers multimédias?

92

Je configure un projet Django qui utilisait le système de fichiers du serveur pour stocker les fichiers statiques des applications ( STATIC_ROOT) et les fichiers téléchargés par l'utilisateur ( MEDIA_ROOT).

Je dois maintenant héberger tout ce contenu sur S3 d'Amazon, j'ai donc créé un compartiment pour cela. En utilisant django-storagesle botobackend de stockage, j'ai réussi à télécharger les statiques collectées dans le compartiment 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'

Ensuite, j'ai eu un problème: les MEDIA_ROOTet STATIC_ROOTne sont pas utilisés dans le seau, donc la racine du seau contient à la fois les fichiers statiques et les chemins téléchargés par l'utilisateur.

Alors je pourrais définir:

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

Et utilisez ces paramètres dans les modèles, mais il n'y a pas de distinction entre les fichiers statiques / multimédias lors du stockage dans S3 avec django-storages.

Comment cela peut-il être fait?

Merci!

Armando Pérez Marqués
la source
8
Parce qu'il n'y a qu'un seul paramètre pour spécifier le nom du compartiment ( AWS_STORAGE_BUCKET_NAME), et c'est celui utilisé lorsqu'une instance de la classe spécifiée dans STATICFILES_STORAGEest instanciée.
Armando Pérez Marqués

Réponses:

126

Je pense que ce qui suit devrait fonctionner et être plus simple que la méthode de Mandx, bien qu'elle soit très similaire:

Créez un s3utils.pyfichier:

from storages.backends.s3boto import S3BotoStorage

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

Puis dans votre settings.py:

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

Un exemple différent mais lié (que j'ai réellement testé) peut être vu dans les deux example_fichiers ici .

Bradenm
la source
1
Certainement plus simple et meilleur que ma version. Bien que je n'ai pas testé cela, je pense également que cela fonctionnera. Merci! Je vérifie également votre dépôt django-s3storage , semble une solution très légère si le projet utilise exclusivement S3.
Armando Pérez Marqués
1
Et, si vous êtes plus dans l'empaquetage, consultez django-s3-folder-storage . Je viens de le trouver, je ne peux pas dire si c'est la même solution mais préemballée.
Armando Pérez Marqués
4
Cela ne fonctionne pas de moi, les fichiers multimédias sont téléchargés dans le / du compartiment s3. Il semble que le réglage de l'emplacement n'est pas respecté. django-storages == 1.1.6, django-extensions == 1.1.1, django = 1.4
Nathan Keller
3
Pour moi, il était plus logique d'avoir des seaux séparés et je n'aime pas avoir la configuration en dehors de mon module de paramètres, donc ma solution a fini par ressembler à ça gist.github.com/antonagestam/6075199
antonagestam
1
Cette solution ne fonctionne pas, d'après ce que je peux dire. Cela devrait être l'approche: gist.github.com/defrex/82680e858281d3d3e6e4
defrex
8

J'utilise actuellement ce code dans un s3utilsmodule séparé :

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)

Ensuite, dans mon module de paramétrage:

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

J'ai pu redéfinir la _normalize_name()méthode privée pour utiliser une version "fixe" de la safe_join()fonction, puisque le code d'origine me donne des SuspiciousOperationexceptions pour les chemins légaux.

Je publie ceci pour examen, si quelqu'un peut donner une meilleure réponse ou améliorer celle-ci, ce sera le bienvenu.

Armando Pérez Marqués
la source
7

Fichier: 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

Fichier: 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

Et courir: python manage.py collectstatic

Oscar Enrique Lotero S
la source
Si vous nommez ce fichier storages.pyau lieu de custom_storages.pyVous voudrez l'utiliserfrom __future__ import absolute_import
Aaron McMillin
2

Je pense que la réponse est assez simple et faite par défaut. Cela fonctionne pour moi sur AWS Elastic Beanstalk avec Django 1.6.5 et 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']

Les clés AWS sont transmises à partir du fichier de configuration du conteneur et je n'en ai aucune STATIC_ROOTou je les ai STATIC_URLdéfinies. En outre, pas besoin du s3utils.pyfichier. Ces détails sont traités automatiquement par le système de stockage. L'astuce ici est que j'avais besoin de référencer ce chemin inconnu dans mes modèles correctement et dynamiquement. Par exemple:

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

C'est ainsi que j'adresse mon favicon qui vit localement (pré-déploiement) dans ~/Projects/my_app/project/my_app/static/img/favicon.ico.

Bien sûr, j'ai un local_settings.pyfichier séparé pour accéder à ce contenu localement dans l'environnement de développement et il a des paramètres STATIC et MEDIA. J'ai dû faire beaucoup d'expérimentation et de lecture pour trouver cette solution et cela fonctionne de manière cohérente sans erreur.

Je comprends que vous avez besoin de la séparation statique et racine et étant donné que vous ne pouvez fournir qu'un seul compartiment, je tiens à souligner que cette méthode prend tous les dossiers de mon environnement local ~/Projects/my_app/project/my_app/static/et crée un dossier dans la racine du compartiment (par exemple: S3bucket / img / comme dans l'exemple ci-dessus). Vous obtenez donc une séparation des fichiers. Par exemple, vous pouvez avoir un mediadossier dans le staticdossier et y accéder via un modèle avec ceci:

{% static "media/" %}

J'espère que ça aide. Je suis venu ici à la recherche de la réponse et j'ai poussé un peu plus dur pour trouver une solution plus simple que pour étendre le système de stockage. Au lieu de cela, j'ai lu la documentation sur l'utilisation prévue de Boto et j'ai trouvé qu'une grande partie de ce dont j'avais besoin était intégrée par défaut. À votre santé!

e.thompsie
la source