ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] DRF를 이용한 API 서버 만들기 (3) - 파일 업로드/다운로드
    개발/Python 2022. 1. 9. 02:18

     

     

    Django DRF를 이용한 API 서버 만들기 (2)

    Django DRF를 이용한 API 서버 만들기 (1) 새로운 프로젝트를 시작하면서 장고를 사용했다. 웹 서비스를 만드는 프로젝트고 흔히들 사용하는 기본적인 게시판 기능이 포함된 서비스를 만들었다. 이

    www.floodnut.com

    이전 글을 통해서 간단하고 야매스럽게 CRUD를 구현해보았다.

    이번 글에서는 저번 내용을 보완하는 내용을 추가하고 파일 업로드/다운로드를 알아보려고 한다.

     

     

    추가 내용

    #models.py
    
    class Chapter(models.Model):
        activityid = models.ForeignKey(Activity, related_name='chapterid',db_column='activityid', on_delete=models.CASCADE)
        chapterid = models.AutoField(primary_key=True)
        subject = models.CharField(max_length=129)
        created_time = models.DateTimeField(auto_now_add=True)
        modified_time = models.DateTimeField(auto_now=True)
        article = models.CharField(max_length=500)
        filepath = models.CharField(max_length=32, blank=True, null=True)
        fileid = models.IntegerField(blank=True, null=True)
        last = models.IntegerField(default=0, blank=True,null=True)
        next = models.IntegerField(default=0, blank=True,null=True)
    
        class Meta:
            managed = True
            db_table = 'chapter'
    
        def __str__(self):
            return self.

    우선 Chapter(챕터) 모델을 보자.

    기본적인 내용은 전과 같고 activityid라는 필드를 보면 필드 타입이 ForeignKey로 설정되어 있는 것을 알 수 있다.

    해당 필드의 내부 인자를 살펴보자

     

    • Activity
    • related_name
    • db_column
    • on_delete

    우선 첫 번째로 보이는 Activity는 참조할 테이블의 모델이다.

    우리는 Chapter라는 이름으로 chapter 테이블을 정의했고 같은 방식으로 Activity테이블 또한 정의했을 것이다.

    이때, 우리는 Activity 테이블의 pk를 외래 키로 참조하기 위해 참조할 테이블을 지정해준 것이다.

     

    두 번째로는 related_name이다.

    추후 알아보겠지만 장고는 모델 간 외래 키 필드를 통해 직접 정 참조-역참조를 수행할 수 있다.

    예시를 들자면 Chapter에 해당하는 Activity에 접근하고 싶을 때 "Chapter.activityid.Activity필드"와 같은 방식의 접근이 가능하다.

    또한, related_name에 대해 참조하는 필드의 별칭을 지정할 수도 있다.

    하지만 우리는 관계형 데이터베이스를 사용하면서 참조 대상 필드를 2개 이상 참조할 수도 있다.

    이때, 어떤 컬럼을 참조하는지 명시해야 한다. 만일 이를 명시하지 않는다면

    위의 예시처럼 Chapter의 activityid들이 Activity의 acitivityid를 참조하는 상황에서 related_name을 지정하지 않는다면

    Chapter의 activityid들이 어떤 것을 참조하는지 알 수 없게 된다.

     

    db_column을 알아보자.

    간단하게 우리는 참조대상 테이블의 필드명을 변경해서 참조할 수 있다. 

    이때, 실제 정의될 그 필드 명을 지정해주는 것이다.

     

    마지막으로  on_delete 인자를 보자.

    관계형 데이터베이스에서 테이블을 참조할 때, 우리는 참조된 행을 삭제할 때 그 행을 참조한 행도 같이 삭제하고자 하는 옵션이다.

     

     

    파일 업로드

    이번에는 파일 업로드에 관해 알아보자.

     

    우선 테이블로 파일 업로드하며 같이 저장할 필드들을 지정해주었다.

    #models.py
    
    class Chapterfile(models.Model):
        filepk = models.AutoField(primary_key=True)
        activityid = models.ForeignKey(Activity, db_column='activityid', on_delete=models.CASCADE)
        chapterid = models.ForeignKey(Chapter, db_column='chapterid', on_delete=models.CASCADE)
        filepath = models.CharField(max_length=64)
        filename = models.CharField(max_length=100)
        create_date = models.DateTimeField(auto_now_add=True)
        fileext = models.CharField(max_length=5)
        file = models.FileField(upload_to=sett.MEDIA_URL, null=False)
        def __str__(self):
            return self.filename
    
        class Meta:
            managed = True
            ordering = ['chapterid']
            db_table = 'chapterfile'

     

    그 후에 views.py에서 파서를 FileUploadParser로 지정해주었다.

    이 파서를 통해 파일 업로드를 수행한다면 url 상에서 인자로 filename을 넘겨주어야 한다.

    #views.py
    
    class FileView(APIView):
        parser_classes = (FileUploadParser,)
    
        def post(self, request, filename, format=None, *args, **kwargs):
        
            acti_id = request.parser_context['kwargs']['pk']
            chap_id = request.parser_context['kwargs']['chapterid']
    
            if 'file' not in request.FILES:
                return Response('Empty Content', status=status.HTTP_400_BAD_REQUEST)
    
            f = request.FILES['file']
    
            savedName = f.name.replace(" ", "_")
            ext = os.path.splitext(f.name)[1]
            print(ext)
            if ext in ext_not_allowed:
                return Response('EXT NOT ALLOWED', status=status.HTTP_400_BAD_REQUEST)
    
            newFilename = "%s.%s" % (uuid.uuid4(), ext.replace(".", ""))
            addAttr = request.data
            date = datetime.datetime.now()
    
            destination = open(settings.MEDIA_ROOT + "/" + newFilename, 'wb+')
            chucks = f.read()
            pattern = re.compile(b"(?<=\r\n\r\n)[\s\S]*(?=\r\n------WebKitFormBoundary)", re.S)
            # pattern = re.compile(b'\r\n\r\n[\s\S]+\r\n------WebKitFormBoundary', re.S)
            data = re.search(pattern, chucks)
            if data:
                destination.write(data.group())
            else:
                destination.write(chucks)
            destination.close()  # File should be closed only after all chuns are added
    
      
            addAttr['activityid'] = acti_id
            addAttr['chapterid'] = chap_id
            addAttr['filepath'] = newFilename  # file_path
            addAttr['filename'] = filename
            addAttr['fileext'] = ext
            addAttr['create_date'] = date
            addAttr['file'] = f
            addAttrDict = QueryDict('', mutable=True)
            addAttrDict.update(addAttr)
            fileSerializer = ChapterfileSerializer(data=addAttrDict)
            if fileSerializer.is_valid():
                fileSerializer.save()
                try:
                    os.remove(settings.MEDIA_ROOT + "/" + savedName)
                except:
                    pass
                return Response(status=status.HTTP_201_CREATED)
            else:
                try:
                    os.remove(settings.MEDIA_ROOT + "/" + savedName)
                except:
                    pass
                return Response(fileSerializer.errors, status=status.HTTP_400_BAD_REQUEST)

    파일 명(filename)과 함께 추가 인자를 받고 이를 post 메서드를 재정의 하면서 인자를 지정해주자.

    파일이 HTTP의 POST 요청 상에서 존재한다면 request.FILES를 통해 해당 파일을 받아온다.

     

    받아온 파일은 파일명을 uuid를 이용하여 난수 값으로 변경한 후, 지정된 장고 미디어 루트 경로에 저장한다.

    그 후, 장고 파일 테이블 모델의 필드에 맞는 값을 딕셔너리를 통해 가져오고 이를 QueryDict 타입으로 변경하여 데이터베이스에 저장한다.

     

    FileUploadParser를 이용하면 미디어 루트 경로에 원본 파일명으로 파일이 그대로 저장된다.

    여기서 중복된 파일명을 가진 파일이 다시 업로드된다면 파일명 뒤에 난수 값이 붙어 저장된다.

    하지만 실제 데이터베이스에서는 난수 값이 없는 원본 파일명이 저장되므로 중복되어 저장된 파일을 찾을 수 없다.

    따라서 나는 uuid를 통해 저장한 파일만을 남기고 이 uuid 값을 데이터베이스에 저장함과 동시에 원본 파일을 삭제하는 방식을 선택했다.

     

     

    파일 다운로드

    파일 업로드를 클래스 뷰로 구현했다면, 다운로드는 함수형 뷰로 구현했다.

    ## File Download
    def getfile(request, pk, chapterid, filename):
    
        file_path = os.path.join(settings.MEDIA_ROOT, filename)
    
        content_types = {
            "zip": "application/zip",
            "jpeg": "image/jpeg",
            "jpg": "image/jpeg",
            "pdf": "application/pdf",
            "ppt": "application/vnd.ms-powerpoint",
            "xls": "application/vnd.ms-excel",
            "7z": "application/x-7z-compressed",
            "gif": "image/gif",
            "others": "application/octet-stream"
        }
        c = content_types["others"]
        if filename.split(".")[1] in content_types:
            c = content_types[filename.split(".")[1]]
    
        if os.path.exists(file_path):
            with open(file_path, 'rb') as fh:
                response = HttpResponse(fh.read(), content_type=c)
                response['Content-Disposition'] = 'inline; filename=' + os.path.basename(file_path)
                return response
        else:
            return Response(status=status.HTTP_400_BAD_REQUEST)

     

    파일 다운로드는 간단하다.

    업로드된 파일 정보는 게시글을 조회할 때 함께 전달된다.

    이때, uuid로 변환된 고유한 파일명을 전달받으면 해당 파일을 미디어 루트 경로에서 찾아 반환해주는 방식으로 구현하였다.

     

     

    '개발 > Python' 카테고리의 다른 글

    [Django] DRF를 이용한 API 서버 만들기 (2)  (0) 2021.12.25
    [Django] DRF를 이용한 API 서버 만들기 (1)  (0) 2021.12.24
    [OpenCV] 환경 구성  (0) 2021.09.14

    댓글

Designed by Tistory.