2020-04-13

Django 25. 업로드(MEDIA) 파일 동시 수정 / 삭제 구현

글 수정, 삭제시 서버단에서도 동시 처리가 되도록 하고 URL로 MEDIA파일 접근을 제한합니다.


이전 파일업로드 / 다운로드 포스팅과 이어집니다.

1. 글 삭제 시 media파일 동시 삭제 구현

업로드된 파일이 있는 게시글을 삭제하거나 수정할 시 FileField에는 파일의 path만 담고 있기 때문에 Django MEDIA_ROOT에 저장된 파일자체는 삭제되거나 수정되지 않습니다.

우선 게시글 객체가 삭제될 시 MEDIA_ROOT의 파일도 삭제되게 하기 위해 아래와 같이 models.pyNotice클래스 내에 delete() 메소드를 오버라이딩합니다.

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

class Notice(models.Model):

def delete(self, *args, **kargs):
if self.upload_files:
os.remove(os.path.join(settings.MEDIA_ROOT, self.upload_files.path))
super(Notice, self).delete(*args, **kargs)

view에서 delete() 메소드가 호출되면 업로드 파일의 유무를 체크한 후 파일의 path경로와 일치하는 MEDIA_ROOT의 파일을 삭제합니다.

2. 글 수정 시 media파일 동시 수정 구현

우선 기본적인 구현 방법은 FileField에 변경된 파일이 있거나 업로드된 파일을 취소할 시, 기존에 업로드되어 있던 파일은 앞서 오버라이딩한 delete() 메소드로 삭제하고 새로운 파일이 있다면 그 파일을 업로드 하도록 구현합니다.

글수정 view인 notice_edit_view에 POST요청이 들어왔을 시 확인해야할 사항은 사용자가 기존에 첨부한 파일을 업로드 취소하였을 경우와 사용자가 첨부파일을 변경하였을 경우 총 2가지입니다. 이 2가지를 클라이언트에서 처리하여 POST로 요청된 경우 둘 중 하나라도 전달되면 delete() 메소드를 수행합니다. 아래와 같이 views.py의 글수정 view인 notice_edit_view를 수정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# notice/views.py

def notice_edit_view(request, pk):
notice = Notice.objects.get(id=pk)
if request.method == "POST":
if(notice.writer == request.user or request.user.level == '0'):
file_change_check = request.POST.get('fileChange', False)
file_check = request.POST.get('upload_files-clear', False)

if file_check or file_change_check:
os.remove(os.path.join(settings.MEDIA_ROOT, notice.upload_files.path))

form = NoticeWriteForm(request.POST, request.FILES, instance=notice)
if form.is_valid():
notice = form.save(commit = False)
if request.FILES:
if 'upload_files' in request.FILES.keys():
notice.filename = request.FILES['upload_files'].name
notice.save()
messages.success(request, "수정되었습니다.")
return redirect('/notice/'+str(pk))
else:
notice = Notice.objects.get(id=pk)
if notice.writer == request.user or request.user.level == '0':
form = NoticeWriteForm(instance=notice)
context = {
'form': form,
'edit': '수정하기',
}
if notice.filename and notice.upload_files:
context['filename'] = notice.filename
context['file_url'] = notice.upload_files.url
return render(request, "notice/notice_write.html", context)
else:
messages.error(request, "본인 게시글이 아닙니다.")
return redirect('/notice/'+str(pk))

file_change_check로 템플릿에서 업로드 파일이 변경되었는지를 확인하고 file_check로 기존의 파일을 사용자가 취소했는지 확인합니다.두가지 경우 하나라도 전달되었을 경우 MEDIA_ROOT의 파일을 삭제하고 현재 request.FILES을 확인하여 다시 파일명과 파일을 저장합니다.

GET메소드로 클라이언트에 글 수정 템플릿을 뿌려줄 때 기존에 암호화되어 저장되있는 파일을 원본 파일명으로 출력하기 위해 filenamefile_urlcontext에 담아 전달합니다.

3. 글 수정 템플릿 수정

방금 글쓰기 view에서 구현한 file_change_check를 전달하기 위해 notice_wirte.html의 form태그 안에 FileField가 변경되었을 시 checkbox가 checked되는 input태그를 추가합니다. display를 none으로 설정하여 보여지지 않게 하고 view에서 전달받을 수 있도록 지정한 fileChange란 name속성을 추가합니다.

1
2
3
4
5
<!-- templates/notice/notice_write.html -->

<form action="" method="POST" enctype="multipart/form-data">
<input type="checkbox" id="fileChangeCheck" name="fileChange" style="display: none;">
</form>

Django의 FileField로 생성되는 form은 사용자의 입장에서 친절한 형태로 표시되지 않습니다. 그렇기에 jquery를 사용하여 FileField로 지정된 form을 사용자가 변경, 삭제할시 적잘한 안내 문구가 출력되도록 아래와 같이 스크립트를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- templates/notice/notice_write.html -->

{% if filename %}
<script type = "text/javascript">
$('[href="{{ file_url }}"]').html("{{ filename }}");
$('#upload_files-clear_id').css({opacity:'100', position:'relative', 'pointer-events':'auto'})

$("#id_upload_files").change(function() {
if ($('#id_upload_files').get(0).files.length == 1) {
$(".col-sm-12").contents().get(0).nodeValue = '[첨부파일 변경 시 기존 파일은 삭제됩니다.]'
$('#upload_files-clear_id').attr("checked", false)
$('#upload_files-clear_id').prev().hide()
$('#upload_files-clear_id').next().hide()
$('#upload_files-clear_id').hide()
}
});

$('#upload_files-clear_id').on('click', function(){
if($(this).prop('checked')){
$('#id_upload_files').hide()
document.getElementById('id_upload_files').previousSibling.nodeValue = '[업로드 된 첨부파일을 삭제합니다.]'
} else{
$('#id_upload_files').show()
document.getElementById('id_upload_files').previousSibling.nodeValue = '변경: '
}
});

$('#write').click( function() {
if($('#id_upload_files').get(0).files.length == 1){
$('#fileChangeCheck').attr("checked",true)
}
});
</script>
{% endif %}

글 수정 템플릿에서 FileField로 생성한 업로드파일 취소 input태그의 id는 ‘필드명-clear_id‘ 로 지정됩니다. 따라서 기존의 암호화 되어있는 파일명은 opacity 속성을 추가해주어 투명도를 주고, view에서 context로 전달받은 filenamefile_url을 사용하여 원본파일명으로 표시되도록 구현합니다.

그리고 FileField로 생성된 타입이 File인 input태그의 id는 ‘id_필드명‘ 이므로 change 메소드로 input 태그의 변동사항을 확인하고, file 또한 존재할시 display=none으로 설정해둔 fileChangeCheckchecked하여 view에 전달합니다. 스크립트가 잘 적용되었을 시 아래와 같은 결과를 확인하실 수 있습니다.

django-project-25

4. URL경로 입력 media 파일 접근 제한

URL로 MEDIA_ROOT에 접근하여 강제적으로 파일을 다운, 이미지파일 등에 접근하는 것을 방지하기 위해 urls.py에 아래와 같이 소스를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
# notice/urls.py

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import messages
from django.shortcuts import redirect

def protected_file(request, path, document_root=None):
messages.error(request, "접근 불가")
return redirect('/')

urlpatterns += static(settings.MEDIA_URL, protected_file, document_root=settings.MEDIA_ROOT)

urlpatterns에 Media File을 제공하는 패턴을 추가하고 protected_file 함수를 구현함으로써 url로 서버상의 MEDIA_ROOT에 접근하는것을 방지합니다.

5. 결과

django-project-25

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