Plugins refactoring for dynamic URL patterns

pull/846/head
Piero Toffanin 2020-03-31 14:02:19 -04:00
rodzic f56667c591
commit 6f7af38c8e
7 zmienionych plików z 170 dodań i 96 usunięć

Wyświetl plik

@ -9,7 +9,7 @@ from guardian.admin import GuardedModelAdmin
from app.models import PluginDatum
from app.models import Preset
from app.models import Plugin
from app.plugins import get_plugin_by_name, enable_plugin, disable_plugin
from app.plugins import get_plugin_by_name, enable_plugin, disable_plugin, delete_plugin
from .models import Project, Task, ImageUpload, Setting, Theme
from django import forms
from codemirror2.widgets import CodeMirrorEditor
@ -105,6 +105,11 @@ class PluginAdmin(admin.ModelAdmin):
self.admin_site.admin_view(self.plugin_disable),
name='plugin-disable',
),
url(
r'^(?P<plugin_name>.+)/delete/$',
self.admin_site.admin_view(self.plugin_delete),
name='plugin-delete',
),
]
return custom_urls + urls
@ -124,14 +129,30 @@ class PluginAdmin(admin.ModelAdmin):
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
def plugin_delete(self, request, plugin_name, *args, **kwargs):
try:
delete_plugin(plugin_name)
except Exception as e:
messages.warning(request, "Cannot delete plugin {}: {}".format(plugin_name, str(e)))
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
def plugin_actions(self, obj):
plugin = get_plugin_by_name(obj.name, only_active=False)
return format_html(
'<a class="button" href="{}" {}>Disable</a>&nbsp;'
'<a class="button" href="{}" {}>Enable</a>',
'<a class="button" href="{}" {}>Enable</a>'
+ ('&nbsp;<a class="button" href="{}" onclick="return confirm(\'Are you sure you want to delete {}?\')"><i class="fa fa-trash"></i></a>' if not plugin.is_persistent() else '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
,
reverse('admin:plugin-disable', args=[obj.pk]) if obj.enabled else '#',
'disabled' if not obj.enabled else '',
reverse('admin:plugin-enable', args=[obj.pk]) if not obj.enabled else '#',
'disabled' if obj.enabled else '',
# TODO
reverse('admin:plugin-delete', args=[obj.pk]),
obj.name
)
plugin_actions.short_description = 'Actions'

Wyświetl plik

@ -1,7 +1,7 @@
from django.conf.urls import url, include
from app.api.presets import PresetViewSet
from app.plugins import get_api_url_patterns
from app.plugins.views import api_view_handler
from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
@ -49,6 +49,6 @@ urlpatterns = [
url(r'^auth/', include('rest_framework.urls')),
url(r'^token-auth/', obtain_jwt_token),
]
urlpatterns += get_api_url_patterns()
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', api_view_handler)
]

Wyświetl plik

@ -59,7 +59,7 @@ def sync_plugin_db():
defaults={'enabled': not disabled},
)
if created:
logger.info("Added [{}] plugin to database".format(plugin.get_name()))
logger.info("Added [{}] plugin to database".format(plugin))
def clear_plugins_cache():
@ -150,40 +150,6 @@ def register_plugins():
logger.warning("Cannot register {}: {}".format(plugin, str(e)))
def get_app_url_patterns():
"""
@return the patterns to expose the /public directory of each plugin (if needed) and
each mount point
"""
url_patterns = []
for plugin in get_plugins():
for mount_point in plugin.app_mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if plugin.path_exists("public"):
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
django.views.static.serve,
{'document_root': plugin.get_path("public")}))
return url_patterns
def get_api_url_patterns():
"""
@return the patterns to expose the plugin API mount points (if any)
"""
url_patterns = []
for plugin in get_plugins():
for mount_point in plugin.api_mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
return url_patterns
plugins = None
def get_plugins():
"""
@ -193,9 +159,10 @@ def get_plugins():
global plugins
if plugins != None: return plugins
plugins_path = get_plugins_path()
plugins_paths = get_plugins_paths()
plugins = []
for plugins_path in plugins_paths:
for dir in [d for d in os.listdir(plugins_path) if os.path.isdir(plugins_path)]:
# Each plugin must have a manifest.json and a plugin.py
plugin_path = os.path.join(plugins_path, dir)
@ -212,11 +179,14 @@ def get_plugins():
# Check plugin required files
if not os.path.isfile(manifest_path) or not os.path.isfile(pluginpy_path):
logger.warning("Found invalid plugin in {}".format(plugin_path))
continue
# Instantiate the plugin
try:
try:
module = importlib.import_module("app.media.plugins.{}".format(dir))
plugin = (getattr(module, "Plugin"))()
except (ModuleNotFoundError, AttributeError):
module = importlib.import_module("plugins.{}".format(dir))
plugin = (getattr(module, "Plugin"))()
@ -235,6 +205,11 @@ def get_plugins():
if plugin.get_name() in settings.PLUGINS_BLACKLIST:
continue
# Skip plugins already added
if plugin.get_name() in [p.get_name() for p in plugins]:
logger.warning("Duplicate plugin name found in {}, skipping".format(plugin_path))
continue
plugins.append(plugin)
except Exception as e:
logger.warning("Failed to instantiate plugin {}: {}".format(dir, e))
@ -281,7 +256,11 @@ def get_current_plugin():
"""
caller_filename = traceback.extract_stack()[-2][0]
relp = os.path.relpath(caller_filename, get_plugins_path())
for p in get_plugins_paths():
relp = os.path.relpath(caller_filename, p)
if ".." in relp:
continue
parts = relp.split(os.sep)
if len(parts) > 0:
plugin_name = parts[0]
@ -289,9 +268,12 @@ def get_current_plugin():
return None
def get_plugins_path():
def get_plugins_paths():
current_path = os.path.dirname(os.path.realpath(__file__))
return os.path.abspath(os.path.join(current_path, "..", "..", "plugins"))
return [
os.path.abspath(get_plugins_persistent_path()),
os.path.abspath(os.path.join(current_path, "..", "..", "plugins")),
]
def get_plugins_persistent_path(*paths):
return os.path.join(settings.MEDIA_ROOT, "plugins", *paths)
@ -322,6 +304,9 @@ def enable_plugin(plugin_name):
def disable_plugin(plugin_name):
Plugin.objects.get(pk=plugin_name).disable()
def delete_plugin(plugin_name):
Plugin.objects.get(pk=plugin_name).disable()
def get_site_settings():
return Setting.objects.first()

Wyświetl plik

@ -1,4 +1,3 @@
import importlib
import json
import logging, os, sys, subprocess
from abc import ABC
@ -120,12 +119,22 @@ class PluginBase(ABC):
"""
return "/plugins/{}/{}".format(self.get_name(), path)
def is_persistent(self):
"""
:return: whether this plugin is persistent (stored in the /plugins directory,
instead of /app/media/plugins which are transient)
"""
return ".." in os.path.relpath(self.get_path(), get_plugins_persistent_path())
def template_path(self, path):
"""
:param path: unix-style path
:return: path used to reference Django templates for a plugin
"""
if self.is_persistent():
return "plugins/{}/templates/{}".format(self.get_name(), path)
else:
return "app/media/plugins/{}/templates/{}".format(self.get_name(), path)
def path_exists(self, path):
return os.path.exists(self.get_path(path))

Wyświetl plik

@ -1,5 +1,8 @@
// Magic to include node_modules of root WebODM's directory
process.env.NODE_PATH = "../../../node_modules";
const fs = require('fs');
let webodmRoot = "../../../"; // Assuming plugins/<name>/public
if (!fs.existsSync(webodmRoot + "webodm.sh")) webodmRoot = "../../../../../"; // Assuming app/media/plugins/<name>/public
process.env.NODE_PATH = webodmRoot + "node_modules";
require("module").Module._initPaths();
let path = require("path");
@ -69,7 +72,7 @@ module.exports = {
modules: ['node_modules', 'bower_components'],
extensions: ['.js', '.jsx'],
alias: {
webodm: path.resolve(__dirname, '../../../app/static/app/js')
webodm: path.resolve(__dirname, webodmRoot + 'app/static/app/js')
}
},

Wyświetl plik

@ -1,3 +1,60 @@
import os
from app.api.tasks import TaskNestedView as TaskView
from app.api.workers import CheckTask as CheckTask
from app.api.workers import GetTaskResult as GetTaskResult
from django.http import HttpResponse, Http404
from .functions import get_plugin_by_name
from django.conf.urls import url
from django.views.static import serve
def try_resolve_url(request, url):
res = url.resolve(request.get_full_path())
if res:
return res
else:
return (None, None, None)
def app_view_handler(request, plugin_name=None):
plugin = get_plugin_by_name(plugin_name) # TODO: this pings the server, which might be bad for performance with very large amount of files
if plugin is None:
raise Http404("Plugin not found")
# Try mountpoints first
for mount_point in plugin.app_mount_points():
view, args, kwargs = try_resolve_url(request, url(r'^/plugins/{}/{}'.format(plugin_name, mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if view:
return view(request, *args, **kwargs)
# Try public assets
if os.path.exists(plugin.get_path("public")):
view, args, kwargs = try_resolve_url(request, url('^/plugins/{}/(.*)'.format(plugin_name),
serve,
{'document_root': plugin.get_path("public")}))
if view:
return view(request, *args, **kwargs)
raise Http404("No valid routes")
def api_view_handler(request, plugin_name=None):
plugin = get_plugin_by_name(plugin_name) # TODO: this pings the server, which might be bad for performance with very large amount of files
if plugin is None:
raise Http404("Plugin not found")
for mount_point in plugin.api_mount_points():
view, args, kwargs = try_resolve_url(request, url(r'^/api/plugins/{}/{}'.format(plugin_name, mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if view:
return view(request, *args, **kwargs)
raise Http404("No valid routes")

Wyświetl plik

@ -1,7 +1,7 @@
from django.conf.urls import url, include
from .views import app as app_views, public as public_views
from .plugins import get_app_url_patterns
from .plugins.views import app_view_handler
from app.boot import boot
from webodm import settings
@ -35,11 +35,10 @@ urlpatterns = [
url(r'^processingnode/([\d]+)/$', app_views.processing_node, name='processing_node'),
url(r'^api/', include("app.api.urls")),
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', app_view_handler)
]
handler404 = app_views.handler404
handler500 = app_views.handler500
# TODO: is there a way to place plugins /public directories
# into the static build directories and let nginx serve them?
urlpatterns += get_app_url_patterns()