Files
winget.pro/winget/models.py
T
2023-10-09 08:06:46 +02:00

151 lines
5.4 KiB
Python

from django.core.validators import MinLengthValidator, RegexValidator
from django.db.models import Model, CharField, DateTimeField, ForeignKey, \
CASCADE, TextField, FileField, CheckConstraint, Q
from django.db.models.signals import pre_save
from django.dispatch import receiver
from hashlib import sha256
from tenants.models import Tenant
from winget.util import CharFieldFromChoices, randomize_filename
class Package(Model):
tenant = ForeignKey(Tenant, on_delete=CASCADE)
identifier = CharField(
max_length=128,
help_text='Unique identifier for the package (e.g. WinMerge.WinMerge).'
)
name = CharField(
max_length=256, validators=[MinLengthValidator(2)],
help_text='Package name (e.g. WinMerge).'
)
publisher = CharField(
max_length=256, validators=[MinLengthValidator(2)],
help_text='Package publisher (eg. Thingamahoochie Software)'
)
description = TextField(
max_length=256, validators=[MinLengthValidator(3)],
help_text=
'Package description (e.g. "An open source differencing tool.")'
)
created = DateTimeField(auto_now_add=True)
modified = DateTimeField(auto_now=True)
class Meta:
unique_together = ('tenant', 'identifier')
def __str__(self):
return self.name
class Version(Model):
package = ForeignKey(Package, on_delete=CASCADE)
version = CharField(
max_length=128, blank=True,
help_text="The package's version (eg. 2.16.26 or 1.2.3.4)."
)
created = DateTimeField(auto_now_add=True)
modified = DateTimeField(auto_now=True)
class Meta:
unique_together = ('package', 'version')
def __str__(self):
result = self.package.name
if self.version:
result += ' ' + self.version
return result
def installer_upload_to(instance, filename):
# Randomize the upload path. This prevents users from guessing it and
# prevents clashes.
randomized_filename = randomize_filename(filename)
return f'{instance.version.package.tenant.uuid}/{randomized_filename}'
class Installer(Model):
version = ForeignKey(Version, on_delete=CASCADE)
architecture = CharFieldFromChoices('x86', 'x64', 'arm', 'arm64')
scope = CharFieldFromChoices(
'user', 'machine', 'both', default='both',
help_text=
"Is this a machine-wide installer, just for the current user, or both?"
)
type = CharFieldFromChoices(
'msix', 'msi', 'appx', 'exe', 'zip', 'inno', 'nullsoft', 'wix', 'burn',
'pwa', 'msstore'
)
nested_installer = CharField(
blank=True, max_length=512, help_text=
"If this is a zip file, which installer inside it should be run?"
)
nested_installer_type = CharFieldFromChoices(
'msix', 'msi', 'appx', 'exe', 'inno', 'nullsoft', 'wix', 'burn',
'portable', blank=True, null=True, help_text=
"If this is a zip file, what's the type of the nested installer?"
)
file = FileField(upload_to=installer_upload_to)
sha256 = CharField(
max_length=64, validators=[RegexValidator('^[a-fA-F0-9]{64}$')]
)
created = DateTimeField(auto_now_add=True)
modified = DateTimeField(auto_now=True)
class Meta:
unique_together = ('version', 'architecture', 'type')
# N.B.: The constraints here need to be checked in .validate(...) too.
constraints = [
CheckConstraint(
check=(Q(type='zip') & ~Q(nested_installer='')) | \
(~Q(type='zip') & Q(nested_installer='')),
name='nested_installer_iff_zip'
),
CheckConstraint(
check=(Q(type='zip') & ~Q(nested_installer_type=None)) | \
(~Q(type='zip') & Q(nested_installer_type=None)),
name='nested_installer_type_iff_zip'
),
]
@classmethod
def validate(cls, data, use_verbose_names=True):
# If possible, the checks here should be in Meta.constraints too.
errors = {}
if data.get('type') == 'zip':
if not data.get('nested_installer'):
errors['nested_installer'] = \
'nested_installer is required when type is "zip".'
if not data.get('nested_installer_type'):
errors['nested_installer_type'] = \
'nested_installer_type is required when type is "zip".'
else:
if data.get('nested_installer'):
errors['nested_installer'] = \
'nested_installer can only be set when type is "zip".'
if data.get('nested_installer_type'):
errors['nested_installer_type'] = \
'nested_installer_type can only be set when type is "zip".'
if use_verbose_names:
errors = {
field: message.replace(
field, cls._meta.get_field(field).verbose_name.capitalize()
)
for field, message in errors.items()
}
return errors
@property
def scopes(self):
return ['user', 'machine'] if self.scope == 'both' else [self.scope]
def __str__(self):
return self.file.url
@receiver(pre_save, sender=Installer)
def pre_installer_save(sender, instance, **kwargs):
m = sha256()
instance.file.seek(0)
for chunk in instance.file.chunks():
m.update(chunk)
instance.sha256 = m.digest().hex()