# Petale - Simple App as Key/Value Storage Interface # Copyright (C) 2017 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import unicode_literals import hashlib from django.utils import six from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ from django.core.validators import RegexValidator from django.core.mail import send_mail from .exceptions import GlobalSpaceExhausted, PetalSizeExhausted from . import utils id_validator = RegexValidator('^[A-Za-z0-9-_]+$') @six.python_2_unicode_compatible class Partner(models.Model): name = models.CharField( verbose_name=_('Partner'), max_length=64, unique=True, validators=[id_validator]) admin_emails = models.CharField( verbose_name=_('Admin emails'), max_length=256, blank=True, help_text=_('List of admin emails separated by comma')) hard_global_max_size = models.IntegerField( verbose_name=_('Hard max size'), help_text=_('as kilobytes')) soft_global_max_size = models.IntegerField( verbose_name=_('Soft max size'), help_text=_('as kilobytes')) hard_per_key_max_size = models.IntegerField( verbose_name=_('Hard max size per key'), help_text=_('as kilobytes')) soft_per_key_max_size = models.IntegerField( verbose_name=_('Soft max size per key'), help_text=_('as kilobytes')) size = models.BigIntegerField( verbose_name=_('Size'), default=0, help_text=_('as bytes')) def __str__(self): return self.name def check_limits(self, size_delta, **kwargs): new_size = self.size + size_delta if new_size > self.hard_global_max_size * 1024: raise GlobalSpaceExhausted if (self.size < self.soft_global_max_size * 1024 and new_size > self.soft_global_max_size * 1024): self.notify_admins( subject=_('Partner %s space almost exhausted') % self.name, # pylint: disable=no-member body=_('Current size: {current_size}, Max size: {max_size}').format( current_size=new_size, max_size=self.hard_global_max_size * 1024), **kwargs) def notify_admins(self, subject, body, **kwargs): if kwargs: body += '\n' for key, value in kwargs.items(): body += '\n %s = %r' % (key, value) send_mail(subject, body, None, self.admin_emails.split(',')) class Meta: verbose_name = _('Partner') verbose_name_plural = _('Partners') ordering = ['name'] @six.python_2_unicode_compatible class CUT(models.Model): uuid = models.CharField( max_length=255, validators=[id_validator], unique=True) def __str__(self): return self.uuid class Meta: verbose_name = _('CUT') verbose_name_plural = _('CUTs') def petal_directory(instance, filename): assert instance.name assert instance.partner assert instance.partner.name assert instance.cut assert instance.cut.uuid return 'data/{0}/{1}/{2}/{3}'.format( instance.partner.name, hashlib.md5(instance.cut.uuid.encode('ascii')).hexdigest()[:3], instance.cut.uuid, instance.name) @six.python_2_unicode_compatible class Petal(models.Model): created_at = models.DateTimeField( _('Created'), auto_now_add=True) updated_at = models.DateTimeField( _('Updated'), auto_now=True) name = models.CharField( _('Name'), max_length=128, validators=[id_validator]) etag = models.CharField( _('ETag'), max_length=256) data = models.FileField( _('Data Content'), max_length=512, upload_to=petal_directory) content_type = models.CharField( _('Content type'), max_length=128) size = models.IntegerField( _('Size'), default=0, help_text=_('as bytes')) cut = models.ForeignKey(CUT, on_delete=models.CASCADE) partner = models.ForeignKey(Partner, on_delete=models.CASCADE) def __str__(self): return u'%s/%s/%s' % (self.partner.name, self.cut.uuid, self.name) def clean(self): if self.data: self.size = self.data.size self.etag = utils.etag(self.data) def check_limits(self, content_length): '''Delegate global limits check to partner, and check per key size limits''' size_delta = content_length - self.size self.partner.check_limits(size_delta, partner=self.partner.name, cut=self.cut.uuid, key=self.name) if content_length > self.partner.hard_per_key_max_size * 1024: raise PetalSizeExhausted if (self.size <= self.partner.soft_per_key_max_size * 1024 and content_length > self.partner.soft_per_key_max_size * 1024): self.partner.notify_admins( # pylint: disable=no-member _('Key {key} space of partner {partner} almost exhausted').format( key=self.name, partner=self.partner.name), # pylint: disable=no-member _('Current size: {current_size}, Max size: {max_size}').format( current_size=content_length, max_size=self.partner.hard_per_key_max_size * 1024), partner=self.partner.name, cut=self.cut.uuid, key=self.name) class Meta: unique_together = (('name', 'partner', 'cut')) verbose_name = _('Petal') verbose_name_plural = _('Petals') @six.python_2_unicode_compatible class AccessControlList(models.Model): order = models.IntegerField( _('Order')) partner = models.ForeignKey( Partner, verbose_name=_('Partner'), on_delete=models.CASCADE) user = models.ForeignKey( User, verbose_name=_('User'), on_delete=models.CASCADE) methods = models.CharField( _('Allowed methods'), max_length=128, default='GET,PUT,DELETE', help_text=("GET, PUT, DELETE")) key = models.CharField( _('Allowed keys'), max_length=128, default='*') def __str__(self): return u'%s %s %s %s' % ( self.partner.name, self.user.username, self.methods, self.key) class Meta: verbose_name = _('Access control list') verbose_name_plural = _('Access control lists') ordering = ['partner__name', 'user__username', 'order', 'key', 'methods']