File indexing completed on 2024-12-22 04:16:27

0001 # SPDX-FileCopyrightText: 2019 Rebecca Breu <rebecca@rbreu.de>
0002 
0003 # This file is part of Krita.
0004 
0005 # SPDX-License-Identifier: GPL-3.0-or-later
0006 
0007 """This module provides the actual importing logic. See
0008 `:class:PluginImporter` for more info.
0009 
0010 For easy command line testing, call this module like this:
0011   > python plugin_importer.py foo.zip /output/path
0012 """
0013 
0014 from configparser import ConfigParser, Error as ConfigParserError
0015 import os
0016 import shutil
0017 import sys
0018 from tempfile import TemporaryDirectory
0019 import zipfile
0020 
0021 
0022 class PluginImportError(Exception):
0023     """Base class for all exceptions of this module."""
0024     pass
0025 
0026 
0027 class NoPluginsFoundException(PluginImportError):
0028     """No valid plugins can be found in the zip file."""
0029     pass
0030 
0031 
0032 class PluginReadError(PluginImportError):
0033     """Zip file can't be read or its content can't be parsed."""
0034     pass
0035 
0036 
0037 class PluginImporter:
0038     """Import a Krita Python Plugin from a zip file into the given
0039     directory.
0040 
0041     The Importer makes barely any assumptions about the file structure
0042     in the zip file. It will find one or more plugins with the
0043     following strategy:
0044 
0045     1. Find files with the ending `.desktop` and read the Python
0046        module name from them
0047     2. Find directories that correspond to the Python module names
0048        and that contain an `__init__.py` file
0049     3. Find files with ending `.action` that have matching
0050        `<Action name=...>` tags (these files are optional)
0051     4. Extract the desktop- and action-files and the Python module
0052        directories into the corresponding pykrita and actions folders
0053 
0054     Usage:
0055 
0056     >>> importer = PluginImporter(
0057             '/path/to/plugin.zip',
0058             '/path/to/krita/resources/',
0059             confirm_overwrite_callback)
0060     >>> imported = importer.import_all()
0061 
0062     """
0063 
0064     def __init__(self, zip_filename, resources_dir,
0065                  confirm_overwrite_callback):
0066 
0067         """Initialise the importer.
0068 
0069         :param zip_filename: Filename of the zip archive containing the
0070           plugin(s)
0071         :param resources_dir: The Krita resources directory into which
0072           to extract the plugin(s)
0073         :param confirm_overwrite_callback: A function that gets called
0074           if a plugin already exists in the resources directory. It gets
0075           called with a dictionary of information about the plugin and
0076           should return whether the user wants to overwrite the plugin
0077           (True) or not (False).
0078         """
0079 
0080         self.resources_dir = resources_dir
0081         self.confirm_overwrite_callback = confirm_overwrite_callback
0082         try:
0083             self.archive = zipfile.ZipFile(zip_filename)
0084         except(zipfile.BadZipFile, zipfile.LargeZipFile, OSError) as e:
0085             raise PluginReadError(str(e))
0086 
0087         self.desktop_filenames = []
0088         self.action_filenames = []
0089         for filename in self.archive.namelist():
0090             if filename.endswith('.desktop'):
0091                 self.desktop_filenames.append(filename)
0092             if filename.endswith('.action'):
0093                 self.action_filenames.append(filename)
0094 
0095     @property
0096     def destination_pykrita(self):
0097         dest = os.path.join(self.resources_dir, 'pykrita')
0098         if not os.path.exists(dest):
0099             os.mkdir(dest)
0100         return dest
0101 
0102     @property
0103     def destination_actions(self):
0104         dest = os.path.join(self.resources_dir, 'actions')
0105         if not os.path.exists(dest):
0106             os.mkdir(dest)
0107         return dest
0108 
0109     def get_destination_module(self, plugin):
0110         return os.path.join(self.destination_pykrita, plugin['name'])
0111 
0112     def get_destination_desktop(self, plugin):
0113         return os.path.join(
0114             self.destination_pykrita, '%s.desktop' % plugin['name'])
0115 
0116     def get_destination_actionfile(self, plugin):
0117         return os.path.join(
0118             self.destination_actions, '%s.action' % plugin['name'])
0119 
0120     def get_source_module(self, name):
0121         namelist = self.archive.namelist()
0122         for filename in namelist:
0123             if (filename.endswith('/%s/' % name)
0124                     or filename == '%s/' % name):
0125                 # Sanity check: There should be an __init__.py inside
0126                 if ('%s__init__.py' % filename) in namelist:
0127                     return filename
0128 
0129     def get_source_actionfile(self, name):
0130         for filename in self.action_filenames:
0131             _, actionfilename = os.path.split(filename)
0132             if actionfilename == '%s.action' % name:
0133                 return filename
0134 
0135     def read_desktop_config(self, desktop_filename):
0136         config = ConfigParser()
0137         try:
0138             config.read_string(
0139                 self.archive.read(desktop_filename).decode('utf-8'))
0140         except ConfigParserError as e:
0141             raise PluginReadError(
0142                 '%s: %s' % (i18n('Desktop file'), str(e)))
0143         return config
0144 
0145     def get_plugin_info(self):
0146         names = []
0147         for filename in self.desktop_filenames:
0148             config = self.read_desktop_config(filename)
0149             try:
0150                 name = config['Desktop Entry']['X-KDE-Library']
0151                 ui_name = config['Desktop Entry']['Name']
0152             except KeyError as e:
0153                 raise PluginReadError(
0154                     'Desktop file: Key %s not found' % str(e))
0155             module = self.get_source_module(name)
0156             if module:
0157                 names.append({
0158                     'name': name,
0159                     'ui_name': ui_name,
0160                     'desktop': filename,
0161                     'module': module,
0162                     'action': self.get_source_actionfile(name)
0163                 })
0164         return names
0165 
0166     def extract_desktop(self, plugin):
0167         with open(self.get_destination_desktop(plugin), 'wb') as f:
0168             f.write(self.archive.read(plugin['desktop']))
0169 
0170     def extract_module(self, plugin):
0171         with TemporaryDirectory() as tmp_dir:
0172             for name in self.archive.namelist():
0173                 if name.startswith(plugin['module']):
0174                     self.archive.extract(name, tmp_dir)
0175             module_dirname = os.path.join(
0176                 tmp_dir, *plugin['module'].split('/'))
0177             try:
0178                 shutil.rmtree(self.get_destination_module(plugin))
0179             except FileNotFoundError:
0180                 pass
0181             shutil.copytree(module_dirname,
0182                             self.get_destination_module(plugin))
0183 
0184     def extract_actionfile(self, plugin):
0185         with open(self.get_destination_actionfile(plugin), 'wb') as f:
0186             f.write(self.archive.read(plugin['action']))
0187 
0188     def extract_plugin(self, plugin):
0189         # Check if the plugin already exists in the source directory:
0190         if (os.path.exists(self.get_destination_desktop(plugin))
0191                 or os.path.exists(self.get_destination_module(plugin))):
0192             confirmed = self.confirm_overwrite_callback(plugin)
0193             if not confirmed:
0194                 return False
0195 
0196         self.extract_desktop(plugin)
0197         self.extract_module(plugin)
0198         if plugin['action']:
0199             self.extract_actionfile(plugin)
0200         return True
0201 
0202     def import_all(self):
0203         """Imports all plugins from the zip archive.
0204 
0205         Returns a list of imported plugins.
0206         """
0207 
0208         plugins = self.get_plugin_info()
0209         if not plugins:
0210             raise NoPluginsFoundException(i18n('No plugins found in archive'))
0211 
0212         imported = []
0213         for plugin in plugins:
0214             success = self.extract_plugin(plugin)
0215             if success:
0216                 imported.append(plugin)
0217 
0218         return imported
0219 
0220 
0221 if __name__ == '__main__':
0222     def callback(plugin):
0223         print('Overwriting plugin:', plugin['ui_name'])
0224         return True
0225 
0226     imported = PluginImporter(
0227         sys.argv[1], sys.argv[2], callback).import_all()
0228     for plugin in imported:
0229         print('Imported plugin:', plugin['ui_name'])