서승완

feat: implement files api

......@@ -125,6 +125,7 @@ STATIC_URL = '/static/'
# Custom Settings
S3_BUCKET = 'khubox-files'
CDN_PATH = 'https://khubox-files.khunet.net'
CLOUDFRONT_KEY_ID = 'APKAJ3FOBWI34OZJTXJQ'
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-----'
......
import boto3
import datetime
from botocore.signers import CloudFrontSigner
from cryptography.hazmat.backends import default_backend
......@@ -15,8 +16,39 @@ def rsa_signer(message):
return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())
def sign(url):
expire_date = datetime.datetime(2020, 6, 10)
def sign_download(url):
expire_date = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
cloudfront_signer = CloudFrontSigner(settings.CLOUDFRONT_KEY_ID, rsa_signer)
signed_url = cloudfront_signer.generate_presigned_url(url, date_less_than=expire_date)
return signed_url
def sign_upload(file_id):
s3 = boto3.client('s3')
signed_url = s3.generate_presigned_url(
'put_object',
Params={'Bucket': settings.S3_BUCKET, 'Key': file_id},
ExpiresIn=3600,
HttpMethod='PUT'
)
return signed_url
def s3_delete(del_list):
s3 = boto3.resource('s3')
bucket = s3.Bucket(settings.S3_BUCKET)
del_s3_list = []
for key in del_list:
del_s3_list.append({'Key': key})
bucket.delete_objects(Delete={'Objects': del_s3_list})
def s3_copy(file_id, new_file_id):
s3 = boto3.resource('s3')
bucket = s3.Bucket(settings.S3_BUCKET)
copy_source = {
'Bucket': settings.S3_BUCKET,
'Key': file_id
}
obj = bucket.Object(str(new_file_id))
obj.copy(copy_source)
......
......@@ -5,7 +5,7 @@ from . import files, groups, users
urlpatterns = [
url(r'^files$', files.index), # 폴더 생성, 파일 업로드, 폴더/파일 목록
url(r'^files/trash$', files.trash), # 휴지통 비우기
url(r'^files/(?P<file_id>[-\w]+)$', files.item), # 폴더/파일 조회, 파일 다운로드, 폴더/파일 수정
url(r'^files/(?P<file_id>[-\w]+)$', files.item), # 폴더/파일 조회, 폴더/파일 수정
url(r'^files/(?P<file_id>[-\w]+)/copy$', files.copy), # 파일 복제
url(r'^groups$', groups.index), # 그룹 생성
url(r'^groups/invite/(?P<invite_code>[-\w]+)$', groups.invite), # 그룹 초대장 조회, 그룹 초대장 사용
......
......@@ -20,7 +20,7 @@ def trash(request):
def item(request, file_id):
# 폴더/파일 조회, 파일 다운로드
# 폴더/파일 조회
if request.method == 'GET':
return JsonResponse(files.find_item(request, file_id))
# 폴더/파일 수정
......
......@@ -26,7 +26,7 @@ class Migration(migrations.Migration):
('size', models.BigIntegerField()),
('is_public', models.IntegerField(default=0)),
('is_starred', models.IntegerField(default=0)),
('is_trahsed', models.IntegerField(default=0)),
('is_trashed', models.IntegerField(default=0)),
('created_at', models.DateTimeField()),
('deleted_at', models.DateTimeField(blank=True, null=True)),
],
......
......@@ -12,7 +12,7 @@ class File(models.Model):
size = models.BigIntegerField()
is_public = models.IntegerField(default=0)
is_starred = models.IntegerField(default=0)
is_trahsed = models.IntegerField(default=0)
is_trashed = models.IntegerField(default=0)
created_at = models.DateTimeField()
deleted_at = models.DateTimeField(blank=True, null=True)
......
import json
import uuid
from django.conf import settings
from django.utils import timezone
from ..aws import sign
from ..aws import sign_upload, sign_download, s3_copy, s3_delete
from ..models import File, GroupUser
# TODO: 폴더/파일 목록
# 폴더/파일 목록
def list_item(request):
return {'result': True}
# TODO: Auth
request.user_id = 1
# Validate
if request.GET.get('is_public') != 'true' \
and request.GET.get('is_starred') != 'true' \
and request.GET.get('is_trashed') != 'true':
return {'result': False, 'error': '입력이 누락되었습니다.'}
# Query Files
files = None
if request.GET.get('is_public') == 'true':
files = File.objects.filter(owner_user_id=request.user_id, is_public=1, deleted_at__isnull=True)
elif request.GET.get('is_starred') == 'true':
files = File.objects.filter(owner_user_id=request.user_id, is_starred=1, deleted_at__isnull=True)
elif request.GET.get('is_trashed') == 'true':
files = File.objects.filter(owner_user_id=request.user_id, is_trashed=1, deleted_at__isnull=True)
# Structure
data = []
for file in files:
data.append({
'id': file.id,
'type': file.type,
'name': file.name,
'size': file.size,
'is_public': file.is_public,
'is_starred': file.is_starred,
'is_trashed': file.is_trashed,
'created_at': file.created_at,
})
return {'result': True, 'data': data}
# 폴더 생성, 파일 업로드
......@@ -16,7 +49,10 @@ def create(request):
request.user_id = 1
# Load
received = json.loads(request.body.decode('utf-8'))
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
# Validate
if 'parent_id' not in received \
......@@ -28,7 +64,7 @@ def create(request):
return {'result': False, 'error': '입력이 잘못되었습니다.'}
# Get Parent
parent = File.objects.filter(id=received['parent_id'], is_trahsed=0, deleted_at__isnull=True)
parent = File.objects.filter(id=received['parent_id'], is_trashed=0, deleted_at__isnull=True)
# Check Exists
if len(parent) == 0:
......@@ -63,18 +99,17 @@ def create(request):
return {'result': True, 'file_id': file_id}
# Return File
upload_url = 'https://khubox-files.khunet.net/%s' % file_id
upload_url = sign(upload_url)
upload_url = sign_upload(str(file_id))
return {'result': True, 'file_id': file_id, 'upload_url': upload_url}
# TODO: 휴지통 비우기
# 휴지통 비우기
def empty_trash(request):
# TODO: Auth
request.user_id = 1
# Query Files
files = File.objects.filter(owner_user_id=request.user_id, is_trahsed=1, deleted_at__isnull=True)
files = File.objects.filter(owner_user_id=request.user_id, is_trashed=1, deleted_at__isnull=True)
# First Depth
del_list = []
......@@ -92,24 +127,201 @@ def empty_trash(request):
for del_file in child_files:
del_check.append(del_file.id)
# TODO: S3 Delete
# S3 Delete
s3_delete(del_list)
# Update
File.objects.filter(id__in=del_list).update(is_trahsed=1, deleted_at=timezone.now())
File.objects.filter(id__in=del_list).update(is_trashed=1, deleted_at=timezone.now())
return {'result': True, 'affected': del_list}
# TODO: 폴더/파일 조회, 파일 다운로드
# 폴더/파일 조회
def find_item(request, file_id):
return {'result': True}
# TODO: Auth
request.user_id = 1
# Query
file = File.objects.filter(id=file_id, deleted_at__isnull=True)
# Check Exists
if len(file) == 0:
return {'result': False, 'error': '잘못된 요청입니다.'}
# Check Owner
is_auth = False
if file[0].owner_user_id == request.user_id:
is_auth = True
is_my_group = GroupUser.objects.filter(group_id=file[0].owner_group_id, user_id=request.user_id)
if len(is_my_group) != 0:
is_auth = True
# Check Public
if file[0].is_public == 1:
is_auth = True
parent_id = file[0].parent_id
while True:
if parent_id is None or is_auth:
break
parent_file = File.objects.filter(id=parent_id)
if parent_file[0].is_public == 1:
is_auth = True
parent_id = parent_file[0].parent_id
# Check Auth
if is_auth is False:
return {'result': False, 'error': '잘못된 요청입니다.'}
# Return File
if file[0].type == 'file':
download_url = '%s/%s' % (settings.CDN_PATH, file[0].id)
download_url = sign_download(download_url)
data = {
'id': file[0].id,
'parent_id': file[0].parent_id,
'uploader_id': file[0].uploader_id,
'name': file[0].name,
'size': file[0].size,
'is_public': file[0].is_public,
'is_starred': file[0].is_starred,
'is_trashed': file[0].is_trashed,
'created_at': file[0].created_at,
'download_url': download_url,
}
return {'result': True, 'data': data}
# Query
files = File.objects.filter(parent_id=file[0].id, is_trashed=0, deleted_at__isnull=True)
# Structure
data = []
for file in files:
data.append({
'id': file.id,
'type': file.type,
'name': file.name,
'size': file.size,
'is_public': file.is_public,
'is_starred': file.is_starred,
'is_trashed': file.is_trashed,
'created_at': file.created_at,
})
# Return Folder
return {'result': True, 'data': data}
# TODO: 폴더/파일 수정
# 폴더/파일 수정
def update_item(request, file_id):
# TODO: Auth
request.user_id = 1
# Load
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
# Validate
if 'name' not in received \
and 'parent_id' not in received \
and 'is_public' not in received \
and 'is_starred' not in received \
and 'is_trashed' not in received:
return {'result': False, 'error': '입력이 누락되었습니다.'}
# Query
file = File.objects.filter(id=file_id, deleted_at__isnull=True)
# Check Exists
if len(file) == 0:
return {'result': False, 'error': '잘못된 요청입니다.'}
# Check Owner
is_auth = False
if file[0].owner_user_id == request.user_id:
is_auth = True
is_my_group = GroupUser.objects.filter(group_id=file[0].owner_group_id, user_id=request.user_id)
if len(is_my_group) != 0 \
and 'is_public' not in received \
and 'is_starred' not in received \
and 'is_trashed' not in received:
is_auth = True
# Check Parent
if 'parent_id' in received:
parent = File.objects.filter(id=received['parent_id'], type='folder', deleted_at__isnull=True)
if len(parent) == 0:
return {'result': False, 'error': '잘못된 요청입니다.'}
if (is_auth is True or len(is_my_group) != 0) \
and parent[0].owner_user_id == file[0].owner_user_id \
and parent[0].owner_group_id == file[0].owner_group_id \
and file_id != received['parent_id']:
is_auth = True
else:
is_auth = False
# Check Auth
if is_auth is False:
return {'result': False, 'error': '잘못된 요청입니다.'}
# Update
if 'name' in received:
file[0].name = received['name']
if 'parent_id' in received:
file[0].parent_id = received['parent_id']
if 'is_public' in received:
file[0].is_public = 1 if received['is_public'] is True else 0
if 'is_starred' in received:
file[0].is_starred = 1 if received['is_starred'] is True else 0
if 'is_trashed' in received:
if file[0].parent_id is None:
return {'result': False, 'error': '잘못된 요청입니다.'}
file[0].is_trashed = 1 if received['is_trashed'] is True else 0
file[0].save()
return {'result': True}
# TODO: 파일 복제
# 파일 복제
def copy(request, file_id):
return {'result': True}
# TODO: Auth
request.user_id = 1
# Get File
file = File.objects.filter(id=file_id, type='file', is_trashed=0, deleted_at__isnull=True)
# Check Exists
if len(file) == 0:
return {'result': False, 'error': '잘못된 요청입니다.'}
# Check Owner
is_auth = False
if file[0].owner_user_id == request.user_id:
is_auth = True
is_my_group = GroupUser.objects.filter(group_id=file[0].owner_group_id, user_id=request.user_id)
if len(is_my_group) != 0:
is_auth = True
if is_auth is False:
return {'result': False, 'error': '경로가 잘못되었습니다.'}
# Create UUID
new_file_id = uuid.uuid4()
# S3 Copy
s3_copy(file_id, new_file_id)
# Create
File.objects.create(
id=new_file_id,
parent_id=file[0].parent_id,
owner_user_id=file[0].owner_user_id,
owner_group_id=file[0].owner_group_id,
uploader_id=request.user_id,
type=file[0].type,
name='%s의 사본' % file[0].name,
size=file[0].size,
created_at=timezone.now()
)
return {'result': True, 'file_id': file_id}
......