From 2a39f3538e2d846d2f3051100ea672b248f27b64 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 26 May 2020 18:52:44 +0300 Subject: [PATCH] Change to standalone stoken objects (+ small optimisation). Makes it possible to now generate Stokens as we need so we can add them to non-revision objects, for example, membership changes. We also slightly improved how we filter by revs. --- django_etesync/models.py | 15 +++++++++++++-- django_etesync/serializers.py | 4 +++- django_etesync/views.py | 26 +++++++++++++++++--------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/django_etesync/models.py b/django_etesync/models.py index ba6665c..3767b3a 100644 --- a/django_etesync/models.py +++ b/django_etesync/models.py @@ -18,6 +18,7 @@ from django.db import models from django.conf import settings from django.core.validators import RegexValidator from django.utils.functional import cached_property +from django.utils.crypto import get_random_string Base64Url256BitlValidator = RegexValidator(regex=r'^[a-zA-Z0-9\-_]{42,43}$', message='Expected a base64url.') @@ -55,7 +56,7 @@ class Collection(models.Model): # FIXME: what is the etag for None? Though if we use the revision for collection it should be shared anyway. return None - return last_revision.uid + return last_revision.stoken.uid class CollectionItem(models.Model): @@ -77,7 +78,7 @@ class CollectionItem(models.Model): @property def stoken(self): - return self.content.uid + return self.content.stoken.uid def chunk_directory_path(instance, filename): @@ -98,7 +99,17 @@ class CollectionItemChunk(models.Model): return self.uid +def generate_stoken_uid(): + return get_random_string(32, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_') + + +class Stoken(models.Model): + uid = models.CharField(db_index=True, unique=True, blank=False, null=False, default=generate_stoken_uid, + max_length=43, validators=[Base64Url256BitlValidator]) + + class CollectionItemRevision(models.Model): + stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT) uid = models.CharField(db_index=True, unique=True, blank=False, null=False, max_length=43, validators=[Base64Url256BitlValidator]) item = models.ForeignKey(CollectionItem, related_name='revisions', on_delete=models.CASCADE) diff --git a/django_etesync/serializers.py b/django_etesync/serializers.py index bff20ad..d08fcfd 100644 --- a/django_etesync/serializers.py +++ b/django_etesync/serializers.py @@ -38,7 +38,9 @@ def process_revisions_for_item(item, revision_data): chunk = models.CollectionItemChunk.objects.get(uid=uid) chunks_objs.append(chunk) - revision = models.CollectionItemRevision.objects.create(**revision_data, item=item) + stoken = models.Stoken.objects.create() + + revision = models.CollectionItemRevision.objects.create(**revision_data, item=item, stoken=stoken) for chunk in chunks_objs: models.RevisionChunkRelation.objects.create(chunk=chunk, revision=revision) return revision diff --git a/django_etesync/views.py b/django_etesync/views.py index 7f07f2e..49b8110 100644 --- a/django_etesync/views.py +++ b/django_etesync/views.py @@ -35,7 +35,15 @@ import nacl.secret import nacl.hash from . import app_settings, permissions -from .models import Collection, CollectionItem, CollectionItemRevision, CollectionMember, CollectionInvitation, UserInfo +from .models import ( + Collection, + CollectionItem, + CollectionItemRevision, + CollectionMember, + CollectionInvitation, + Stoken, + UserInfo, + ) from .serializers import ( b64encode, AuthenticationSignupSerializer, @@ -94,18 +102,18 @@ class BaseViewSet(viewsets.ModelViewSet): user = self.request.user return queryset.filter(members__user=user) - def get_cstoken_rev(self, request): + def get_cstoken_obj(self, request): cstoken = request.GET.get('cstoken', None) if cstoken is not None: - return get_object_or_404(CollectionItemRevision.objects.all(), uid=cstoken) + return get_object_or_404(Stoken.objects.all(), uid=cstoken) return None def filter_by_cstoken(self, request, queryset): cstoken_id_field = self.cstoken_id_field + '__id' - cstoken_rev = self.get_cstoken_rev(request) + cstoken_rev = self.get_cstoken_obj(request) if cstoken_rev is not None: filter_by = {cstoken_id_field + '__gt': cstoken_rev.id} queryset = queryset.filter(**filter_by) @@ -116,7 +124,7 @@ class BaseViewSet(viewsets.ModelViewSet): cstoken_id_field = self.cstoken_id_field + '__id' new_cstoken_id = queryset.aggregate(cstoken_id=Max(cstoken_id_field))['cstoken_id'] - new_cstoken = new_cstoken_id and CollectionItemRevision.objects.get(id=new_cstoken_id).uid + new_cstoken = new_cstoken_id and Stoken.objects.get(id=new_cstoken_id).uid return queryset, new_cstoken @@ -139,7 +147,7 @@ class CollectionViewSet(BaseViewSet): queryset = Collection.objects.all() serializer_class = CollectionSerializer lookup_field = 'uid' - cstoken_id_field = 'items__revisions' + cstoken_id_field = 'items__revisions__stoken' def get_queryset(self, queryset=None): if queryset is None: @@ -199,7 +207,7 @@ class CollectionItemViewSet(BaseViewSet): queryset = CollectionItem.objects.all() serializer_class = CollectionItemSerializer lookup_field = 'uid' - cstoken_id_field = 'revisions' + cstoken_id_field = 'revisions__stoken' def get_queryset(self): collection_uid = self.kwargs['collection_uid'] @@ -290,8 +298,8 @@ class CollectionItemViewSet(BaseViewSet): queryset, cstoken_rev = self.filter_by_cstoken(request, queryset) uids, stokens = zip(*[(item['uid'], item.get('stoken')) for item in serializer.validated_data]) - rev_ids = CollectionItemRevision.objects.filter(uid__in=stokens, current=True).values_list('id', flat=True) - queryset = queryset.filter(uid__in=uids).exclude(revisions__id__in=rev_ids) + revs = CollectionItemRevision.objects.filter(stoken__uid__in=stokens, current=True) + queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs) queryset, new_cstoken = self.get_queryset_cstoken(queryset) cstoken = cstoken_rev and cstoken_rev.uid