Index the name field. Add help texts for maximum size and current size methods. Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
244 lines
7.8 KiB
Python
244 lines
7.8 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from contextlib import contextmanager
|
|
import logging
|
|
|
|
from django.core.files.base import ContentFile
|
|
from django.db import models, transaction
|
|
from django.db.models import Sum
|
|
from django.template.defaultfilters import filesizeformat
|
|
from django.utils.encoding import python_2_unicode_compatible
|
|
from django.utils.functional import cached_property
|
|
from django.utils.module_loading import import_string
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from mayan.apps.lock_manager.exceptions import LockError
|
|
from mayan.apps.lock_manager.runtime import locking_backend
|
|
|
|
from .events import (
|
|
event_cache_created, event_cache_edited, event_cache_purged
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class Cache(models.Model):
|
|
name = models.CharField(
|
|
db_index=True, help_text=_('Internal name of the cache.'),
|
|
max_length=128, unique=True, verbose_name=_('Name')
|
|
)
|
|
label = models.CharField(
|
|
help_text=_('A short text describing the cache.'), max_length=128,
|
|
verbose_name=_('Label')
|
|
)
|
|
maximum_size = models.PositiveIntegerField(
|
|
help_text=_('Maximum size of the cache in bytes.'),
|
|
verbose_name=_('Maximum size')
|
|
)
|
|
storage_instance_path = models.CharField(
|
|
help_text=_(
|
|
'Dotted path to the actual storage class used for the cache.'
|
|
), max_length=255, unique=True, verbose_name=_('Storage instance path')
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _('Cache')
|
|
verbose_name_plural = _('Caches')
|
|
|
|
def __str__(self):
|
|
return self.label
|
|
|
|
def get_files(self):
|
|
return CachePartitionFile.objects.filter(partition__cache__id=self.pk)
|
|
|
|
def get_maximum_size_display(self):
|
|
return filesizeformat(bytes_=self.maximum_size)
|
|
|
|
get_maximum_size_display.help_text = _(
|
|
'Size at which the cache will start deleting old entries.'
|
|
)
|
|
get_maximum_size_display.short_description = _('Maximum size')
|
|
|
|
def get_total_size(self):
|
|
"""
|
|
Return the actual usage of the cache.
|
|
"""
|
|
return self.get_files().aggregate(
|
|
file_size__sum=Sum('file_size')
|
|
)['file_size__sum'] or 0
|
|
|
|
def get_total_size_display(self):
|
|
return filesizeformat(bytes_=self.get_total_size())
|
|
|
|
get_total_size_display.short_description = _('Total size')
|
|
get_total_size_display.help_text = _('Current size of the cache.')
|
|
|
|
def prune(self):
|
|
"""
|
|
Deletes files until the total size of the cache is below the allowed
|
|
maximum size of the cache.
|
|
"""
|
|
while self.get_total_size() > self.maximum_size:
|
|
self.get_files().earliest().delete()
|
|
|
|
def purge(self, _user=None):
|
|
"""
|
|
Deletes the entire cache.
|
|
"""
|
|
for partition in self.partitions.all():
|
|
partition.purge()
|
|
|
|
event_cache_purged.commit(actor=_user, target=self)
|
|
|
|
def save(self, *args, **kwargs):
|
|
_user = kwargs.pop('_user', None)
|
|
with transaction.atomic():
|
|
is_new = not self.pk
|
|
result = super(Cache, self).save(*args, **kwargs)
|
|
if is_new:
|
|
event_cache_created.commit(
|
|
actor=_user, target=self
|
|
)
|
|
else:
|
|
event_cache_edited.commit(
|
|
actor=_user, target=self
|
|
)
|
|
|
|
self.prune()
|
|
return result
|
|
|
|
@cached_property
|
|
def storage(self):
|
|
return import_string(self.storage_instance_path)
|
|
|
|
|
|
class CachePartition(models.Model):
|
|
cache = models.ForeignKey(
|
|
on_delete=models.CASCADE, related_name='partitions',
|
|
to=Cache, verbose_name=_('Cache')
|
|
)
|
|
name = models.CharField(
|
|
max_length=128, verbose_name=_('Name')
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = ('cache', 'name')
|
|
verbose_name = _('Cache partition')
|
|
verbose_name_plural = _('Cache partitions')
|
|
|
|
@staticmethod
|
|
def get_combined_filename(parent, filename):
|
|
return '{}-{}'.format(parent, filename)
|
|
|
|
@contextmanager
|
|
def create_file(self, filename):
|
|
lock_id = 'cache_partition-create_file-{}-{}'.format(self.pk, filename)
|
|
try:
|
|
logger.debug('trying to acquire lock: %s', lock_id)
|
|
lock = locking_backend.acquire_lock(lock_id)
|
|
logger.debug('acquired lock: %s', lock_id)
|
|
try:
|
|
self.cache.prune()
|
|
|
|
# Since open "wb+" doesn't create files force the creation of an
|
|
# empty file.
|
|
self.cache.storage.delete(
|
|
name=self.get_full_filename(filename=filename)
|
|
)
|
|
self.cache.storage.save(
|
|
name=self.get_full_filename(filename=filename),
|
|
content=ContentFile(content='')
|
|
)
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
partition_file = self.files.create(filename=filename)
|
|
yield partition_file.open(mode='wb')
|
|
partition_file.update_size()
|
|
except Exception as exception:
|
|
logger.error(
|
|
'Unexpected exception while trying to save new '
|
|
'cache file; %s', exception
|
|
)
|
|
self.cache.storage.delete(
|
|
name=self.get_full_filename(filename=filename)
|
|
)
|
|
raise
|
|
finally:
|
|
lock.release()
|
|
except LockError:
|
|
logger.debug('unable to obtain lock: %s' % lock_id)
|
|
raise
|
|
|
|
def delete(self, *args, **kwargs):
|
|
self.purge()
|
|
return super(CachePartition, self).delete(*args, **kwargs)
|
|
|
|
def get_file(self, filename):
|
|
try:
|
|
return self.files.get(filename=filename)
|
|
except self.files.model.DoesNotExist:
|
|
return None
|
|
|
|
def get_full_filename(self, filename):
|
|
return CachePartition.get_combined_filename(
|
|
parent=self.name, filename=filename
|
|
)
|
|
|
|
def purge(self):
|
|
for parition_file in self.files.all():
|
|
parition_file.delete()
|
|
|
|
|
|
class CachePartitionFile(models.Model):
|
|
partition = models.ForeignKey(
|
|
on_delete=models.CASCADE, related_name='files',
|
|
to=CachePartition, verbose_name=_('Cache partition')
|
|
)
|
|
datetime = models.DateTimeField(
|
|
auto_now_add=True, db_index=True, verbose_name=_('Date time')
|
|
)
|
|
filename = models.CharField(max_length=255, verbose_name=_('Filename'))
|
|
file_size = models.PositiveIntegerField(
|
|
default=0, verbose_name=_('File size')
|
|
)
|
|
|
|
class Meta:
|
|
get_latest_by = 'datetime'
|
|
unique_together = ('partition', 'filename')
|
|
verbose_name = _('Cache partition file')
|
|
verbose_name_plural = _('Cache partition files')
|
|
|
|
def delete(self, *args, **kwargs):
|
|
self.partition.cache.storage.delete(name=self.full_filename)
|
|
return super(CachePartitionFile, self).delete(*args, **kwargs)
|
|
|
|
def exists(self):
|
|
return self.partition.cache.storage.exists(name=self.full_filename)
|
|
|
|
@cached_property
|
|
def full_filename(self):
|
|
return CachePartition.get_combined_filename(
|
|
parent=self.partition.name, filename=self.filename
|
|
)
|
|
|
|
def open(self, mode='rb'):
|
|
# Open the file for reading. If the file is written to, the
|
|
# .update_size() must be called.
|
|
try:
|
|
return self.partition.cache.storage.open(
|
|
name=self.full_filename, mode=mode
|
|
)
|
|
except Exception as exception:
|
|
logger.error(
|
|
'Unexpected exception opening the cache file; %s', exception
|
|
)
|
|
raise
|
|
|
|
def update_size(self):
|
|
self.file_size = self.partition.cache.storage.size(
|
|
name=self.full_filename
|
|
)
|
|
self.save()
|