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'])