며칠 전 장고를 사용해서 과제를 하면서 파일 업로드 & 다운로드 기능을 만들어보았다. 처음 만들어 본 기능이라서 이리저리 찾아보면서 했다. 간단하게 구현 과정을 정리해보고자 한다.
장고는 기본적으로 media root로 지정된 경로에 파일을 저장한다. 업로드한 파일을 관리할 media root를 settings.py
파일에 추가해준다.
...
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
...
그리고 root url 파일에도 경로를 추가해준다.
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
업로드할 파일 컬럼은 filefield나 imagefield를 사용하면 된다. imagefield는 "filefield의 기능 + image인지 여부 validation 기능"이라고 생각하면 된다.
나는 PDF 파일도 받을 것이기 때문에 filefield를 사용했다.
filefield의 upload_to
라는 속성을 지정해주면 지정한 루트에 파일이 저장된다.
class Document(models.Model):
attached = models.FileField('첨부 파일', upload_to='uploads/')
참고로 field의 첫 번째 인자는 form에 나타날 이름을 지정해준 것이다.
업로드된 파일은 file object로 취급된다. 전체 속성과 메소드를 확인하려면 File Object 문서와 Storage Class 문서를 참고하면 된다.
속성 중에는 file name을 가져오는 name, 이미지 파일의 경우 파일 사이즈를 가져오는 size, 파일이 위치한 경로를 보여주는 path와 url 등이 있다.
>>> docs = Document.objects.get(pk=1)
>>> docs.attached
<FieldFile: uploads/choco.jpg>
>>> docs.attached.name
'uploads/choco.jpg'
>>> docs.attached.size
2054935
>>> docs.attached.path
'home/task/project1/media/uploads/choco.jpg'
>>> car.photo.url
'/media/uploads/choco.jpg'
공식 문서에는 path는 상대경로, url은 절대경로를 보여주는 속성으로 나오는데, 직접 해봤을 때는 url은 상대 경로를, path가 절대 경로를 보여줬다.
추가로, name의 경우 파일 저장 경로인 'uploads/'까지 붙여서 반환되길래 경로를 떼고 파일 이름만 가져오는 메소드를 따로 만들었다.
import os
class Document(models.Model):
attached = models.FileField('첨부 파일', upload_to='uploads/')
def get_filename(self):
return os.path.basename(self.attached.name)
장고 템플릿을 쓸 거라서 form을 만들어줬다.
ModelForm을 가져와서 쓰면 제일 쉬운데, 첨부파일의 경우 필수로 안받을 거라서 + CSS 적용을 위해 따로 필드를 만들어줬다.
from djangoimport forms
from .models import Document
class DocumentForm(forms.ModelForm)
upload = forms.FileField(label='첨부 파일', required=False,
widget=forms.FileInput(attrs={'class': 'form'}))
class Meta:
model = Document
exclude = ['attached']
view에서 업로드한 파일을 가져오려면 request.FILES['필드명']
으로 가져오면 된다.
Generic CBV인 FormView를 사용했다. 장고는 이렇게 기본적으로 제공해주는 뷰 기능이 있어서 참 편하다.
from django.views.generic.edit import FormView
from django.urls import reverse_lazy
class DocumentCreateView(FormView):
template_name = "document/new.html"
form_class = DocumentForm
success_url = reverse_lazy('document_list')
def form_valid(self, form):
if self.request.FILES:
form.instance.attached = self.request.FILES['upload']
form.save()
return super().form_valid(form)
form_valid()는 입력된 form이 validated한지를 체크해주고 통과하면 instance를 만들어주는 메소드인데, 파일을 저장하기 위해 덮어썼다.
참고로 업로드한 파일의 content type은 self.request.FILES['upload'].content_type
으로 알 수 있는데, 다운로드할 때 쓰기 위해 얘도 같이 저장해줬다.
urls.py파일에 경로를 등록하는 것은 당연해서 생략한다.
파일을 업로드하려면 그 form이 첨부 파일을 포함하고 있다는 사실을 알려줘야 한다. form 태그 속성인 enctype
을 "multipart/form-data"로 지정하면 된다.
<form action="{% url 'new' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
</form>
이렇게 하면 파일 업로드 구현은 끝!
파일 다운로드는 view를 따로 만들었다. 업로드보다 훨씬 간단하다.
다운로드 받을 파일을 FileSystemStorage object
로 만들어서 FileResponse
로 반환해주면 된다.
from django.views.generic.detail import SingleObjectMixin
from django.http import FileResponse
from django.core.files.storage import FileSystemStorage
from .models import Document
class FileDownloadView(SingleObjectMixin, View):
queryset = Document.objects.all()
def get(self, request, document_id):
object = self.get_object(document_id)
file_path = object.attached.path
file_type = object.content_type # django file object에 content type 속성이 없어서 따로 저장한 필드
fs = FileSystemStorage(file_path)
response = FileResponse(fs.open(file_path, 'rb'), content_type=file_type)
response['Content-Disposition'] = f'attachment; filename={object.get_filename()}'
return response
마지막에 response에 반드시 Content-Disposition 헤더를 추가해줘서 응답에 첨부된 파일이 있다는 사실을 알려줘야 한다.
from django.urls import path
from .views import FileDownloadView
urlpatterns = [
...
path('document/<int:document_id>/',
FileDownloadView.as_view(), name="download"),
...
]
form 태그에서 action만 지정해주면 된다.
<div>
<label>첨부 파일</label>
<div>{{ document.get_filename }}</div>
<form action='{% url "download" document_id=document.pk %}'>
<button type="submit">
다운로드
</button>
</form>
</div>
이러면 파일 다운로드 기능 구현도 끝이다!
업로드보다 훨씬 간단하다.
이번에 장고 form과 generic CBV를 사용하면서 아직 모르는 게 너무 많다는 생각이 들었다. "Two Scoops of Django"라는 책을 추천받았는데, 읽어보면서 더 공부해봐야겠다.