From 5e461f7dd61bacd2896c16662ded2ab0c21353f8 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 08:53:44 +0100 Subject: [PATCH] =?UTF-8?q?Ecriture=20des=20fonctions=20d'acc=C3=A9s=20aux?= =?UTF-8?q?=20logos=20(et=20aux=20images)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_logos.py | 217 ++++++++++++++++++++++++++++++++-------- app/scodoc/sco_utils.py | 5 + 2 files changed, 180 insertions(+), 42 deletions(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index e29b5183b..0737e7f03 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -34,30 +34,188 @@ SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos """ import imghdr import os +import re +from pathlib import Path -from flask import abort, current_app +from flask import abort, current_app, url_for +from werkzeug.utils import secure_filename +from app import Departement, ScoValueError from app.scodoc import sco_utils as scu +from PIL import Image as PILImage + +GLOBAL = "_GLOBAL" # category for server level logos -def get_logo_filename(logo_type: str, scodoc_dept: str) -> str: - """return full filename for this logo, or "" if not found - an existing file with extension. - logo_type: "header" or "footer" - scodoc-dept: acronym +def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX): """ - # Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_), - # then in config dir /opt/scodoc-data/config/logos/ - for image_dir in ( - scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept, - scu.SCODOC_LOGOS_DIR, # global logos - ): - for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: - filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}") - if os.path.isfile(filename) and os.access(filename, os.R_OK): - return filename + "Recherche un logo 'name' existant. + Deux strategies: + si strict: + reherche uniquement dans le département puis si non trouvé au niveau global + sinon + On recherche en local au dept d'abord puis si pas trouvé recherche globale + quelque soit la stratégie, retourne None si pas trouvé + :param logoname: le nom recherche + :param dept_id: l'id du département dans lequel se fait la recherche (None si global) + :param strict: stratégie de recherche (strict = False => dept ou global) + :param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX) + :return: un objet Logo désignant le fichier image trouvé (ou None) + """ + logo = Logo(logoname, dept_id, prefix).select() + if logo is None and not strict: + logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select() + return logo - return "" + +def write_logo(stream, name, dept_id=None): + """Crée le fichier logo sur le serveur. + Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream""" + Logo(logoname=name, dept_id=dept_id).create(stream) + + +def list_logos(): + """Crée l'inventaire de tous les logos existants. + L'inventaire se présente comme un dictionnaire de dictionnaire de Logo: + [GLOBAL][name] pour les logos globaux + [dept_id][name] pour les logos propres à un département (attention id numérique du dept) + """ + inventory = {GLOBAL: _list_dept_logos()} # logos globaux (header / footer) + for dept in Departement.query.filter_by(visible=True).all(): + logos_dept = _list_dept_logos(dept_id=dept.id) + if logos_dept: + inventory[dept.acronym] = _list_dept_logos(dept.id) + return inventory + + +def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX): + """nventorie toutes les images existantes pour un niveau (GLOBAL ou un département). + retourne un dictionnaire de Logo [logoname] -> Logo + les noms des fichiers concernés doivent être de la forme: /. + : répertoire de recherche (déduit du dept_id) + : le prefix (LOGO_FILE_PREFIX pour les logos) + : un des suffixes autorisés + :param dept_id: l'id du departement concerné (si None -> global) + :param prefix: le préfixe utilisé + :return: le résultat de la recherche ou None si aucune image trouvée + """ + allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES) + filename_parser = re.compile(f"{prefix}([^.]*).({allowed_ext})") + logos = {} + path_dir = Path(scu.SCODOC_LOGOS_DIR) + if dept_id: + path_dir = Path( + os.path.sep.join( + [scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)] + ) + ) + if path_dir.exists(): + for entry in path_dir.iterdir(): + if os.access(path_dir.joinpath(entry).absolute(), os.R_OK): + result = filename_parser.match(entry.name) + if result: + logoname = result.group(1) + logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select() + return logos if len(logos.keys()) > 0 else None + + +class Logo: + """Responsable des opérations (select, create), du calcul des chemins et url + ainsi que de la récupération des informations sur un logp. + Usage: + logo existant: Logo(, , ...).select() (retourne None si fichier non trouvé) + logo en création: Logo(, , ...).create(stream) + Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations + select ou save (le format n'est pas encore connu à ce moement là) + """ + + def __init__(self, logoname, dept_id=None, prefix=scu.LOGO_FILE_PREFIX): + """Initialisation des noms et département des logos. + if prefix = None on recherche simplement une image 'logoname.*' + Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet + """ + self.logoname = secure_filename(logoname) + self.scodoc_dept_id = dept_id + self.prefix = prefix or "" + self.suffix = None + self.dimensions = None + if self.scodoc_dept_id: + self.dirpath = os.path.sep.join( + [ + scu.SCODOC_LOGOS_DIR, + scu.LOGOS_DIR_PREFIX + secure_filename(str(dept_id)), + ] + ) + else: + self.dirpath = scu.SCODOC_LOGOS_DIR + self.basepath = os.path.sep.join( + [self.dirpath, self.prefix + secure_filename(self.logoname)] + ) + self.filepath = None + self.filename = None + + def _set_format(self, fmt): + self.suffix = fmt + self.filepath = self.basepath + "." + fmt + self.filename = self.logoname + "." + fmt + + def _ensure_directory_exists(self): + "create enclosing directory if necessary" + if not Path(self.dirpath).exists(): + current_app.logger.info(f"sco_logos creating directory %s", self.dirpath) + os.mkdir(self.dirpath) + + def create(self, stream): + img_type = guess_image_type(stream) + if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: + abort(400, "type d'image invalide") + self._set_format(img_type) + self._ensure_directory_exists() + filename = self.basepath + "." + self.suffix + with open(filename, "wb") as f: + f.write(stream.read()) + current_app.logger.info(f"sco_logos.store_image %s", self.filename) + # erase other formats if they exists + for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): + try: + os.unlink(self.basepath + "." + suffix) + except IOError: + pass + + def select(self): + """ + Récupération des données pour un logo existant + il doit exister un et un seul fichier image parmi de suffixe/types autorisés + (sinon on prend le premier trouvé) + cette opération permet d'affiner le format d'un logo de format inconnu + """ + for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: + path = Path(self.basepath + "." + suffix) + if path.exists(): + self._set_format(suffix) + with open(self.filepath, "rb") as f: + img = PILImage.open(f) + self.dimensions = img.size + return self + return None + + def get_url(self): + """Retourne l'URL permettant d'obtenir l'image du logo""" + return url_for( + "scodoc.get_logo", + scodoc_dept=self.scodoc_dept_id, + name=self.logoname, + global_if_not_found=False, + ) + + def get_url_small(self): + """Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature""" + return url_for( + "scodoc.get_logo_small", + scodoc_dept=self.scodoc_dept_id, + name=self.logoname, + global_if_not_found=False, + ) def guess_image_type(stream) -> str: @@ -68,28 +226,3 @@ def guess_image_type(stream) -> str: if not fmt: return None return fmt if fmt != "jpeg" else "jpg" - - -def _ensure_directory_exists(filename): - "create enclosing directory if necessary" - directory = os.path.split(filename)[0] - if not os.path.exists(directory): - current_app.logger.info(f"sco_logos creating directory %s", directory) - os.mkdir(directory) - - -def store_image(stream, basename): - img_type = guess_image_type(stream) - if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: - abort(400, "type d'image invalide") - filename = basename + "." + img_type - _ensure_directory_exists(filename) - with open(filename, "wb") as f: - f.write(stream.read()) - current_app.logger.info(f"sco_logos.store_image %s", filename) - # erase other formats if they exists - for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): - try: - os.unlink(basename + "." + extension) - except IOError: - pass diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 861e9487f..c023c738f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -283,7 +283,12 @@ if not os.path.exists(SCO_TMP_DIR) and os.path.exists(Config.SCODOC_VAR_DIR): # ----- Les logos: /opt/scodoc-data/config/logos SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf +LOGOS_DIR_PREFIX = "logos_" +LOGO_FILE_PREFIX = "logo_" +# forme générale des noms des fichiers logos/background: +# SCODOC_LOGO_DIR/LOGO_FILE_PREFIX. (fichier global) ou +# SCODOC_LOGO_DIR/LOGOS_DIR_PREFIX/LOGO_FILE_PREFIX. (fichier départemental) # ----- Les outils distribués SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools")