서승완

feat: implement files api

...@@ -125,6 +125,7 @@ STATIC_URL = '/static/' ...@@ -125,6 +125,7 @@ STATIC_URL = '/static/'
125 125
126 126
127 # Custom Settings 127 # Custom Settings
128 +S3_BUCKET = 'khubox-files'
128 CDN_PATH = 'https://khubox-files.khunet.net' 129 CDN_PATH = 'https://khubox-files.khunet.net'
129 CLOUDFRONT_KEY_ID = 'APKAJ3FOBWI34OZJTXJQ' 130 CLOUDFRONT_KEY_ID = 'APKAJ3FOBWI34OZJTXJQ'
130 CLOUDFRONT_KEY_PRIVATE = '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA18VtzURs+fQev5L00LRwRbJaObQI4kfJCIsOE7eWSOqq4Akh\nA7fI6vs3z8orXBvgc+k6GgAHrIdNwckxoQuTsCxrTDm1104qy1T4JkVxkIBYHJgH\nGzKUloK5IqdmcYbOK7IQeHJ2gR9Mv/3oKUytJSsrbM9k4oLrsxGpyEuJeHIg28aP\nwhoVWmBGcPu48l4aYAZEVY7LZRJSOQ9y7Lf8FS1u7Xtw1P91gEaqrqVXqRWY02C8\nsixpJJuiAPnM3rpcpVNlAaPdDkWmaWYJoJDOlce7Dmx1a9Ckr24krM//vpEljurC\nGml0AsHpL8LE9msM5VA+miCxCz/K+wDgm2xvvQIDAQABAoIBAFmP3pLceyuJVBYK\n5smWjB+x91eKTkG2sFB2f8JZau0bUxApWeXULHa1DiaW8UaLX7BdN7vBFW5cvz7X\nx1zklEoFNghuz/btwD+kJlikbI4hZ/F+fTyh0yFiY3xp5dDrtrpWcBW+1UeleVMc\nDnjOFfSepajFsUeANlue0k2MZSRz34s2T2scV5ZkooqdXddYUF/wDhefYm6uvCgI\nPyvY/mbJTyhte/xagY/m6yzk5gxgad0qP2ZZrHhLLMlJ/GEZToWDxD2xUei61NQT\nFFc5ZutkAE6fVb3I4SJUBSX5fl0tTMz4Aak1GP2phMhjZyjYnQMq8kvL4BNFb7gp\nary8W/UCgYEA+eKkfjjlPsEx6yHMhD7pAwy/MpUqJmF5LMxIG+qfd+GZMQ2oucn5\npUAwBHP7BD6E9H7/7jdjnCiO+iPrzM9vNLfqsdCtPWzoFYJp/6Fv002uX8seNYvJ\nQyQqrM85LYIghhnkcmJMA8GR/Iu5ZEeE2BkAl9T2EKclzmB62d/ki6MCgYEA3Q0V\nz08IEwSJW+jEsOM+XGg2YkNqCVKGQD9n4CPx0TFVJxfqFl2nVwlN2hfrlJLUQ9+l\nfXnS5AW3tE88t9we+ea0saJZEqqlm/rGsfTV/twS9cWSgvG5fTzhUbu9/ElMU29L\nmydQfWTvCup7zCuQtgwM5ZRtPwuKsI8urUg6zR8CgYAt0coZvvMCI8i0dbkbkrGF\nNqQkcUeOTBc9CKQ8QjRFdh9x6DBFCOz2ySNE3cNsTs5wSo1BL/Ta4HD/GvEU2ABr\nKUImor3xYnPX5dbr4b0wgLD1rbf3V49q+Um98C1q086E6GCEPNP1aFwNc81lvtt0\nCHmcXZdVDGEZS4WbR7uPgwKBgBO/moY12lPQoPDsH75p3uVkjg9DVJLWo5XT1FTr\nASyeSqw+b7Rl05BsDV+BqZNRdtNFhMRsANJMTHg4aAVJDh9nZBdGmMyZIEiKI/w8\nEm49fRgl+YvnSpoMuViS/EswxTfjBo8q+P7q6IxCHKNF9Ry+gNx14TizsEVL1XC3\ntkEjAoGBAMyp7wdPobJMXcclRVq6rqHs9OMcnZAveVKyxNgDbZu4OB5X4xTxGEYT\nNZQ0MFf/HcwlnH7797gVQeqF9dlqUJYe+Fc8lc/Rcwta/4R5uMgri9t8RKN91YKF\nUUFBsDEkWlkoAmfPkcrrq9cLJlmSNt3ehQj4p5iAJwoVBXXa++PO\n-----END RSA PRIVATE KEY-----' 131 CLOUDFRONT_KEY_PRIVATE = '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA18VtzURs+fQev5L00LRwRbJaObQI4kfJCIsOE7eWSOqq4Akh\nA7fI6vs3z8orXBvgc+k6GgAHrIdNwckxoQuTsCxrTDm1104qy1T4JkVxkIBYHJgH\nGzKUloK5IqdmcYbOK7IQeHJ2gR9Mv/3oKUytJSsrbM9k4oLrsxGpyEuJeHIg28aP\nwhoVWmBGcPu48l4aYAZEVY7LZRJSOQ9y7Lf8FS1u7Xtw1P91gEaqrqVXqRWY02C8\nsixpJJuiAPnM3rpcpVNlAaPdDkWmaWYJoJDOlce7Dmx1a9Ckr24krM//vpEljurC\nGml0AsHpL8LE9msM5VA+miCxCz/K+wDgm2xvvQIDAQABAoIBAFmP3pLceyuJVBYK\n5smWjB+x91eKTkG2sFB2f8JZau0bUxApWeXULHa1DiaW8UaLX7BdN7vBFW5cvz7X\nx1zklEoFNghuz/btwD+kJlikbI4hZ/F+fTyh0yFiY3xp5dDrtrpWcBW+1UeleVMc\nDnjOFfSepajFsUeANlue0k2MZSRz34s2T2scV5ZkooqdXddYUF/wDhefYm6uvCgI\nPyvY/mbJTyhte/xagY/m6yzk5gxgad0qP2ZZrHhLLMlJ/GEZToWDxD2xUei61NQT\nFFc5ZutkAE6fVb3I4SJUBSX5fl0tTMz4Aak1GP2phMhjZyjYnQMq8kvL4BNFb7gp\nary8W/UCgYEA+eKkfjjlPsEx6yHMhD7pAwy/MpUqJmF5LMxIG+qfd+GZMQ2oucn5\npUAwBHP7BD6E9H7/7jdjnCiO+iPrzM9vNLfqsdCtPWzoFYJp/6Fv002uX8seNYvJ\nQyQqrM85LYIghhnkcmJMA8GR/Iu5ZEeE2BkAl9T2EKclzmB62d/ki6MCgYEA3Q0V\nz08IEwSJW+jEsOM+XGg2YkNqCVKGQD9n4CPx0TFVJxfqFl2nVwlN2hfrlJLUQ9+l\nfXnS5AW3tE88t9we+ea0saJZEqqlm/rGsfTV/twS9cWSgvG5fTzhUbu9/ElMU29L\nmydQfWTvCup7zCuQtgwM5ZRtPwuKsI8urUg6zR8CgYAt0coZvvMCI8i0dbkbkrGF\nNqQkcUeOTBc9CKQ8QjRFdh9x6DBFCOz2ySNE3cNsTs5wSo1BL/Ta4HD/GvEU2ABr\nKUImor3xYnPX5dbr4b0wgLD1rbf3V49q+Um98C1q086E6GCEPNP1aFwNc81lvtt0\nCHmcXZdVDGEZS4WbR7uPgwKBgBO/moY12lPQoPDsH75p3uVkjg9DVJLWo5XT1FTr\nASyeSqw+b7Rl05BsDV+BqZNRdtNFhMRsANJMTHg4aAVJDh9nZBdGmMyZIEiKI/w8\nEm49fRgl+YvnSpoMuViS/EswxTfjBo8q+P7q6IxCHKNF9Ry+gNx14TizsEVL1XC3\ntkEjAoGBAMyp7wdPobJMXcclRVq6rqHs9OMcnZAveVKyxNgDbZu4OB5X4xTxGEYT\nNZQ0MFf/HcwlnH7797gVQeqF9dlqUJYe+Fc8lc/Rcwta/4R5uMgri9t8RKN91YKF\nUUFBsDEkWlkoAmfPkcrrq9cLJlmSNt3ehQj4p5iAJwoVBXXa++PO\n-----END RSA PRIVATE KEY-----'
......
1 +import boto3
1 import datetime 2 import datetime
2 from botocore.signers import CloudFrontSigner 3 from botocore.signers import CloudFrontSigner
3 from cryptography.hazmat.backends import default_backend 4 from cryptography.hazmat.backends import default_backend
...@@ -15,8 +16,39 @@ def rsa_signer(message): ...@@ -15,8 +16,39 @@ def rsa_signer(message):
15 return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1()) 16 return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())
16 17
17 18
18 -def sign(url): 19 +def sign_download(url):
19 - expire_date = datetime.datetime(2020, 6, 10) 20 + expire_date = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
20 cloudfront_signer = CloudFrontSigner(settings.CLOUDFRONT_KEY_ID, rsa_signer) 21 cloudfront_signer = CloudFrontSigner(settings.CLOUDFRONT_KEY_ID, rsa_signer)
21 signed_url = cloudfront_signer.generate_presigned_url(url, date_less_than=expire_date) 22 signed_url = cloudfront_signer.generate_presigned_url(url, date_less_than=expire_date)
22 return signed_url 23 return signed_url
24 +
25 +
26 +def sign_upload(file_id):
27 + s3 = boto3.client('s3')
28 + signed_url = s3.generate_presigned_url(
29 + 'put_object',
30 + Params={'Bucket': settings.S3_BUCKET, 'Key': file_id},
31 + ExpiresIn=3600,
32 + HttpMethod='PUT'
33 + )
34 + return signed_url
35 +
36 +
37 +def s3_delete(del_list):
38 + s3 = boto3.resource('s3')
39 + bucket = s3.Bucket(settings.S3_BUCKET)
40 + del_s3_list = []
41 + for key in del_list:
42 + del_s3_list.append({'Key': key})
43 + bucket.delete_objects(Delete={'Objects': del_s3_list})
44 +
45 +
46 +def s3_copy(file_id, new_file_id):
47 + s3 = boto3.resource('s3')
48 + bucket = s3.Bucket(settings.S3_BUCKET)
49 + copy_source = {
50 + 'Bucket': settings.S3_BUCKET,
51 + 'Key': file_id
52 + }
53 + obj = bucket.Object(str(new_file_id))
54 + obj.copy(copy_source)
......
...@@ -5,7 +5,7 @@ from . import files, groups, users ...@@ -5,7 +5,7 @@ from . import files, groups, users
5 urlpatterns = [ 5 urlpatterns = [
6 url(r'^files$', files.index), # 폴더 생성, 파일 업로드, 폴더/파일 목록 6 url(r'^files$', files.index), # 폴더 생성, 파일 업로드, 폴더/파일 목록
7 url(r'^files/trash$', files.trash), # 휴지통 비우기 7 url(r'^files/trash$', files.trash), # 휴지통 비우기
8 - url(r'^files/(?P<file_id>[-\w]+)$', files.item), # 폴더/파일 조회, 파일 다운로드, 폴더/파일 수정 8 + url(r'^files/(?P<file_id>[-\w]+)$', files.item), # 폴더/파일 조회, 폴더/파일 수정
9 url(r'^files/(?P<file_id>[-\w]+)/copy$', files.copy), # 파일 복제 9 url(r'^files/(?P<file_id>[-\w]+)/copy$', files.copy), # 파일 복제
10 url(r'^groups$', groups.index), # 그룹 생성 10 url(r'^groups$', groups.index), # 그룹 생성
11 url(r'^groups/invite/(?P<invite_code>[-\w]+)$', groups.invite), # 그룹 초대장 조회, 그룹 초대장 사용 11 url(r'^groups/invite/(?P<invite_code>[-\w]+)$', groups.invite), # 그룹 초대장 조회, 그룹 초대장 사용
......
...@@ -20,7 +20,7 @@ def trash(request): ...@@ -20,7 +20,7 @@ def trash(request):
20 20
21 21
22 def item(request, file_id): 22 def item(request, file_id):
23 - # 폴더/파일 조회, 파일 다운로드 23 + # 폴더/파일 조회
24 if request.method == 'GET': 24 if request.method == 'GET':
25 return JsonResponse(files.find_item(request, file_id)) 25 return JsonResponse(files.find_item(request, file_id))
26 # 폴더/파일 수정 26 # 폴더/파일 수정
......
...@@ -26,7 +26,7 @@ class Migration(migrations.Migration): ...@@ -26,7 +26,7 @@ class Migration(migrations.Migration):
26 ('size', models.BigIntegerField()), 26 ('size', models.BigIntegerField()),
27 ('is_public', models.IntegerField(default=0)), 27 ('is_public', models.IntegerField(default=0)),
28 ('is_starred', models.IntegerField(default=0)), 28 ('is_starred', models.IntegerField(default=0)),
29 - ('is_trahsed', models.IntegerField(default=0)), 29 + ('is_trashed', models.IntegerField(default=0)),
30 ('created_at', models.DateTimeField()), 30 ('created_at', models.DateTimeField()),
31 ('deleted_at', models.DateTimeField(blank=True, null=True)), 31 ('deleted_at', models.DateTimeField(blank=True, null=True)),
32 ], 32 ],
......
...@@ -12,7 +12,7 @@ class File(models.Model): ...@@ -12,7 +12,7 @@ class File(models.Model):
12 size = models.BigIntegerField() 12 size = models.BigIntegerField()
13 is_public = models.IntegerField(default=0) 13 is_public = models.IntegerField(default=0)
14 is_starred = models.IntegerField(default=0) 14 is_starred = models.IntegerField(default=0)
15 - is_trahsed = models.IntegerField(default=0) 15 + is_trashed = models.IntegerField(default=0)
16 created_at = models.DateTimeField() 16 created_at = models.DateTimeField()
17 deleted_at = models.DateTimeField(blank=True, null=True) 17 deleted_at = models.DateTimeField(blank=True, null=True)
18 18
......
1 import json 1 import json
2 import uuid 2 import uuid
3 +from django.conf import settings
3 from django.utils import timezone 4 from django.utils import timezone
4 -from ..aws import sign 5 +from ..aws import sign_upload, sign_download, s3_copy, s3_delete
5 from ..models import File, GroupUser 6 from ..models import File, GroupUser
6 7
7 8
8 -# TODO: 폴더/파일 목록 9 +# 폴더/파일 목록
9 def list_item(request): 10 def list_item(request):
10 - return {'result': True} 11 + # TODO: Auth
12 + request.user_id = 1
13 +
14 + # Validate
15 + if request.GET.get('is_public') != 'true' \
16 + and request.GET.get('is_starred') != 'true' \
17 + and request.GET.get('is_trashed') != 'true':
18 + return {'result': False, 'error': '입력이 누락되었습니다.'}
19 +
20 + # Query Files
21 + files = None
22 + if request.GET.get('is_public') == 'true':
23 + files = File.objects.filter(owner_user_id=request.user_id, is_public=1, deleted_at__isnull=True)
24 + elif request.GET.get('is_starred') == 'true':
25 + files = File.objects.filter(owner_user_id=request.user_id, is_starred=1, deleted_at__isnull=True)
26 + elif request.GET.get('is_trashed') == 'true':
27 + files = File.objects.filter(owner_user_id=request.user_id, is_trashed=1, deleted_at__isnull=True)
28 +
29 + # Structure
30 + data = []
31 + for file in files:
32 + data.append({
33 + 'id': file.id,
34 + 'type': file.type,
35 + 'name': file.name,
36 + 'size': file.size,
37 + 'is_public': file.is_public,
38 + 'is_starred': file.is_starred,
39 + 'is_trashed': file.is_trashed,
40 + 'created_at': file.created_at,
41 + })
42 +
43 + return {'result': True, 'data': data}
11 44
12 45
13 # 폴더 생성, 파일 업로드 46 # 폴더 생성, 파일 업로드
...@@ -16,7 +49,10 @@ def create(request): ...@@ -16,7 +49,10 @@ def create(request):
16 request.user_id = 1 49 request.user_id = 1
17 50
18 # Load 51 # Load
52 + try:
19 received = json.loads(request.body.decode('utf-8')) 53 received = json.loads(request.body.decode('utf-8'))
54 + except json.decoder.JSONDecodeError:
55 + return {'result': False, 'error': '입력이 잘못되었습니다.'}
20 56
21 # Validate 57 # Validate
22 if 'parent_id' not in received \ 58 if 'parent_id' not in received \
...@@ -28,7 +64,7 @@ def create(request): ...@@ -28,7 +64,7 @@ def create(request):
28 return {'result': False, 'error': '입력이 잘못되었습니다.'} 64 return {'result': False, 'error': '입력이 잘못되었습니다.'}
29 65
30 # Get Parent 66 # Get Parent
31 - parent = File.objects.filter(id=received['parent_id'], is_trahsed=0, deleted_at__isnull=True) 67 + parent = File.objects.filter(id=received['parent_id'], is_trashed=0, deleted_at__isnull=True)
32 68
33 # Check Exists 69 # Check Exists
34 if len(parent) == 0: 70 if len(parent) == 0:
...@@ -63,18 +99,17 @@ def create(request): ...@@ -63,18 +99,17 @@ def create(request):
63 return {'result': True, 'file_id': file_id} 99 return {'result': True, 'file_id': file_id}
64 100
65 # Return File 101 # Return File
66 - upload_url = 'https://khubox-files.khunet.net/%s' % file_id 102 + upload_url = sign_upload(str(file_id))
67 - upload_url = sign(upload_url)
68 return {'result': True, 'file_id': file_id, 'upload_url': upload_url} 103 return {'result': True, 'file_id': file_id, 'upload_url': upload_url}
69 104
70 105
71 -# TODO: 휴지통 비우기 106 +# 휴지통 비우기
72 def empty_trash(request): 107 def empty_trash(request):
73 # TODO: Auth 108 # TODO: Auth
74 request.user_id = 1 109 request.user_id = 1
75 110
76 # Query Files 111 # Query Files
77 - files = File.objects.filter(owner_user_id=request.user_id, is_trahsed=1, deleted_at__isnull=True) 112 + files = File.objects.filter(owner_user_id=request.user_id, is_trashed=1, deleted_at__isnull=True)
78 113
79 # First Depth 114 # First Depth
80 del_list = [] 115 del_list = []
...@@ -92,24 +127,201 @@ def empty_trash(request): ...@@ -92,24 +127,201 @@ def empty_trash(request):
92 for del_file in child_files: 127 for del_file in child_files:
93 del_check.append(del_file.id) 128 del_check.append(del_file.id)
94 129
95 - # TODO: S3 Delete 130 + # S3 Delete
131 + s3_delete(del_list)
96 132
97 # Update 133 # Update
98 - File.objects.filter(id__in=del_list).update(is_trahsed=1, deleted_at=timezone.now()) 134 + File.objects.filter(id__in=del_list).update(is_trashed=1, deleted_at=timezone.now())
99 135
100 return {'result': True, 'affected': del_list} 136 return {'result': True, 'affected': del_list}
101 137
102 138
103 -# TODO: 폴더/파일 조회, 파일 다운로드 139 +# 폴더/파일 조회
104 def find_item(request, file_id): 140 def find_item(request, file_id):
105 - return {'result': True} 141 + # TODO: Auth
142 + request.user_id = 1
143 +
144 + # Query
145 + file = File.objects.filter(id=file_id, deleted_at__isnull=True)
146 +
147 + # Check Exists
148 + if len(file) == 0:
149 + return {'result': False, 'error': '잘못된 요청입니다.'}
106 150
151 + # Check Owner
152 + is_auth = False
153 + if file[0].owner_user_id == request.user_id:
154 + is_auth = True
155 + is_my_group = GroupUser.objects.filter(group_id=file[0].owner_group_id, user_id=request.user_id)
156 + if len(is_my_group) != 0:
157 + is_auth = True
107 158
108 -# TODO: 폴더/파일 수정 159 + # Check Public
160 + if file[0].is_public == 1:
161 + is_auth = True
162 + parent_id = file[0].parent_id
163 + while True:
164 + if parent_id is None or is_auth:
165 + break
166 + parent_file = File.objects.filter(id=parent_id)
167 + if parent_file[0].is_public == 1:
168 + is_auth = True
169 + parent_id = parent_file[0].parent_id
170 +
171 + # Check Auth
172 + if is_auth is False:
173 + return {'result': False, 'error': '잘못된 요청입니다.'}
174 +
175 + # Return File
176 + if file[0].type == 'file':
177 + download_url = '%s/%s' % (settings.CDN_PATH, file[0].id)
178 + download_url = sign_download(download_url)
179 + data = {
180 + 'id': file[0].id,
181 + 'parent_id': file[0].parent_id,
182 + 'uploader_id': file[0].uploader_id,
183 + 'name': file[0].name,
184 + 'size': file[0].size,
185 + 'is_public': file[0].is_public,
186 + 'is_starred': file[0].is_starred,
187 + 'is_trashed': file[0].is_trashed,
188 + 'created_at': file[0].created_at,
189 + 'download_url': download_url,
190 + }
191 + return {'result': True, 'data': data}
192 +
193 + # Query
194 + files = File.objects.filter(parent_id=file[0].id, is_trashed=0, deleted_at__isnull=True)
195 +
196 + # Structure
197 + data = []
198 + for file in files:
199 + data.append({
200 + 'id': file.id,
201 + 'type': file.type,
202 + 'name': file.name,
203 + 'size': file.size,
204 + 'is_public': file.is_public,
205 + 'is_starred': file.is_starred,
206 + 'is_trashed': file.is_trashed,
207 + 'created_at': file.created_at,
208 + })
209 +
210 + # Return Folder
211 + return {'result': True, 'data': data}
212 +
213 +
214 +# 폴더/파일 수정
109 def update_item(request, file_id): 215 def update_item(request, file_id):
216 + # TODO: Auth
217 + request.user_id = 1
218 +
219 + # Load
220 + try:
221 + received = json.loads(request.body.decode('utf-8'))
222 + except json.decoder.JSONDecodeError:
223 + return {'result': False, 'error': '입력이 잘못되었습니다.'}
224 +
225 + # Validate
226 + if 'name' not in received \
227 + and 'parent_id' not in received \
228 + and 'is_public' not in received \
229 + and 'is_starred' not in received \
230 + and 'is_trashed' not in received:
231 + return {'result': False, 'error': '입력이 누락되었습니다.'}
232 +
233 + # Query
234 + file = File.objects.filter(id=file_id, deleted_at__isnull=True)
235 +
236 + # Check Exists
237 + if len(file) == 0:
238 + return {'result': False, 'error': '잘못된 요청입니다.'}
239 +
240 + # Check Owner
241 + is_auth = False
242 + if file[0].owner_user_id == request.user_id:
243 + is_auth = True
244 + is_my_group = GroupUser.objects.filter(group_id=file[0].owner_group_id, user_id=request.user_id)
245 + if len(is_my_group) != 0 \
246 + and 'is_public' not in received \
247 + and 'is_starred' not in received \
248 + and 'is_trashed' not in received:
249 + is_auth = True
250 +
251 + # Check Parent
252 + if 'parent_id' in received:
253 + parent = File.objects.filter(id=received['parent_id'], type='folder', deleted_at__isnull=True)
254 + if len(parent) == 0:
255 + return {'result': False, 'error': '잘못된 요청입니다.'}
256 + if (is_auth is True or len(is_my_group) != 0) \
257 + and parent[0].owner_user_id == file[0].owner_user_id \
258 + and parent[0].owner_group_id == file[0].owner_group_id \
259 + and file_id != received['parent_id']:
260 + is_auth = True
261 + else:
262 + is_auth = False
263 +
264 + # Check Auth
265 + if is_auth is False:
266 + return {'result': False, 'error': '잘못된 요청입니다.'}
267 +
268 + # Update
269 + if 'name' in received:
270 + file[0].name = received['name']
271 + if 'parent_id' in received:
272 + file[0].parent_id = received['parent_id']
273 + if 'is_public' in received:
274 + file[0].is_public = 1 if received['is_public'] is True else 0
275 + if 'is_starred' in received:
276 + file[0].is_starred = 1 if received['is_starred'] is True else 0
277 + if 'is_trashed' in received:
278 + if file[0].parent_id is None:
279 + return {'result': False, 'error': '잘못된 요청입니다.'}
280 + file[0].is_trashed = 1 if received['is_trashed'] is True else 0
281 + file[0].save()
282 +
110 return {'result': True} 283 return {'result': True}
111 284
112 285
113 -# TODO: 파일 복제 286 +# 파일 복제
114 def copy(request, file_id): 287 def copy(request, file_id):
115 - return {'result': True} 288 + # TODO: Auth
289 + request.user_id = 1
290 +
291 + # Get File
292 + file = File.objects.filter(id=file_id, type='file', is_trashed=0, deleted_at__isnull=True)
293 +
294 + # Check Exists
295 + if len(file) == 0:
296 + return {'result': False, 'error': '잘못된 요청입니다.'}
297 +
298 + # Check Owner
299 + is_auth = False
300 + if file[0].owner_user_id == request.user_id:
301 + is_auth = True
302 + is_my_group = GroupUser.objects.filter(group_id=file[0].owner_group_id, user_id=request.user_id)
303 + if len(is_my_group) != 0:
304 + is_auth = True
305 + if is_auth is False:
306 + return {'result': False, 'error': '경로가 잘못되었습니다.'}
307 +
308 + # Create UUID
309 + new_file_id = uuid.uuid4()
310 +
311 + # S3 Copy
312 + s3_copy(file_id, new_file_id)
313 +
314 + # Create
315 + File.objects.create(
316 + id=new_file_id,
317 + parent_id=file[0].parent_id,
318 + owner_user_id=file[0].owner_user_id,
319 + owner_group_id=file[0].owner_group_id,
320 + uploader_id=request.user_id,
321 + type=file[0].type,
322 + name='%s의 사본' % file[0].name,
323 + size=file[0].size,
324 + created_at=timezone.now()
325 + )
326 +
327 + return {'result': True, 'file_id': file_id}
......