2020-04-12

Django 24. 파일 업로드 암호화 / 한글명 파일 다운로드 구현

파일 업로드 / 다운로드를 구현하고 파일명 암호화 저장, 한글명 파일 다운 등을 구현합니다.


프로젝트에서 파일 업로드 / 다운로드 구현 로직은 다음과 같습니다.

  1. 파일 업로드 시 서버에 파일명을 암호화시켜 업로드한다.
  2. 파일 업로드 시 서버 MEDIA_ROOT에 ‘’년, 월, 일’’ 디렉토리 별로 저장한다.
  3. 클라이언트에 파일명을 보여주기 위해 게시글 작성시 파일명을 따로 저장한다.
  4. 한글명 파일 다운로드가 가능하도록 구현한다.
  5. 글 수정, 삭제 시 서버 MEDIA_ROOT에 있는 암호화된 파일들도 동시 수정, 삭제 처리한다.
  6. URL 강제 입력으로 첨부된 게시글 다운로드, 즉 MEDIA_URL에 접근하지 못하게 한다.

게시판의 첨부파일 업로드 기능을 구현하기 위해서 Django FileField를 사용합니다. 포스팅할 내용이 많아 업로드, 다운로드를 나누어 포스팅할 예정이었으나, 업로드와 다운로드는 합치고 다음 포스팅에 클라이언트단에서 게시글이 삭제되거나 수정되었을시 서버쪽 MEDIA_ROOT에서도 암호화되어 저장된 미디어파일들이 동시에 수정, 삭제되도록 구현하는것을 포스팅하겠습니다.

이번 포스팅 (Django 24) : 위의 구현 로직 1번, 2번, 3번, 4번

다음 포스팅 (Django 25) : 위의 구현 로직 5번, 6번

1. MEDIA_URL 설정

포스팅 초기, 프로젝트 세팅하기 부분에서 미리 설정해두었지만 자세한 설명을 위해 다시 한번 언급하겠습니다. Djangomedia파일이란, 모델에서 FileField로 지정된 필드를 통해 접근하고 저장되는 파일들입니다. 쉽게 말해 업로드된 파일은 MEDIA_ROOT에 저장되고, 파일을 요청할 시 MEDIA_URL로 접근하게 됩니다. settings.py에 아래와 같이 경로를 추가하고, 상단 루트에 media폴더를 생성합니다.

1
2
3
4
# cs_web/settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

2. 파일명 암호화 / 날짜별 저장경로 구현

Django의 FileFieldupload_to 옵션을 지정해줌으로써 MEDIA_ROOT 내 저장될 경로를 설정해줄수 있습니다. FileField를 상속받는 ImageFiled 또한 같이 적용됨으로 이미지 첨부만 구현할 때에는 FileFiled 대신 ImageFiled를 사용합니다.

upload_to 옵션은 models.FileField(upload_to=”%Y/%m%/d”)와 같은 형식으로 사용할 수 있습니다. 하지만 따로 함수로 구현하여 저장경로 설정 뿐아니라 파일명을 암호화하여 서버에 저장시키기 위해 아래와 같이 models.py의 모델 클래스 위에 get_file_path라는 함수를 추가합니다.

1
2
3
4
5
6
7
8
9
# notice/models.py

from uuid import uuid4
from datetime import datetime

def get_file_path(instance, filename):
ymd_path = datetime.now().strftime('%Y/%m/%d')
uuid_name = uuid4().hex
return '/'.join(['upload_file/', ymd_path, uuid_name])

객체와, 파일명필드를 인자로 받고 ymd_path에 현재 날짜를 지정된 형식으로 포맷합니다. 또한 파일명을 고유한 문자열로 암호화하기 위해 python uuid를 import하여 사용합니다.

3. 파일 / 파일명 모델 추가

공지사항 게시판 앱에 파일 필드와 파일명 필드를 추가하기 위해 models.py를 수정합니다. 아래와 같이 upload_files, filename을 추가하고 upload_fils필드에 방금 구현한 get_file_path를 upload_to 옵션으로 지정합니다. 이제 upload_files에 접근하면 get_file_path로 구현한 경로, 파일명으로 MEDIA_ROOT에 저장되게 됩니다.

1
2
3
4
# notice/models.py

upload_files = models.FileField(upload_to=get_file_path, null=True, blank=True, verbose_name='파일')
filename = models.CharField(max_length=64, null=True, verbose_name='첨부파일명')

(게시판에 Summernote 텍스트에디터를 적용하였을 경우 Summernote의 이미지업로드가 Django의 FileField에 추가되는 버그가 있을 수 있습니다. Summernote 최신버전을 이용하시거나 Summernote용 FileField를 추가로 생성하시면 됩니다.)

Image파일을 프로세싱하기 위해 python 라이브러리인 pillow를 설치해주고 migrate를 진행합니다.

1
2
3
$ pip install pillow
$ python manage.py makemigrations notice
$ python manage.py migrate

파일 업로드 기능을 구현한 후 파일 업로드를 하면 MEDIA_ROOT에 아래처럼 get_file_path에서 구현한대로 파일명이 암호화되고, 날짜별로 저장되게 됩니다.

django-project-24

4. forms.py 파일필드 추가

파일 업로드 기능을 view에 추가하기 위해 우선 글쓰기 폼인 NoticeWriteForm Meta클래스에 upload_fileds를 추가합니다. 아래와 같이 forms.py를 수정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# notice/forms.py

class NoticeWriteForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(NoticeWriteForm, self).__init__(*args, **kwargs)
self.fields['title'].label = '제목'
self.fields['title'].widget.attrs.update({
'placeholder': '제목을 입력해주세요.',
'class': 'form-control',
'autofocus': True,
})

class Meta:
model = Notice
fields = ['title', 'content', 'top_fixed', 'upload_files']

5. 글작성 view 파일명 저장 추가

업로드된 파일이 암호화되어 저장되어도 파일이 있는 게시글에 접근했을때 그 파일 path를 따라 암호화된 파일명으로 나타내지 않고 원본 파일명 그대로 출력하기 위해 POST요청시 filename필드에 파일명을 저장하는 소스를 아래와 같이 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# notice/views.py

def notice_write_view(request):
if request.method == "POST":
form = NoticeWriteForm(request.POST, request.FILES)
user = request.session['user_id']
user_id = User.objects.get(user_id = user)

if form.is_valid():
notice = form.save(commit = False)
notice.writer = user_id
if request.FILES:
if 'upload_files' in request.FILES.keys():
notice.filename = request.FILES['upload_files'].name
notice.save()
return redirect('notice:notice_list')
else:
form = NoticeWriteForm()

return render(request, "notice/notice_write.html", {'form': form})

POST로 폼이 제출되면 업로드된 파일은 request.POST가 아닌 request.FILES를 통해 전달됩니다. 따라서 NoticeWriteForm의 인자에 request.FILES를 추가합니다. form의 is_valid가 통과되면 요청된 파일들 중에 upload_files를 찾아 그 파일의 파일명을 필드에 저장하게 됩니다.

Django의 request.FILES는 딕셔너리형으로 반환하기에 summernote와 같은 에디터의 이미지업로드와 중복되지 않도록 지정된 Field인 upload_files를 key로 찾아 그 파일명을 filename필드에 저장합니다.

6. 글작성 templates 파일 첨부하기 추가

글작성 템플릿 하단부에 파일첨부하기 영역을 생성하기 위해 notice_write.html에 아래와 같이 upload_files를 폼으로 전달받는 소스를 추가합니다. 또한 POST로 파일을 넘기기 위해서 enctype 설정을 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- templates/notice/notice_write.html -->

<form action="" method="POST" enctype="multipart/form-data">
<div class="card-footer">
<div class="form-group">
{{ form.upload_files }}
<small id="help" class="form-text text-muted">
[파일 업로드]&nbsp;첨부할 파일을 업로드해주세요. 여러 파일은 압축해서 업로드해주세요.
</small>
</div>
</div>
</form>

(FileField는 브라우저 내장 기능이므로 style 지정 등 커스텀이 불가능합니다. 파일업로드 버튼의 색상, Text 등만 커스텀을 원할 시 opacity와 z-index를 설정하고 그 위에 임의로 생성한 버튼을 겹치는 방법이 있습니다.)

7. 한글명 첨부파일 다운로드 구현

파일 업로드 구현이 끝났으므로 이제 업로드된 파일을 다운로드 하기 위한 view를 구현합니다. 한글명으로된 파일도 다운로드가 가능하도록 아래와 같이 views.py에 인코딩 위한 각 모듈들을 import 한 후 소스를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# notice/views.py

import urllib
import os
from django.http import HttpResponse, Http404
import mimetypes

@login_message_required
def notice_download_view(request, pk):
notice = get_object_or_404(Notice, pk=pk)
url = notice.upload_files.url[1:]
file_url = urllib.parse.unquote(url)

if os.path.exists(file_url):
with open(file_url, 'rb') as fh:
quote_file_url = urllib.parse.quote(notice.filename.encode('utf-8'))
response = HttpResponse(fh.read(), content_type=mimetypes.guess_type(file_url)[0])
response['Content-Disposition'] = 'attachment;filename*=UTF-8\'\'%s' % quote_file_url
return response
raise Http404

템플릿의 첨부파일을 클릭할시 매핑된 url을 통해 notice_download_view를 호출합니다. 우선 pk값을 통해 notice 객체를 얻고, url에 첨부된 파일의 경로를 저장합니다. 그 후 퍼센트 인코딩된 텍스트를 되돌리기 위해 urllib.parse.unquote로 변환 후 file_url에 저장합니다. (이 포스팅을 따라 파일명을 암호화 처리하여 저장하셨다면 unquote 변환은 생략하셔도 됩니다.)

os.path.exists로 파일의 존재유무를 체크하고, 바이너리 파일을 읽기 위해 rb 인자로 fh를 설정해둡니다. 그 후 filename을 utf-8로 인코딩한 파일을 urllib.parse.quote를 통해 퍼센트 인코딩으로 변환합니다. response 설정에서 다운로드 받을 타입을 mimetypes.guses_type으로 구분할 수 있습니다. (mimetypes 설정 또한 이 포스팅을 따라 파일명 암호화 저장을 구현하셨다면 생략하셔도 괜찮습니다.)

마지막으로 Content-Disposition의 헤더에 attachment; 를 추가해줌으로써 유니코드 파일명을 utf-8로 인코딩 후 직접 강제 다운로드를 가능할 수 있게 합니다.

8. urls.py 작성, templates 수정

구현한 download view를 연결하기 위해 urls.pyurlpatterns에 아래와 같이 path를 추가합니다.

1
2
3
4
5
# notice/urls.py

urlpatterns = [
path('download/<int:pk>', views.notice_download_view, name="notice_download"),
]

첨부된 파일이 있을시 게시글에 표시하고 다운로드 받을 수 있게 notice_detail.html의 적절한 위치에 아래의 소스를 추가합니다.

1
2
3
4
5
6
7
8
<!-- templates/notice/notice_detail.html -->

{% if notice.upload_files %}
<div class="col-12 text-right">
<span>첨부파일 :&nbsp;</span>
<a href="{% url 'notice:notice_download' notice.id %}" >{{ notice.filename|truncatechars:25 }}</a>
</div>
{% endif %}

9. 결과

django-project-24

다음 파일 동시 수정 / 삭제 포스팅에서 글 수정, 삭제 시 서버 MEDIA_ROOT에 있는 암호화된 파일들도 동시 수정, 삭제 처리와 URL 강제 입력으로 첨부된 게시글 다운로드 방지 구현을 하겠습니다.

*전체 html, css 등은 자세하게 포스팅하지 않습니다. 제 Github에서 소스를 확인하실 수 있습니다.