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