Krita Source Code Documentation
Loading...
Searching...
No Matches
plugin_importer.py
Go to the documentation of this file.
1# SPDX-FileCopyrightText: 2019 Rebecca Breu <rebecca@rbreu.de>
2
3# This file is part of Krita.
4
5# SPDX-License-Identifier: GPL-3.0-or-later
6
7"""This module provides the actual importing logic. See
8`:class:PluginImporter` for more info.
9
10For easy command line testing, call this module like this:
11 > python plugin_importer.py foo.zip /output/path
12"""
13
14from configparser import ConfigParser, Error as ConfigParserError
15import os
16import shutil
17import sys
18from tempfile import TemporaryDirectory
19import zipfile
20
21from builtins import i18n
22
23class PluginImportError(Exception):
24 """Base class for all exceptions of this module."""
25 pass
26
27
29 """No valid plugins can be found in the zip file."""
30 pass
31
32
34 """Zip file can't be read or its content can't be parsed."""
35 pass
36
37
39 """Import a Krita Python Plugin from a zip file into the given
40 directory.
41
42 The Importer makes barely any assumptions about the file structure
43 in the zip file. It will find one or more plugins with the
44 following strategy:
45
46 1. Find files with the ending `.desktop` and read the Python
47 module name from them
48 2. Find directories that correspond to the Python module names
49 and that contain an `__init__.py` file
50 3. Find files with ending `.action` that have matching
51 `<Action name=...>` tags (these files are optional)
52 4. Extract the desktop- and action-files and the Python module
53 directories into the corresponding pykrita and actions folders
54
55 Usage:
56
57 >>> importer = PluginImporter(
58 '/path/to/plugin.zip',
59 '/path/to/krita/resources/',
60 confirm_overwrite_callback)
61 >>> imported = importer.import_all()
62
63 """
64
65 def __init__(self, zip_filename, resources_dir,
66 confirm_overwrite_callback):
67
68 """Initialise the importer.
69
70 :param zip_filename: Filename of the zip archive containing the
71 plugin(s)
72 :param resources_dir: The Krita resources directory into which
73 to extract the plugin(s)
74 :param confirm_overwrite_callback: A function that gets called
75 if a plugin already exists in the resources directory. It gets
76 called with a dictionary of information about the plugin and
77 should return whether the user wants to overwrite the plugin
78 (True) or not (False).
79 """
80
81 self.resources_dir = resources_dir
82 self.confirm_overwrite_callback = confirm_overwrite_callback
83 try:
84 self.archive = zipfile.ZipFile(zip_filename)
85 except(zipfile.BadZipFile, zipfile.LargeZipFile, OSError) as e:
86 raise PluginReadError(str(e))
87
90 for filename in self.archive.namelist():
91 if filename.endswith('.desktop'):
92 self.desktop_filenames.append(filename)
93 if filename.endswith('.action'):
94 self.action_filenames.append(filename)
95
96 @property
98 dest = os.path.join(self.resources_dir, 'pykrita')
99 if not os.path.exists(dest):
100 os.mkdir(dest)
101 return dest
102
103 @property
105 dest = os.path.join(self.resources_dir, 'actions')
106 if not os.path.exists(dest):
107 os.mkdir(dest)
108 return dest
109
110 def get_destination_module(self, plugin):
111 return os.path.join(self.destination_pykritadestination_pykrita, plugin['name'])
112
113 def get_destination_desktop(self, plugin):
114 return os.path.join(
115 self.destination_pykritadestination_pykrita, '%s.desktop' % plugin['name'])
116
117 def get_destination_actionfile(self, plugin):
118 return os.path.join(
119 self.destination_actionsdestination_actions, '%s.action' % plugin['name'])
120
121 def get_source_module(self, name):
122 namelist = self.archive.namelist()
123 for filename in namelist:
124 if (filename.endswith('/%s/' % name)
125 or filename == '%s/' % name):
126 # Sanity check: There should be an __init__.py inside
127 if ('%s__init__.py' % filename) in namelist:
128 return filename
129
130 def get_source_actionfile(self, name):
131 for filename in self.action_filenames:
132 _, actionfilename = os.path.split(filename)
133 if actionfilename == '%s.action' % name:
134 return filename
135
136 def read_desktop_config(self, desktop_filename):
137 config = ConfigParser()
138 try:
139 config.read_string(
140 self.archive.read(desktop_filename).decode('utf-8'))
141 except ConfigParserError as e:
142 raise PluginReadError(
143 '%s: %s' % (i18n('Desktop file'), str(e)))
144 return config
145
147 names = []
148 for filename in self.desktop_filenames:
149 config = self.read_desktop_config(filename)
150 try:
151 name = config['Desktop Entry']['X-KDE-Library']
152 ui_name = config['Desktop Entry']['Name']
153 except KeyError as e:
154 raise PluginReadError(
155 'Desktop file: Key %s not found' % str(e))
156 module = self.get_source_module(name)
157 if module:
158 names.append({
159 'name': name,
160 'ui_name': ui_name,
161 'desktop': filename,
162 'module': module,
163 'action': self.get_source_actionfile(name)
164 })
165 return names
166
167 def extract_desktop(self, plugin):
168 with open(self.get_destination_desktop(plugin), 'wb') as f:
169 f.write(self.archive.read(plugin['desktop']))
170
171 def extract_module(self, plugin):
172 with TemporaryDirectory() as tmp_dir:
173 for name in self.archive.namelist():
174 if name.startswith(plugin['module']):
175 self.archive.extract(name, tmp_dir)
176 module_dirname = os.path.join(
177 tmp_dir, *plugin['module'].split('/'))
178 try:
179 shutil.rmtree(self.get_destination_module(plugin))
180 except FileNotFoundError:
181 pass
182 shutil.copytree(module_dirname,
183 self.get_destination_module(plugin))
184
185 def extract_actionfile(self, plugin):
186 with open(self.get_destination_actionfile(plugin), 'wb') as f:
187 f.write(self.archive.read(plugin['action']))
188
189 def extract_plugin(self, plugin):
190 # Check if the plugin already exists in the source directory:
191 if (os.path.exists(self.get_destination_desktop(plugin))
192 or os.path.exists(self.get_destination_module(plugin))):
193 confirmed = self.confirm_overwrite_callback(plugin)
194 if not confirmed:
195 return False
196
197 self.extract_desktop(plugin)
198 self.extract_module(plugin)
199 if plugin['action']:
200 self.extract_actionfile(plugin)
201 return True
202
203 def import_all(self):
204 """Imports all plugins from the zip archive.
205
206 Returns a list of imported plugins.
207 """
208
209 plugins = self.get_plugin_info()
210 if not plugins:
211 raise NoPluginsFoundException(i18n('No plugins found in archive'))
212
213 imported = []
214 for plugin in plugins:
215 success = self.extract_plugin(plugin)
216 if success:
217 imported.append(plugin)
218
219 return imported
220
221
222if __name__ == '__main__':
223 def callback(plugin):
224 print('Overwriting plugin:', plugin['ui_name'])
225 return True
226
227 imported = PluginImporter(
228 sys.argv[1], sys.argv[2], callback).import_all()
229 for plugin in imported:
230 print('Imported plugin:', plugin['ui_name'])
__init__(self, zip_filename, resources_dir, confirm_overwrite_callback)