Skip to content

Commit

Permalink
Plugin Installation and Removing (#792)
Browse files Browse the repository at this point in the history
* fixes #788
* Adding function for resolving plugin dir
* allow removing a plugin from users plugin dir
  • Loading branch information
luzip665 committed Oct 6, 2022
1 parent b5e4749 commit 0405aac
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 35 deletions.
2 changes: 2 additions & 0 deletions data/ui/preferences/plugin.ui
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<column type="gboolean"/>
<!-- column-name toggle-visible -->
<column type="gboolean"/>
<!-- column-name user_installed -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkBox" id="preferences_pane">
Expand Down
1 change: 1 addition & 0 deletions xl/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ def __init__(self):
xdg.config_home = alldatadir
xdg.config_dirs.insert(0, xdg.config_home)
xdg.cache_home = alldatadir
xdg.plugin_dirs[0] = os.path.join(alldatadir, 'plugins')

try:
xdg._make_missing_dirs()
Expand Down
57 changes: 33 additions & 24 deletions xl/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,17 @@ def __str__(self):
return str(self.args[0])


class PluginExistsError(Exception):
def __str__(self):
return str(self.args[0])


class PluginsManager:
def __init__(self, exaile, load=True):
self.plugindirs = [os.path.join(p, 'plugins') for p in xdg.get_data_dirs()]
if xdg.local_hack:
self.plugindirs.insert(1, os.path.join(xdg.exaile_dir, 'plugins'))

try:
os.makedirs(self.plugindirs[0])
except Exception:
pass
self.user_installed_plugindir = xdg.get_user_plugin_dir()
"""Dir for user installed plugins"""

self.plugindirs = [x for x in self.plugindirs if os.path.exists(x)]
self.loaded_plugins = {}

self.exaile = exaile
Expand All @@ -63,7 +62,7 @@ def __init__(self, exaile, load=True):
self.load = load

def __findplugin(self, pluginname):
for plugin_dir in self.plugindirs:
for plugin_dir in xdg.get_plugin_dirs():
path = os.path.join(plugin_dir, pluginname)
if os.path.exists(path):
return path
Expand Down Expand Up @@ -95,25 +94,29 @@ def load_plugin(self, pluginname, reload_plugin=False):
self.loaded_plugins[pluginname] = plugin
return plugin

def install_plugin(self, path):
def install_plugin(self, path, overwrite_existing: bool = False) -> str:
try:
tar = tarfile.open(path, "r:*") # transparently supports gz, bz2
except (tarfile.ReadError, OSError):
raise InvalidPluginError(_('Plugin archive is not in the correct format.'))

# ensure the paths in the archive are sane
mems = tar.getmembers()
base = os.path.basename(path).split('.')[0]
if os.path.isdir(os.path.join(self.plugindirs[0], base)):
raise InvalidPluginError(

installed_plugins = self.list_installed_plugins()
if not overwrite_existing and base in installed_plugins:
raise PluginExistsError(
_('A plugin with the name "%s" is already installed.') % base
)

# ensure the paths in the archive are sane
for m in mems:
if not m.name.startswith(base):
raise InvalidPluginError(_('Plugin archive contains an unsafe path.'))

tar.extractall(self.plugindirs[0])
tar.extractall(self.user_installed_plugindir)

return base

def __on_new_plugin_loaded(self, eventname, exaile, maybe_name, fn):
event.remove_callback(self.__on_new_plugin_loaded, eventname)
Expand Down Expand Up @@ -144,15 +147,16 @@ def __enable_new_plugin(self, plugin):
else:
plugin.on_exaile_loaded()

def uninstall_plugin(self, pluginname):
self.disable_plugin(pluginname)
for plugin_dir in self.plugindirs:
try:
shutil.rmtree(self.__findplugin(pluginname))
return True
except Exception:
pass
return False
def uninstall_plugin(self, pluginname: str) -> None:
if not self.is_user_installed(pluginname):
raise Exception("Cannot remove built-in plugins")
if pluginname in self.enabled_plugins:
self.disable_plugin(pluginname)
plugin_path = os.path.join(self.user_installed_plugindir, pluginname)
try:
shutil.rmtree(plugin_path)
except Exception as e:
raise e

def enable_plugin(self, pluginname, installation: bool = False):
try:
Expand Down Expand Up @@ -194,7 +198,7 @@ def disable_plugin(self, pluginname):

def list_installed_plugins(self):
pluginlist = []
for directory in self.plugindirs:
for directory in xdg.get_plugin_dirs():
if not os.path.exists(directory):
continue
for name in os.listdir(directory):
Expand All @@ -213,6 +217,11 @@ def list_available_plugins(self):
def list_updateable_plugins(self):
pass

def is_user_installed(self, pluginname: str) -> bool:
if os.path.isdir(os.path.join(self.user_installed_plugindir, pluginname)):
return True
return False

def get_plugin_info(self, pluginname):
path = os.path.join(self.__findplugin(pluginname), 'PLUGININFO')
infodict = {}
Expand Down
26 changes: 23 additions & 3 deletions xl/xdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
else:
data_dirs = [os.path.join(d, "exaile") for d in data_dirs.split(os.pathsep)]


config_dirs = os.getenv("XDG_CONFIG_DIRS")
if config_dirs is None:
if sys.platform == 'win32':
Expand All @@ -67,18 +68,17 @@
else:
config_dirs = [os.path.join(d, "exaile") for d in config_dirs.split(os.pathsep)]

local_hack = False
fhs_compliant = True
# Detect if Exaile is not installed.
if os.path.exists(os.path.join(exaile_dir, 'data')):
local_hack = True
fhs_compliant = False
# Insert the "data" directory to data_dirs.
data_dir = os.path.join(exaile_dir, 'data')
data_dirs.insert(0, data_dir)
# insert the config dir
config_dir = os.path.join(exaile_dir, 'data', 'config')
config_dirs.insert(0, config_dir)


data_dirs.insert(0, data_home)


Expand Down Expand Up @@ -155,4 +155,24 @@ def _make_missing_dirs():
os.makedirs(logs_home)


plugin_dirs = [os.path.join(p, 'plugins') for p in get_data_dirs()]
if not fhs_compliant:
plugin_dirs.insert(1, os.path.join(exaile_dir, 'plugins'))

try:
os.makedirs(plugin_dirs[0])
except Exception:
pass

plugin_dirs = [x for x in plugin_dirs if os.path.exists(x)]


def get_user_plugin_dir():
return plugin_dirs[0]


def get_plugin_dirs():
return plugin_dirs


# vim: et sts=4 sw=4
2 changes: 1 addition & 1 deletion xlgui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(self, exaile):

exaile_icon_path = add_icon('exaile', images_dir)
Gtk.Window.set_default_icon_name('exaile')
if xdg.local_hack:
if not xdg.fhs_compliant:
# PulseAudio also attaches the above name to streams. However, if
# Exaile is not installed, any app trying to display the icon won't
# be able to find it just by name. The following is a hack to tell
Expand Down
85 changes: 78 additions & 7 deletions xlgui/preferences/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ def __init__(self, preferences, builder):
builder.connect_signals(self)
self.plugins = main.exaile().plugins

self.window = builder.get_object('PreferencesDialog')

self.message = dialogs.MessageBar(
parent=builder.get_object('preferences_pane'), buttons=Gtk.ButtonsType.CLOSE
)
Expand All @@ -72,6 +74,16 @@ def __init__(self, preferences, builder):
name_column.pack_start(reload_cellrenderer, True)
name_column.add_attribute(reload_cellrenderer, 'visible', 3)

"""Show trash can"""
remove_cellrenderer = common.ClickableCellRendererPixbuf()
remove_cellrenderer.props.icon_name = 'edit-delete'
remove_cellrenderer.props.xalign = 1
remove_cellrenderer.connect('clicked', self.on_remove_cellrenderer_clicked)

name_column = builder.get_object('name_column')
name_column.pack_start(remove_cellrenderer, True)
name_column.add_attribute(remove_cellrenderer, 'visible', 8)

self.version_label = builder.get_object('version_label')
self.author_label = builder.get_object('author_label')
self.name_label = builder.get_object('name_label')
Expand Down Expand Up @@ -99,7 +111,7 @@ def __init__(self, preferences, builder):
GLib.idle_add(selection.select_path, (0,))
GLib.idle_add(self.list.grab_focus)

def _load_plugin_list(self):
def _load_plugin_list(self, select_plugin=None):
"""
Loads the plugin list
"""
Expand All @@ -116,6 +128,7 @@ def _load_plugin_list(self):

compatible = self.plugins.is_compatible(info)
broken = self.plugins.is_potentially_broken(info)
user_installed = self.plugins.is_user_installed(plugin_name)

except Exception:
failed_list += [plugin_name]
Expand All @@ -139,6 +152,7 @@ def _load_plugin_list(self):
broken,
compatible,
True,
user_installed,
)

if 'Category' in info:
Expand All @@ -161,7 +175,7 @@ def categorykey(item):
plugins_list.sort(key=lambda x: locale.strxfrm(x[1]))

it = self.model.append(
None, (None, category, '', False, '', False, True, False)
None, (None, category, '', False, '', False, True, False, False)
)

for plugin_data in plugins_list:
Expand All @@ -174,6 +188,12 @@ def categorykey(item):
# TODO: Keep track of which categories are already expanded, and only expand those
self.list.expand_all()

if select_plugin:
path = self.plugin_to_path[select_plugin]
selection = self.list.get_selection()
selection.select_path(path)
self.list.scroll_to_cell(path)

if failed_list:
self.message.show_error(
_('Could not load plugin info!'),
Expand All @@ -198,6 +218,35 @@ def on_plugin_tree_row_activated(self, tree, path, column):
"""
self.enabled_cellrenderer.emit('toggled', path[0])

def on_remove_cellrenderer_clicked(self, cellrenderer, path: str) -> None:
"""
Remove a user installed plugin
"""
plugin_name = self.filter_model[path][0]
user_installed = self.filter_model[path][8]

if not user_installed:
return

response = dialogs.yesno(
self.window,
"\n".join(
[
_('Do you really want to remove this plugin?'),
]
),
)
if response != Gtk.ResponseType.YES:
return

try:
self.plugins.uninstall_plugin(plugin_name)
except Exception as e:
self.message.show_error(_('Could not remove plugin!'), str(e))
return

self._load_plugin_list()

def on_reload_cellrenderer_clicked(self, cellrenderer, path):
"""
Reloads a plugin from scratch
Expand Down Expand Up @@ -254,14 +303,36 @@ def on_install_plugin_button_clicked(self, button):
dialog.hide()

if result == Gtk.ResponseType.OK:
try:
self.plugins.install_plugin(dialog.get_filename())
except plugins.InvalidPluginError as e:
self.message.show_error(_('Plugin file installation failed!'), str(e))

def install(filename: str, overwrite_existing: bool = False) -> str:
try:
plugin_name = self.plugins.install_plugin(
filename, overwrite_existing
)
except plugins.PluginExistsError as e:
response = dialogs.yesno(
self.window,
"\n".join(
[
_('Plugin allready exists!'),
_('Do you want to override it?'),
]
),
)
if response == Gtk.ResponseType.YES:
return install(filename, True)
except plugins.InvalidPluginError as e:
self.message.show_error(
_('Plugin file installation failed!'), str(e)
)
return ''
return plugin_name

plugin_name = install(dialog.get_filename())
if not plugin_name:
return

self._load_plugin_list()
self._load_plugin_list(plugin_name)

def on_selection_changed(self, selection, user_data=None):
"""
Expand Down

0 comments on commit 0405aac

Please sign in to comment.