diff --git a/etebase_fastapi/collection.py b/etebase_fastapi/collection.py index 77abe0f..0ceb988 100644 --- a/etebase_fastapi/collection.py +++ b/etebase_fastapi/collection.py @@ -7,8 +7,8 @@ from django.core.files.base import ContentFile from django.db import transaction from django.db.models import Q from django.db.models import QuerySet -from fastapi import APIRouter, Depends, status -from pydantic import BaseModel +from fastapi import APIRouter, Depends, status, Query +from pydantic import BaseModel, Field from django_etebase import models from django_etebase.models import Collection, AccessLevels, CollectionMember @@ -22,12 +22,57 @@ collection_router = APIRouter(route_class=MsgpackRoute) default_queryset = Collection.objects.all() +Prefetch = t.Literal["auto", "medium"] +PrefetchQuery = Query(default="auto") + + class ListMulti(BaseModel): collectionTypes: t.List[bytes] +class CollectionItemRevisionOut(BaseModel): + uid: str + meta: bytes + deleted: bool + chunks: t.List[t.Tuple[str, t.Optional[bytes]]] + + class Config: + orm_mode = True + + @classmethod + def from_orm_user( + cls: t.Type["CollectionItemRevisionOut"], obj: models.CollectionItemRevision, prefetch: Prefetch + ) -> "CollectionItemRevisionOut": + chunk_obj = obj.chunks_relation.get().chunk + if prefetch == "auto": + with open(chunk_obj.chunkFile.path, "rb") as f: + chunks = chunk_obj.uid, f.read() + else: + chunks = (chunk_obj.uid,) + return cls(uid=obj.uid, meta=obj.meta, deleted=obj.deleted, chunks=[chunks]) + + class CollectionItemOut(BaseModel): uid: str + version: int + encryptionKey: t.Optional[bytes] + etag: t.Optional[str] + content: CollectionItemRevisionOut + + class Config: + orm_mode = True + + @classmethod + def from_orm_user( + cls: t.Type["CollectionItemOut"], obj: models.CollectionItem, prefetch: Prefetch + ) -> "CollectionItemOut": + return cls( + uid=obj.uid, + version=obj.version, + encryptionKey=obj.encryptionKey, + etag=obj.etag, + content=CollectionItemRevisionOut.from_orm_user(obj.content, prefetch), + ) class CollectionOut(BaseModel): @@ -38,16 +83,17 @@ class CollectionOut(BaseModel): item: CollectionItemOut @classmethod - def from_orm_user(cls: t.Type["CollectionOut"], obj: Collection, user: User) -> "CollectionOut": + def from_orm_user(cls: t.Type["CollectionOut"], obj: Collection, user: User, prefetch: Prefetch) -> "CollectionOut": member: CollectionMember = obj.members.get(user=user) collection_type = member.collectionType - return cls( + ret = cls( collectionType=collection_type and collection_type.uid, collectionKey=member.encryptionKey, accessLevel=member.accessLevel, stoken=obj.stoken, - item=CollectionItemOut(uid=obj.main_item.uid), + item=CollectionItemOut.from_orm_user(obj.main_item, prefetch), ) + return ret class ListResponse(BaseModel): @@ -56,11 +102,26 @@ class ListResponse(BaseModel): done: bool +class ItemIn(BaseModel): + uid: str + version: int + etag: t.Optional[str] + content: CollectionItemRevisionOut + + +class CollectionIn(BaseModel): + collectionType: bytes + collectionKey: bytes + item: ItemIn + + @sync_to_async -def list_common(queryset: QuerySet, user: User, stoken: t.Optional[str], limit: int) -> MsgpackResponse: +def list_common( + queryset: QuerySet, user: User, stoken: t.Optional[str], limit: int, prefetch: Prefetch +) -> MsgpackResponse: result, new_stoken_obj, done = filter_by_stoken_and_limit(stoken, limit, queryset, Collection.stoken_annotation) new_stoken = new_stoken_obj and new_stoken_obj.uid - data: t.List[CollectionOut] = [CollectionOut.from_orm_user(item, user) for item in queryset] + data: t.List[CollectionOut] = [CollectionOut.from_orm_user(item, user, prefetch) for item in queryset] ret = ListResponse(data=data, stoken=new_stoken, done=done) return MsgpackResponse(content=ret) @@ -71,39 +132,22 @@ def get_collection_queryset(user: User, queryset: QuerySet) -> QuerySet: @collection_router.post("/list_multi/") async def list_multi( - data: ListMulti, stoken: t.Optional[str] = None, limit: int = 50, user: User = Depends(get_authenticated_user) + data: ListMulti, + stoken: t.Optional[str] = None, + limit: int = 50, + user: User = Depends(get_authenticated_user), + prefetch: Prefetch = PrefetchQuery, ): queryset = get_collection_queryset(user, default_queryset) # FIXME: Remove the isnull part once we attach collection types to all objects ("collection-type-migration") queryset = queryset.filter( Q(members__collectionType__uid__in=data.collectionTypes) | Q(members__collectionType__isnull=True) ) - response = await list_common(queryset, user, stoken, limit) + response = await list_common(queryset, user, stoken, limit, prefetch) return response -class CollectionItemRevision(BaseModel): - uid: str - meta: bytes - deleted: bool - chunks: t.List[t.Tuple[str, t.Optional[bytes]]] - - -class Item(BaseModel): - uid: str - version: int - etag: t.Optional[str] - encryptionKey: t.Optional[bytes] - content: CollectionItemRevision - - -class CollectionIn(BaseModel): - collectionType: bytes - collectionKey: bytes - item: Item - - -def process_revisions_for_item(item: models.CollectionItem, revision_data: CollectionItemRevision): +def process_revisions_for_item(item: models.CollectionItem, revision_data: CollectionItemRevisionOut): chunks_objs = [] revision = models.CollectionItemRevision(**revision_data.dict(exclude={"chunks"}), item=item) @@ -147,7 +191,7 @@ def _create(data: CollectionIn, user: User): instance.save() main_item = models.CollectionItem.objects.create( - uid=data.item.uid, version=data.item.version, encryptionKey=data.item.encryptionKey, collection=instance + uid=data.item.uid, version=data.item.version, collection=instance ) instance.main_item = main_item @@ -172,3 +216,10 @@ def _create(data: CollectionIn, user: User): async def create(data: CollectionIn, user: User = Depends(get_authenticated_user)): await sync_to_async(_create)(data, user) return MsgpackResponse({}, status_code=status.HTTP_201_CREATED) + + +@collection_router.get("/{uid}/") +def get_collection(uid: str, user: User = Depends(get_authenticated_user), prefetch: Prefetch = PrefetchQuery): + obj = get_collection_queryset(user, default_queryset).get(uid=uid) + ret = CollectionOut.from_orm_user(obj, user, prefetch) + return MsgpackResponse(ret)