150 lines
6.3 KiB
Python
150 lines
6.3 KiB
Python
"""Drive navigation, folder management, metadata and trash endpoints."""
|
||
from pathlib import Path
|
||
from django.conf import settings
|
||
from django.core.exceptions import ValidationError
|
||
from django.db import transaction
|
||
from django.db.models import Q, Sum, Count
|
||
from django.utils import timezone
|
||
from rest_framework.decorators import api_view
|
||
from rest_framework.response import Response
|
||
from .models import DriveItem
|
||
from .services import error, item_info, get_item, chain, readable, writable, valid_name, target_parent, descendant_files
|
||
|
||
|
||
@api_view(['GET'])
|
||
def me(request):
|
||
return Response({'id':request.user.pk,'username':request.user.username,
|
||
'is_admin':request.user.is_staff or request.user.is_superuser,
|
||
'first_name':request.user.first_name})
|
||
|
||
|
||
@api_view(['GET'])
|
||
def listing(request):
|
||
user=request.user
|
||
view=request.query_params.get('view','files')
|
||
q=request.query_params.get('q','').strip()
|
||
parent_id=request.query_params.get('parent')
|
||
parent=None
|
||
if parent_id:
|
||
parent=get_item(parent_id)
|
||
if not parent.is_folder or not readable(parent,user):
|
||
return error('Dossier inaccessible.',403)
|
||
if view=='trash':
|
||
nodes=DriveItem.objects.filter(owner=user,trashed_at__isnull=False).select_related('owner')
|
||
elif view=='shared':
|
||
nodes=DriveItem.objects.filter(parent__isnull=True,is_shared=True,trashed_at__isnull=True).select_related('owner')
|
||
if not user.is_staff:
|
||
nodes=nodes.exclude(owner=user)
|
||
elif view=='starred':
|
||
nodes=DriveItem.objects.filter(owner=user,is_starred=True,trashed_at__isnull=True).select_related('owner','parent')
|
||
elif view=='recent':
|
||
nodes=DriveItem.objects.filter(owner=user,is_folder=False,trashed_at__isnull=True).select_related('owner','parent').order_by('-updated_at')
|
||
elif q:
|
||
nodes=DriveItem.objects.filter(owner=user,trashed_at__isnull=True,name__icontains=q).select_related('owner','parent')
|
||
elif parent:
|
||
nodes=DriveItem.objects.filter(parent=parent,trashed_at__isnull=True).select_related('owner','parent')
|
||
else:
|
||
nodes=DriveItem.objects.filter(parent__isnull=True,trashed_at__isnull=True).filter(
|
||
Q(owner=user)|Q(is_shared=True)).select_related('owner','parent')
|
||
items=[]
|
||
for n in nodes[:500]:
|
||
if view=='trash' or readable(n,user):
|
||
items.append(item_info(n))
|
||
trail=[]
|
||
if parent:
|
||
for n in reversed(chain(parent)):
|
||
if readable(n,user):
|
||
trail.append({'id':str(n.id),'name':n.name})
|
||
return Response({'items':items,'breadcrumb':trail,'limited':len(items)>=500})
|
||
|
||
|
||
@api_view(['GET'])
|
||
def stats(request):
|
||
user=request.user
|
||
result=DriveItem.objects.filter(owner=user,trashed_at__isnull=True,is_folder=False).aggregate(
|
||
count=Count('id'),total=Sum('size'))
|
||
return Response({'files':result['count'] or 0,'bytes':result['total'] or 0,
|
||
'folders':DriveItem.objects.filter(owner=user,is_folder=True,trashed_at__isnull=True).count(),
|
||
'shared':DriveItem.objects.filter(owner=user,parent__isnull=True,is_shared=True,trashed_at__isnull=True).count()})
|
||
|
||
|
||
@api_view(['POST'])
|
||
def create_folder(request):
|
||
try:
|
||
name=valid_name(request.data.get('name'))
|
||
parent=target_parent(request.data.get('parent'),request.user,request.user.pk)
|
||
except ValueError as ex: return error(str(ex))
|
||
except PermissionError as ex: return error(str(ex),403)
|
||
folder=DriveItem(owner=request.user,parent=parent,name=name,is_folder=True)
|
||
folder.full_clean()
|
||
folder.save()
|
||
return Response(item_info(folder),status=201)
|
||
|
||
|
||
|
||
|
||
@api_view(['PATCH'])
|
||
def update_item(request,item_id):
|
||
item=get_item(item_id)
|
||
if not readable(item,request.user) or not writable(item,request.user):
|
||
return error('Modification interdite.',403)
|
||
data=request.data
|
||
if 'name' in data:
|
||
try:item.name=valid_name(data['name'])
|
||
except ValueError as ex:return error(str(ex))
|
||
if 'is_starred' in data:item.is_starred=bool(data['is_starred'])
|
||
if 'is_shared' in data:
|
||
if item.parent_id:return error('Seuls les éléments à la racine peuvent être partagés.')
|
||
item.is_shared=bool(data['is_shared'])
|
||
if 'parent' in data:
|
||
try:
|
||
new_parent=target_parent(data['parent'],request.user,item.owner_id)
|
||
except ValueError as ex:return error(str(ex))
|
||
except PermissionError as ex:return error(str(ex),403)
|
||
if new_parent:
|
||
if new_parent.id==item.id or item.id in [n.id for n in chain(new_parent)]:
|
||
return error('Déplacement impossible dans un sous-dossier de cet élément.')
|
||
item.parent=new_parent
|
||
if new_parent:item.is_shared=False
|
||
try:item.full_clean()
|
||
except ValidationError as ex:return error(str(ex))
|
||
item.save()
|
||
return Response(item_info(item))
|
||
|
||
|
||
@api_view(['POST'])
|
||
def trash_item(request,item_id):
|
||
item=get_item(item_id)
|
||
if not readable(item,request.user) or not writable(item,request.user):return error('Accès refusé.',403)
|
||
if item.trashed_at:return error('Élément déjà dans la corbeille.')
|
||
item.trashed_at=timezone.now();item.save(update_fields=['trashed_at','updated_at'])
|
||
return Response({'ok':True})
|
||
|
||
|
||
@api_view(['POST'])
|
||
def restore_item(request,item_id):
|
||
item=get_item(item_id)
|
||
if not writable(item,request.user):return error('Accès refusé.',403)
|
||
if not item.trashed_at:return error('Cet élément n’est pas dans la corbeille.')
|
||
# An item whose parent was separately trashed is restored to root.
|
||
if item.parent and any(n.trashed_at for n in chain(item.parent)):
|
||
item.parent=None
|
||
item.trashed_at=None;item.save()
|
||
return Response(item_info(item))
|
||
|
||
|
||
@api_view(['DELETE'])
|
||
def delete_item(request,item_id):
|
||
item=get_item(item_id)
|
||
if not writable(item,request.user):return error('Accès refusé.',403)
|
||
if not item.trashed_at:return error('Déplace d’abord cet élément dans la corbeille.')
|
||
files=descendant_files(item)
|
||
with transaction.atomic():
|
||
item.delete()
|
||
def remove_files():
|
||
for key in files:
|
||
path=Path(settings.DRIVE_STORAGE_ROOT)/key[:2]/key
|
||
path.unlink(missing_ok=True)
|
||
transaction.on_commit(remove_files)
|
||
return Response(status=204)
|