feat: 支持 virtual app (#12199)

* feat: 支持 virtual app

* perf: 增加 virtual host

* perf: 新增 virtual app 上传接口

* perf: 更名为 app provider

* perf: 优化代码

---------

Co-authored-by: Eric <xplzv@126.com>
This commit is contained in:
fit2bot
2023-12-05 16:52:11 +08:00
committed by GitHub
parent a43bb25b5a
commit d2429f7883
25 changed files with 605 additions and 5 deletions

View File

@@ -1,3 +1,4 @@
from .session import *
from .component import *
from .applet import *
from .virtualapp import *

View File

@@ -0,0 +1,2 @@
from .provider import *
from .virtualapp import *

View File

@@ -0,0 +1,28 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
__all__ = ['AppProvider', ]
class AppProvider(JMSBaseModel):
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
terminal = models.OneToOneField(
'terminal.Terminal', on_delete=models.CASCADE, null=True, blank=True,
related_name='app_provider', verbose_name=_('Terminal')
)
apps = models.ManyToManyField(
'VirtualApp', verbose_name=_('VirtualApp'),
through='VirtualAppPublication', through_fields=('provider', 'app'),
)
class Meta:
ordering = ('-date_created',)
@property
def load(self):
if not self.terminal:
return 'offline'
return self.terminal.load

View File

@@ -0,0 +1,103 @@
import os
import shutil
from django.conf import settings
from django.core.files.storage import default_storage
from django.db import models
from django.utils._os import safe_join
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import ValidationError
from common.db.models import JMSBaseModel
from common.utils import lazyproperty
from common.utils.yml import yaml_load_with_i18n
__all__ = ['VirtualApp', 'VirtualAppPublication']
class VirtualApp(JMSBaseModel):
name = models.SlugField(max_length=128, verbose_name=_('Name'), unique=True)
display_name = models.CharField(max_length=128, verbose_name=_('Display name'))
version = models.CharField(max_length=16, verbose_name=_('Version'))
author = models.CharField(max_length=128, verbose_name=_('Author'))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
protocols = models.JSONField(default=list, verbose_name=_('Protocol'))
image_name = models.CharField(max_length=128, verbose_name=_('Image name'))
image_protocol = models.CharField(max_length=16, default='vnc', verbose_name=_('Image protocol'))
image_port = models.IntegerField(default=5900, verbose_name=_('Image port'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
tags = models.JSONField(default=list, verbose_name=_('Tags'))
providers = models.ManyToManyField(
through_fields=('app', 'provider',), through='VirtualAppPublication',
to='AppProvider', verbose_name=_('Providers')
)
class Meta:
verbose_name = _('Virtual app')
def __str__(self):
return self.name
@property
def path(self):
return default_storage.path('virtual_apps/{}'.format(self.name))
@lazyproperty
def readme(self):
readme_file = os.path.join(self.path, 'README.md')
if os.path.isfile(readme_file):
with open(readme_file, 'r') as f:
return f.read()
return ''
@property
def icon(self):
path = os.path.join(self.path, 'icon.png')
if not os.path.exists(path):
return None
return os.path.join(settings.MEDIA_URL, 'virtual_apps', self.name, 'icon.png')
@staticmethod
def validate_pkg(d):
files = ['manifest.yml', 'icon.png', ]
for name in files:
path = safe_join(d, name)
if not os.path.exists(path):
raise ValidationError({'error': _('Applet pkg not valid, Missing file {}').format(name)})
with open(safe_join(d, 'manifest.yml'), encoding='utf8') as f:
manifest = yaml_load_with_i18n(f)
if not manifest.get('name', ''):
raise ValidationError({'error': 'Missing name in manifest.yml'})
return manifest
@classmethod
def install_from_dir(cls, path):
from terminal.serializers import VirtualAppSerializer
manifest = cls.validate_pkg(path)
name = manifest['name']
instance = cls.objects.filter(name=name).first()
serializer = VirtualAppSerializer(instance=instance, data=manifest)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
pkg_path = default_storage.path('virtual_apps/{}'.format(name))
if os.path.exists(pkg_path):
shutil.rmtree(pkg_path)
shutil.copytree(path, pkg_path)
return instance, serializer
class VirtualAppPublication(JMSBaseModel):
provider = models.ForeignKey(
'AppProvider', on_delete=models.CASCADE, related_name='publications', verbose_name=_('App Provider')
)
app = models.ForeignKey(
'VirtualApp', on_delete=models.CASCADE, related_name='publications', verbose_name=_('Virtual App')
)
status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
class Meta:
verbose_name = _('Virtual app publication')
unique_together = ('provider', 'app')