spiegel-keyman/linux/keyman-config/keyman_config/install_kmp.py
Eberhard Beilharz e5b9b81806
fix(linux): Gracefully handle makedirs failures
This change catches the exception we're getting if we try to create
a directory but e.g. there is already a file with that name. In this
case we now output an error message. This change also gracefully
handles failures when linking files.

Fixes #6285.
2022-02-24 18:13:03 +01:00

296 lines
12 KiB
Python
Executable file

#!/usr/bin/python3
import json
import logging
import os
import zipfile
from shutil import rmtree
from enum import Enum
from keyman_config import _, secure_lookup
from keyman_config.canonical_language_code_utils import CanonicalLanguageCodeUtils
from keyman_config.fcitx_util import is_fcitx_running, restart_fcitx
from keyman_config.get_kmp import get_keyboard_data, get_keyboard_dir, get_keyman_doc_dir
from keyman_config.get_kmp import get_keyman_font_dir, InstallLocation
from keyman_config.kmpmetadata import get_metadata, KMFileTypes
from keyman_config.convertico import extractico, checkandsaveico
from keyman_config.kvk2ldml import convert_kvk_to_ldml, output_ldml
from keyman_config.ibus_util import install_to_ibus, restart_ibus, get_ibus_bus
from keyman_config.gnome_keyboards_util import GnomeKeyboardsUtil, get_ibus_keyboard_id, is_gnome_shell
# TODO userdir install
# special processing for kmn if needed
# TODO optionally standardise throughout on variable names
# packageID for kmps and keyboardID for keyboards
# see https://docs.google.com/\
# document/d/1sj7W6pCiN-_iRss5iRdib1aHaSTmYoLIueQSKJeNy8Q/edit#heading=h.mq0rc28mf031
class InstallStatus(Enum):
Continue = 0
Warning = 1
Abort = 2
class InstallError(Exception):
"""Exception raised for errors in KMP installation.
Attributes:
status -- InstallStatus for what to do when the error occurrs
message -- explanation of the error
"""
def __init__(self, status, message):
self.status = status
self.message = message
class InstallKmp():
def __init__(self):
self.packageID = ''
self.packageDir = ''
self.kmpdocdir = ''
self.kmpfontdir = ''
def _check_keyman_dir(self, basedir, error_message):
# check if keyman subdir exists
keyman_dir = os.path.join(basedir, "keyman")
if os.path.isdir(keyman_dir):
# Check for write access of keyman dir to be able to create subdir
if not os.access(keyman_dir, os.X_OK | os.W_OK):
raise InstallError(InstallStatus.Abort, error_message)
else:
# Check for write access of basedir and create keyman subdir if we can
if not os.access(basedir, os.X_OK | os.W_OK):
raise InstallError(InstallStatus.Abort, error_message)
os.mkdir(keyman_dir)
def _extract_package_id(self, inputfile):
packageID, ext = os.path.splitext(os.path.basename(inputfile))
return packageID.lower()
def install_kmp_shared(self, inputfile, online=False, language=None):
"""
Install a kmp file to /usr/local/share/keyman
Args:
inputfile (str): path to kmp file
online (bool, default=False): whether to attempt to get online keyboard data
"""
self._check_keyman_dir(
'/usr/local/share',
_("You do not have permissions to install the keyboard files to the shared area "
"/usr/local/share/keyman"))
self._check_keyman_dir(
'/usr/local/share/doc',
_("You do not have permissions to install the documentation to the shared "
"documentation area /usr/local/share/doc/keyman"))
self._check_keyman_dir(
'/usr/local/share/fonts',
_("You do not have permissions to install the font files to the shared font area "
"/usr/local/share/fonts"))
return self._install_kmp(inputfile, online, language, InstallLocation.Shared)
def install_kmp_user(self, inputfile, online=False, language=None):
return self._install_kmp(inputfile, online, language, InstallLocation.User)
def _install_kmp(self, inputfile, online, language, area):
self.packageID = self._extract_package_id(inputfile)
self.packageDir = get_keyboard_dir(area, self.packageID)
self.kmpdocdir = get_keyman_doc_dir(area, self.packageID)
self.kmpfontdir = get_keyman_font_dir(area, self.packageID)
if not self._safeMakeDirs(self.packageDir):
return
if not os.path.isfile(inputfile):
message = _("File {kmpfile} doesn't exist").format(kmpfile=inputfile)
logging.error("install_kmp.py: %s", message)
raise InstallError(InstallStatus.Abort, message)
extract_kmp(inputfile, self.packageDir)
if is_fcitx_running():
restart_fcitx()
else:
restart_ibus()
info, _, _, keyboards, files = get_metadata(self.packageDir)
if keyboards:
logging.info("Installing %s", secure_lookup(info, 'name', 'description'))
if online:
process_keyboard_data(self.packageID, self.packageDir)
for kb in keyboards:
if kb['id'] != self.packageID:
process_keyboard_data(kb['id'], self.packageDir)
for f in files:
fpath = os.path.join(self.packageDir, f['name'])
ftype = f['type']
if ftype == KMFileTypes.KM_DOC or ftype == KMFileTypes.KM_IMAGE:
# Special handling of doc and images to hard link them into doc dir
logging.info("Installing %s as documentation", f['name'])
if self._safeMakeDirs(self.kmpdocdir):
kmpdocpath = os.path.join(self.kmpdocdir, f['name'])
self._safeLinkFile(fpath, kmpdocpath)
elif ftype == KMFileTypes.KM_FONT:
# Special handling of font to hard link it into font dir
logging.info("Installing %s as font", f['name'])
if self._safeMakeDirs(self.kmpfontdir):
fontpath = os.path.join(self.kmpfontdir, f['name'])
self._safeLinkFile(fpath, fontpath)
elif ftype == KMFileTypes.KM_OSK:
# Special handling to convert kvk into LDML
logging.info("Converting %s to LDML and installing both as as keyman file",
f['name'])
ldml = convert_kvk_to_ldml(fpath)
name, ext = os.path.splitext(f['name'])
ldmlfile = os.path.join(self.packageDir, name + ".ldml")
output_ldml(ldmlfile, ldml)
elif ftype == KMFileTypes.KM_ICON:
# Special handling of icon to convert to PNG
logging.info("Converting %s to PNG and installing both as keyman files",
f['name'])
checkandsaveico(fpath)
elif ftype == KMFileTypes.KM_SOURCE:
# TODO for the moment just leave it for ibus-kmfl to ignore if it doesn't load
pass
elif ftype == KMFileTypes.KM_KMX:
# Sanitize keyboard filename if not lower case
kmx_id, ext = os.path.splitext(os.path.basename(f['name']))
for kb in keyboards:
if kmx_id.lower() == kb['id'] and kmx_id != kb['id']:
os.rename(os.path.join(self.packageDir, f['name']),
os.path.join(self.packageDir, kb['id'] + '.kmx'))
fpath = os.path.join(self.packageDir, kb['id'] + '.kmx')
extractico(fpath)
return self.install_keyboards(keyboards, self.packageDir, language)
else:
logging.error("install_kmp.py: error: No kmp.json or kmp.inf found in %s", inputfile)
logging.info("Contents of %s:", inputfile)
for o in os.listdir(self.packageDir):
logging.info(o)
rmtree(self.packageDir)
message = _("No kmp.json or kmp.inf found in {packageFile}").format(
packageFile=inputfile)
raise InstallError(InstallStatus.Abort, message)
def _safeMakeDirs(self, dir):
if not os.path.isdir(dir):
try:
os.makedirs(dir)
except NotADirectoryError:
logging.error("Can't create directory %s", dir)
return None
return dir
def _safeLinkFile(self, source, target):
if os.path.isfile(target):
return
if os.path.exists(target):
logging.error("Can't link file %s to %s - something exists at the target", source, target)
return
os.link(source, target)
def _normalize_language(self, supportedLanguages, language):
if len(supportedLanguages) <= 0:
return ''
if not language:
return language
language = CanonicalLanguageCodeUtils.findBestTag(language, False, True)
for supportedLanguage in supportedLanguages:
id = CanonicalLanguageCodeUtils.findBestTag(supportedLanguage['id'], False, True)
if id == language:
return id
return None
def install_keyboards(self, keyboards, packageDir, language=None):
firstKeyboard = keyboards[0]
if secure_lookup(firstKeyboard, 'languages') and len(firstKeyboard['languages']) > 0:
language = self._normalize_language(firstKeyboard['languages'], language)
if is_fcitx_running():
return self._install_keyboards_to_fcitx()
elif is_gnome_shell():
return self._install_keyboards_to_gnome(keyboards, packageDir, language)
else:
return self._install_keyboards_to_ibus(keyboards, packageDir, language)
def _install_keyboards_to_ibus(self, keyboards, packageDir, language=None):
bus = get_ibus_bus()
if bus or os.environ.get('SUDO_USER'):
# install all kmx for first lang not just packageID
for kb in keyboards:
ibus_keyboard_id = get_ibus_keyboard_id(kb, packageDir, language)
install_to_ibus(bus, ibus_keyboard_id)
restart_ibus(bus)
if bus:
bus.destroy()
else:
logging.debug("could not install keyboards to IBus")
return ''
def _install_keyboards_to_gnome(self, keyboards, packageDir, language=None):
gnomeKeyboardsUtil = GnomeKeyboardsUtil()
sources = gnomeKeyboardsUtil.read_input_sources()
# install all kmx for first lang not just packageID
for kb in keyboards:
ibus_keyboard_id = get_ibus_keyboard_id(kb, packageDir, language)
input_source = ('ibus', ibus_keyboard_id)
if input_source not in sources:
sources.append(input_source)
gnomeKeyboardsUtil.write_input_sources(sources)
return ''
def _install_keyboards_to_fcitx(self):
return _('Please use fcitx5-configtool to add the keyboard to the desired group')
def extract_kmp(kmpfile, directory):
with zipfile.ZipFile(kmpfile, "r") as zip_ref:
zip_ref.extractall(directory)
def process_keyboard_data(keyboardID, packageDir):
kbdata = get_keyboard_data(keyboardID)
if kbdata:
if not os.path.isdir(packageDir) and os.access(os.path.join(packageDir, os.pardir), os.X_OK | os.W_OK):
try:
os.makedirs(packageDir)
except Exception as e:
logging.warning('Exception %s creating %s %s', type(e), packageDir, e.args)
if os.access(packageDir, os.X_OK | os.W_OK):
try:
with open(os.path.join(packageDir, keyboardID + '.json'), 'w') as outfile:
json.dump(kbdata, outfile)
logging.info("Installing api data file %s.json as keyman file", keyboardID)
except Exception as e:
logging.warning('Exception %s writing %s/%s.json %s', type(e), packageDir, keyboardID, e.args)
# else:
# message = "install_kmp.py: error: cannot download keyboard data so not installing."
# rmtree(kbdir)
# raise InstallError(InstallStatus.Abort, message)
def install_kmp(inputfile, online=False, sharedarea=False, language=None):
"""
Install a kmp file
Args:
inputfile (str): path to kmp file
online(bool, default=False): whether to attempt to get online keyboard data
sharedarea(bool, default=False): whether install kmp to shared area or user directory
"""
if sharedarea:
return InstallKmp().install_kmp_shared(inputfile, online, language)
else:
return InstallKmp().install_kmp_user(inputfile, online, language)