File indexing completed on 2024-05-12 05:46:38

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