본문 바로가기

Django and Mongo

Django REST API & Djongo - 3. Image Upload & Download

본 작성자는 아직 1년차도 못채운 햇병아리 코더입니다.

 

(사내에 장고를 다루는 사람이 저 혼자라서) 기술한 내용중에 틀린 내용이 있을 수도 있습니다.

 

예제랍시고 올려놓은 코드가 아주 허접할 수도 있습니다.

 

미숙한 제게 조언을 주실 수 있다면 댓글 부탁드립니다. 겸허히 배우겠습니다.

 

개발 대상 사양은 모두 아래를 공통적으로 사용합니다.

[개발 OS : Windows10]

[설치 OS : CentOS 8 with gnomeUI]

[Python: 3.8]

[Mongodb : 4.4.1]

[PostgreSQL : 12.4]

[Elasticsearch : 7.9.3]

[Tensorflow : 2.4.1]

[scikit-learn : 0.23.2]

------------------------------------------------------------------

 

파일 업로드 및 다운로드 기능은 여러가지 방식을 통해서 구현할 수 있습니다.

 

윈도우에서 제공하는 기능으로 FTP 서버를 구축할수도 있고, API 서버에서 파일을 담아 넘겨줄수도 있습니다.

 

API 서버에서 파일을 응답 형식으로 넘겨줄 때는 하드에 있는 파일을 열어서 보내줄수도 있으며, DB에 있는 바이너리 데이터를 그대로 돌려보내서 그 다음 단계에서 디코딩 할 수도 있을 겁니다.

 

상황에 따라서 적절하게 사용하면 될 것인데, 어떤 상황에 어떤 방법이 어울리는지 모르는 것이 신입의 한계인가 봅니다.

 

그런 관계로 API를 활용한 방법들을 모두 다뤄보고자 합니다.

 

1. 장고의 Media Root를 이용하여 URL을 전송하는 방법.

2. 파일 시스템에 파일을 저장하고 이 저장된 파일을 그때그때 읽어서 돌려주는 방법.

3. 몽고DB의 BinaryField를 활용하여 이를 돌려주는 방법.

 

생각나는건 이 3가지 이고, 3번을 제외하고 2개의 데이터모델을 만들어서 처리해보겠습니다.

 

from djongo import models


class PreprocessedImages(models.Model):
    _id = models.ObjectIdField()
    image_title = models.TextField(null=True, default="a.jpg")
    image_path = models.FilePathField(null=False, default="repository/preprocessed_images/a.jpg")
    image_context = models.TextField(null=False, default="일방통행")

    class Meta:
        db_table = "preprocessed_images"


class UploadedImages(models.Model):
    _id = models.ObjectIdField()
    image_title = models.TextField(null=True, default="a.jpg")
    image_file = models.ImageField(null=False)
    image_context = models.TextField(null=False, default="일방통행")

    class Meta:
        db_table = "uploaded_images"

일단 모델을 수정했습니다.

PreprocessedImages는 파일시스템을 활용,

UploadedImages는 장고의 Media Root를 활용합니다.

 

기본값에 맞게 파일을 하나 알맞은 경로에 넣어줍니다.

 

 

[ 1. 파일 시스템 활용 ]

 

 

옳바른 path만 가지고 있다면 얼마든지 파일을 열어서 돌려줄 수 있습니다.

이번 경우에는 serializer를 수정해야 합니다.

 

<orcapp/serializers.py>

import base64
from rest_framework import serializers
from ocrapp.models import PreprocessedImages
from django.core.files import File


class PreprocessedImageSerializer(serializers.ModelSerializer):
    image = serializers.SerializerMethodField()

    class Meta:
        model = PreprocessedImages
        fields = ["image_title", "image_context", "_id"]

    def get_image(self, obj):
        try:
            f = open(obj.image_path, 'rb')
            data = base64.b64encode(File(f).read())
            f.close()
            return data
        except IOError:
            f = open("repository/preprocessed_images/404.png", "rb")
            data = base64.b64encode(File(f).read())
            f.close()
            return data

이렇게 하면 base64 형식으로 인코딩된 이미지 파일이 문자열로 돌아가게 됩니다.

 

이 문자열을 다시 디코드하면 이미지로 사용할 수 있게 됩니다.

 

postman을 이용한 테스트. 포스팅이 늦어진 주범....

포스트맨을 사용하면 API에서 돌려준 응답을 그대로 테스트해볼 수 있습니다.

이번 경우에는 아래 코드로 값과 이미지가 잘 들어 있는지 확인해보았습니다.

<test code>

let template = `
    <html>
    <h1>{{image_title}}</h1>
    <h1>{{image_context}}</h1>
   <img src="data:image/jpg;base64,  {{image}}" />
    </html>

`;
let api_data = pm.response.json();
pm.visualizer.set(template, api_data);

 

 

 

 

[ 2. 장고 미디어 루트 활용 ]

(OCRandNLP) C:\your\path>python -m pip install Pillow

DRF 에서는 Pillow 혹은 PIL을 기본 이미지 처리 라이브러리로 사용합니다. 다만, PIL은 지원이 끊겨 있으므로 Pillow를 사용하도록 합니다.

 

Django에서는 Media Root를 등록하면 해당 경로에 있는 파일을 유저에게 url 형태로 공개할 수 있습니다. 이 url로 접속하게 되면 장고는 그 파일을 읽어 돌려주게 됩니다.

 

또한 현재 상황에서 Media Root를 등록하지않으면 장고 프로젝트의 최상위 폴더를 Media Root로 사용합니다.

당연히 현재 상황은 바람직하지 않으므로 Media Root를 설정해 줍니다.

 

<OCRandNLP/settings.py>

import os
STATIC_URL = '/static/'

MEDIA_URL = '/media/'

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MEDIA_ROOT = os.path.join(BASE_DIR, 'repository')

(import os 는 맨위로 올려주세요.)

 

MEDIA_ROOT는 반드시 절대경로를 적어야합니다.

 

이제 이미지는 repository 폴더에 저장됩니다.

여기서 모델별로 다른 경로를 사용하고 싶다면 upload_to 매개변수에 폴더명을 적어주면 됩니다.

 

<ocrapp/models.py>

from djongo import models


class UploadedImages(models.Model):
    _id = models.ObjectIdField()
    image_title = models.TextField(null=True, default="a.jpg")
    image_url = models.ImageField(null=False, upload_to="uploaded_pictures")
    image_context = models.TextField(null=False, default="일방통행")

    class Meta:
        db_table = "uploaded_images"

이제 파일은 repository/uploaded_images 에 저장됩니다.

 

<ocrapp/serializers.py>

from rest_framework import serializers
from ocrapp.models import UploadedImages


class UploadedImageSerializer(serializers.ModelSerializer):
    image_url = serializers.ImageField(use_url=True)
    
    class Meta:
        model = UploadedImages
        fields = ["image_title", "image_context", "_id", "image_url"]

serializers 에는 image_url 필드가 image 임을 명시합니다. use_url을 사용하면 url을 응답으로 돌려주게 됩니다.

 

<ocrapp/views.py>

from rest_framework.viewsets import ModelViewSet
from ocrapp.serializers import UploadedImageSerializer
from ocrapp.models import UploadedImages
from ocrapp.decorators import request_converting_to_object_id


class UploadedImageViewSets(ModelViewSet):
    queryset = UploadedImages.objects.all()
    serializer_class = UploadedImageSerializer

    @request_converting_to_object_id
    def retrieve(self, request, *args, **kwargs):
        return super().retrieve(self, request, args, kwargs)

    @request_converting_to_object_id
    def partial_update(self, request, *args, **kwargs):
        return super().partial_update(self, request, args, kwargs)

    @request_converting_to_object_id
    def update(self, request, *args, **kwargs):
        return super().update(self, request, args, kwargs)

    @request_converting_to_object_id
    def destroy(self, request, *args, **kwargs):
        return super().destroy(self, request, args, kwargs)

views.py 에는 특별한 코드를 추가할 필요는 없습니다.

 

 

 

 

[ 3. DB 파일 저장 활용 ]

 

대체로 거의 사용할 일이 없는 경우입니다.

반드시 이렇게 해야 한다면 설계가 잘못된게 아닐까 의심하라는 조언을 많이 보았습니다.

 

장고에서 ImageField와 FileFeild는 Media root를 사용하는 필드에 해당합니다.

실제 DB에 바이너리 파일을 저장해서 사용하려면 BinaryField를 사용할 필요가 있습니다.

 

class ModelSerializer(Serializer):
    """
    A `ModelSerializer` is just a regular `Serializer`, except that:

    * A set of default fields are automatically populated.
    * A set of default validators are automatically populated.
    * Default `.create()` and `.update()` implementations are provided.

    The process of automatically determining a set of serializer fields
    based on the model fields is reasonably complex, but you almost certainly
    don't need to dig into the implementation.

    If the `ModelSerializer` class *doesn't* generate the set of fields that
    you need you should either declare the extra/differing fields explicitly on
    the serializer class, or simply use a `Serializer` class.
    """
    serializer_field_mapping = {
        models.AutoField: IntegerField,
        models.BigIntegerField: IntegerField,
        models.BooleanField: BooleanField,
        models.CharField: CharField,
        models.CommaSeparatedIntegerField: CharField,
        models.DateField: DateField,
        models.DateTimeField: DateTimeField,
        models.DecimalField: DecimalField,
        models.DurationField: DurationField,
        models.EmailField: EmailField,
        models.Field: ModelField,
        models.FileField: FileField,
        models.FloatField: FloatField,
        models.ImageField: ImageField,
        models.IntegerField: IntegerField,
        models.NullBooleanField: BooleanField,
        models.PositiveIntegerField: IntegerField,
        models.PositiveSmallIntegerField: IntegerField,
        models.SlugField: SlugField,
        models.SmallIntegerField: IntegerField,
        models.TextField: CharField,
        models.TimeField: TimeField,
        models.URLField: URLField,
        models.UUIDField: UUIDField,
        models.GenericIPAddressField: IPAddressField,
        models.FilePathField: FilePathField,
    }
        if hasattr(models, 'JSONField'):
        serializer_field_mapping[models.JSONField] = JSONField
    if postgres_fields:
        serializer_field_mapping[postgres_fields.HStoreField] = HStoreField
        serializer_field_mapping[postgres_fields.ArrayField] = ListField
        serializer_field_mapping[postgres_fields.JSONField] = JSONField
    serializer_related_field = PrimaryKeyRelatedField
    serializer_related_to_field = SlugRelatedField
    serializer_url_field = HyperlinkedIdentityField
    serializer_choice_field = ChoiceField
    
    
    ...

 

하지만 DRF 에서는 Binary Field 를 지원하지 않습니다.

 

github.com/encode/django-rest-framework/issues/1855

 

Add `BinaryField`. · Issue #1855 · encode/django-rest-framework

A serializer BinaryField which is mapped to from Django's BinaryField model class (present from 1.6 onwards)

github.com

2014년도에 해당 이슈가 신설되어 진행이 되는 듯 했으나, 현재로써는 더 이상 진행되지 않는다 합니다.

(있을 줄 알고 1주일 동안 찾아본게 함정)

 

정말로 필요하다면 DRF가 아니라 기본 장고 view를 통해서 처리하는 것이 바람직해 보입니다.

 

 

 

 

 

 

 

 

참고:

 

stackoverflow.com/questions/42874638/django-rest-framework-how-to-download-image-with-this-image-send-directly-in-t