Django、Gunicorn、Nginx、Postgres、Digitial Ocean Server 图像上传错误 500

Django, Gunicorn, Nginx, Postgres, Digitial Ocean Server Error 500 on Image Upload

我正在使用 Django 开发 website/blog,并且正在寻找合适的 setup/settings。我在运行UbuntuServer 16.04虚拟机中进行测试。我使用的似乎是 Gunicorn 和 Nginx 以及 PostgreSQL 数据库的常见设置,并在 Digital Ocean Spaces 上托管静态和媒体文件。我也打算在 Digital Ocean 上托管该网站。

我从几个不同的指南中拼凑了一些东西here, here, here, and here

我还使用 Django-Imagekit 处理图像(url、调整大小等)并在 Django Admin 中管理所有内容。

我面临的问题是,当我上传图像(直接到图像表单或通过 post 表单)并保存对象时,我最终收到服务器错误 (500)。如果我刷新页面,它就可以正常工作。这也发生在网站本身(即转到主页,服务器错误,刷新,没有错误)。

我的 Gunicorn 和 Nginx 日志也完全没有错误。

文件结构:

site
├── project
│   ├── gallery
│   │   ├── static
│   │   │   ├── gallery
│   │   │   │   ├── css
│   │   │   │   └── images
│   │   ├── templates
│   │   │   └── gallery
│   │   ├── admin.py
│   │   ├── models.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── posts
│   │   ├── static
│   │   │   ├── posts
│   │   │   │   ├── css
│   │   │   │   └── images
│   │   ├── templates
│   │   │   └── gallery
│   │   ├── admin.py
│   │   ├── models.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── project
│   │   ├── settings
│   │   │   ├── base.py
│   │   │   ├── development.py
│   │   │   ├── local.py
│   │   │   ├── production.py
│   │   │   └── testing.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── static
│   └── templates

gallery/models.py:

...
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit, ResizeToFill
...

class Watermark(object):
    def process(self, image):
        pass

class Image(models.Model):
    original = ImageField(upload_to='images/%Y/%m/%d/')
    large = ImageSpecField(source='original', processors=[Watermark(), \
        ResizeToFit(width=2000, height=2000, upscale=False)], \
        format='JPEG', options={'quality': 90})    
    medium=...
    small=...
    wide=...
    home=...
    upload_date = models.DateTimeField(null=True, editable=False)

    def save(self):
    if not self.id and not self.original:
        return

    if self.upload_date is None:
        self.upload_date = timezone.now()

    image = PIL.Image.open(self.original)
    imgFormat = image.format
    MAX_HEIGHT = 4000
    MAX_WIDTH = 4000

    # Resize image if over MAX pixels in either direction
    (width, height) = image.size
    if height > MAX_HEIGHT or width > MAX_WIDTH:
        ratio = width / height
        output = BytesIO()

        if width > height:
            width = MAX_WIDTH
            height = int(width / ratio)
        else:
            height = MAX_HEIGHT
            width = int(height * ratio)

        size = (width, height)
        image = image.resize(size, PIL.Image.ANTIALIAS)
        image.save(output, format=imgFormat, quality=100)
        self.original = InMemoryUploadedFile(output, 'ImageField', \
            self.original.name, 'images/', sys.getsizeof(output), None)
    super(Image, self).save()

posts/models.py:

class Post(models.Model):
    title = models.CharField(max_length=75)
    ...
    image = models.ForeignKey(Image, blank=True, null=True, \
        on_delete=models.SET_NULL)
    ...

settings/base.py:

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

SECRET_KEY = os.environ['SECRET_KEY']

DEBUG = True

ALLOWED_HOSTS = []

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts',
    'gallery',
    'taggit',
    'ckeditor',
    'storages',
    'imagekit',
    ...
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.media',
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

settings/testing.py:

from project.settings.base import *

# Override base.py settings here

DEBUG = False

ALLOWED_HOSTS = ['*']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'test',
        'USER': 'user',
        'PASSWORD': '****',
        'HOST': 'localhost',
        'PORT': '1234',
    }
}

# DigitalOcean Spaces Settings
AWS_ACCESS_KEY_ID = '*****'
AWS_SECRET_ACCESS_KEY = '*****'
AWS_STORAGE_BUCKET_NAME = 'production-storage'
AWS_S3_ENDPOINT_URL = 'https://nyc3.digitaloceanspaces.com'
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400'
}
AWS_LOCATION = 'static_test/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static', 'static'),
]

STATIC_URL = 'https://%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.NonValidatingImageCacheBackend'

# Needed for CKEditor to work
AWS_QUERYSTRING_AUTH = False

venv/bin/gunicorn_start:

#!/bin/bash

NAME="project"
DIR=/home/user/site/project
USER=brandon
GROUP=brandon
WORKERS=3
BIND=unix:/home/user/run/gunicorn.sock
DJANGO_SETTINGS_MODULE=project.settings.testing
DJANGO_WSGI_MODULE=project.wsgi
SECRET_KEY='*****'
LOG_LEVEL=error

cd $DIR
source ../../venv/bin/activate

export SECRET_KEY=$SECRET_KEY
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DIR:$PYTHONPATH

exec ../../venv/bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
 --name $NAME \
 --workers $WORKERS \
 --user=$USER \
 --group=$GROUP \
 --bind=$BIND \
 --log-level=$LOG_LEVEL \
 --log-file=-

/etc/nginx/sites-available/project:

upstream app_server {
    server unix:/home/user/run/gunicorn.sock fail_timeout=0;
}

server {
    listen 80;

    # add here the ip address of your server
    # or a domain pointing to that ip(like example.com or www.example.com)
    server_name 192.168.1.179

    keepalive_timeout 5;
    client_max_body_size 4G;
    access_log /home/user/logs/nginx-access.log;
    error_log /home/user/logs/nginx-error.log;

    location /static/ {
        alias /home/user/site/project/static;   
    }

    # checks for static file, if not found proxy to app
    location / {
        try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://app_server;
    }
}

如有任何帮助,我们将不胜感激。

编辑:

将 Debug 设置为 True 时出现以下错误。

这个问题似乎与 Django-Imagekit 有关

Internal Server Error: /posts/
Traceback (most recent call last):
    File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 882, in _resolve_lookup
    current = current[bit]
TypeError: 'ImageCacheFile' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/venv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/home/user/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/user/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/user/site/project/posts/views.py", line 39, in posts
    return render(request, 'posts/posts.html', {'posts':posts, 'recentTags':recent_tags})
  File "/home/user/venv/lib/python3.5/site-packages/django/shortcuts.py", line 30, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader.py", line 68, in render_to_string
    return template.render(context, request)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/backends/django.py", line 66, in render
    return self.template.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 207, in render
    return self._render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader_tags.py", line 177, in render
    return compiled_parent._render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader_tags.py", line 72, in render
    result = block.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/loader_tags.py", line 216, in render
    return template.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 209, in render
    return self._render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/defaulttags.py", line 216, in render
    nodelist.append(node.render_annotated(context))
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/defaulttags.py", line 322, in render
    return nodelist.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 990, in render
    bit = node.render_annotated(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 957, in render_annotated
    return self.render(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 1040, in render
    output = self.filter_expression.resolve(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 708, in resolve
    obj = self.var.resolve(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 849, in resolve
    value = self._resolve_lookup(context)
  File "/home/user/venv/lib/python3.5/site-packages/django/template/base.py", line 890, in _resolve_lookup
    current = getattr(current, bit)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 85, in url
    return self._storage_attr('url')
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 75, in _storage_attr
    existence_required.send(sender=self, file=self)
  File "/home/user/venv/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 193, in send
    for receiver in self._live_receivers(sender)
  File "/home/user/venv/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 193, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/registry.py", line 53, in existence_required_receiver
    self._receive(file, 'on_existence_required')
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/registry.py", line 61, in _receive
    call_strategy_method(file, callback)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/utils.py", line 166, in call_strategy_method
    fn(file)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/strategies.py", line 15, in on_existence_required
    file.generate()
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 94, in generate
    self.cachefile_backend.generate(self, force)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/backends.py", line 109, in generate
    self.generate_now(file, force=force)
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/backends.py", line 96, in generate_now
    file._generate()
  File "/home/user/venv/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py", line 103, in _generate
    content.seek(0)
ValueError: I/O operation on closed file.

看来我找到了解决方法。问题是 django-storages 在图像文件上传后关闭导致 django-imagekit 中出现 I/O 错误。

我找到了解决方法 here

import os
from storages.backends.s3boto3 import S3Boto3Storage
from tempfile import SpooledTemporaryFile

class CustomS3Boto3Storage(S3Boto3Storage):
"""
This is our custom version of S3Boto3Storage that fixes a bug in boto3 where the passed in file is closed upon upload.

https://github.com/boto/boto3/issues/929
https://github.com/matthewwithanm/django-imagekit/issues/391
"""

def _save_content(self, obj, content, parameters):
    """
    We create a clone of the content file as when this is passed to boto3 it wrongly closes
    the file upon upload where as the storage backend expects it to still be open
    """
    # Seek our content back to the start
    content.seek(0, os.SEEK_SET)

    # Create a temporary file that will write to disk after a specified size
    content_autoclose = SpooledTemporaryFile()

    # Write our original content into our copy that will be closed by boto3
    content_autoclose.write(content.read())

    # Upload the object which will auto close the content_autoclose instance
    super(CustomS3Boto3Storage, self)._save_content(obj, content_autoclose, parameters)

    # Cleanup if this is fixed upstream our duplicate should always close        
    if not content_autoclose.closed:
        content_autoclose.close()

在项目的某处创建一个文件并添加代码(即storage_backends.py)。然后在设置中,设置:

DEFAULT_FILE_STORAGE='project.storage_backends.CustomS3Boto3Storage'

最新django-storages 版本删除了_save_content 方法。这是此自定义存储的更新版本 class:

class CustomS3Boto3Storage(S3Boto3Storage, ABC):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in
    boto3 where the passed in file is closed upon upload.
    From:
    https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """

    def _save(self, name, content):
        """
        We create a clone of the content file as when this is passed to
        boto3 it wrongly closes the file upon upload where as the storage
        backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)

        # Create a temporary file that will write to disk after a specified
        # size
        content_autoclose = SpooledTemporaryFile()

        # Write our original content into our copy that will be closed by boto3
        content_autoclose.write(content.read())

        # Upload the object which will auto close the content_autoclose
        # instance
        super(CustomS3Boto3Storage, self)._save(name, content_autoclose)

        # Cleanup if this is fixed upstream our duplicate should always close
        if not content_autoclose.closed:
            content_autoclose.close()