From a5b373b8eeddcf5c5091b5bbdd8c189aaf9f970f Mon Sep 17 00:00:00 2001 From: lapineige Date: Fri, 13 Oct 2023 16:18:06 +0200 Subject: [PATCH] Create YunohostAppGenerator.py --- tools/yunopackage/YunohostAppGenerator.py | 577 ++++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 tools/yunopackage/YunohostAppGenerator.py diff --git a/tools/yunopackage/YunohostAppGenerator.py b/tools/yunopackage/YunohostAppGenerator.py new file mode 100644 index 00000000..cdd8536f --- /dev/null +++ b/tools/yunopackage/YunohostAppGenerator.py @@ -0,0 +1,577 @@ +#### Imports +import jinja2 as j2 +from flask import Flask, render_template, render_template_string, request, redirect, flash, send_file +from markupsafe import Markup # No longer imported from Flask +# Form libraries +from flask_wtf import FlaskForm +from wtforms import StringField, RadioField, SelectField, SubmitField, TextAreaField, BooleanField, SelectMultipleField +from wtforms.validators import DataRequired, InputRequired, Optional, Regexp, URL, Length +# Markdown to HTML - for debugging purposes +from misaka import Markdown, HtmlRenderer +# Managing zipfiles +import zipfile +from flask_cors import CORS +from urllib import parse +from secrets import token_urlsafe + +#### GLOBAL VARIABLES +YOLOGEN_VERSION = '0.7.5' +GENERATOR_DICT = {'GENERATOR_VERSION': YOLOGEN_VERSION} + +#### Create FLASK and Jinja Environments +url_prefix = '' +# url_prefix = '/yunohost-app-generator' + +# app = Flask(__name__) +app = Flask(__name__) # Blueprint('main', __name__, url_prefix=url_prefix) +app.config['SECRET_KEY'] = token_urlsafe(16) # Necessary for the form CORS +cors = CORS(app) + +environment = j2.Environment(loader=j2.FileSystemLoader("templates/")) + + +#### Custom functions + +# Define custom filter +@app.template_filter('render_markdown') +def render_markdown(text): + renderer = HtmlRenderer() + markdown = Markdown(renderer) + return markdown(text) + + +# Add custom filter +j2.filters.FILTERS['render_markdown'] = render_markdown + + +# Converting markdown to html +def markdown_file_to_html_string(file): + with open(file, 'r') as file: + markdown_content = file.read() + # Convert content from Markdown to HTML + html_content = render_markdown(markdown_content) + # Return Markdown and HTML contents + return markdown_content, html_content + + +### Forms + +## PHP forms +class Form_PHP_Config(FlaskForm): + php_config_file = SelectField('Type de fichier PHP :', choices=[ + ('php-fpm.conf', Markup('Fichier de configuration PHP complet (php-fpm.conf)')), + ('php_extra-fpm.conf', + Markup('Fichier de configuration PHP particulier (extra_php-fpm.conf)'))], + default='php_extra-fpm.conf', validators=[DataRequired()]) + ## TODO : figure out how to include these comments/title values + # 'title': 'Remplace la configuration générée par défaut par un fichier de configuration complet. À éviter si possible. + # 'title': "Choisir un fichier permettant un paramétrage d'options complémentaires. C'est généralement recommandé." + + php_config_file_content = TextAreaField("Saisissez le contenu du fichier de configuration PHP :", + validators=[Optional()], + render_kw={"class": "form-control", + "style": "width: 50%;height:11em;min-height: 5.5em; max-height: 55em;flex-grow: 1;box-sizing: border-box;", + "title": "TODO", + "placeholder": "; Additional php.ini defines, specific to this pool of workers. \n\nphp_admin_value[upload_max_filesize] = 100M \nphp_admin_value[post_max_size] = 100M"}) + + +class Form_PHP(Form_PHP_Config): + use_php = BooleanField('Nécessite PHP', default=False) + + +## NodeJS forms +class Form_NodeJS(FlaskForm): + use_nodejs = BooleanField('Nécessite NodeJS', default=False) + use_nodejs_version = StringField("Version de NodeJS :", + render_kw={"placeholder": "20", "class": "form-control", + "title": "Saisissez la version de NodeJS à installer. Cela peut-être une version majeure (ex: 20) ou plus précise (ex: 20.1)."}) # TODO : this should be validated using a regex, should be only numbers and any (≥0) number of dots in between + use_nodejs_needs_yarn = BooleanField('Nécessite Yarn', default=False, render_kw={ + "title": "Faut-il installer automatiquement Yarn ? Cela configurera les dépôts spécifiques à Yarn."}) + + +## Python forms + +class Form_Python(FlaskForm): + use_python = BooleanField('Nécessite Python', + default=False) ## TODO -> python3, python3-pip, python3-ven dependencies by default + python_dependencies_type = SelectField('Configuration des dépendances Python :', choices=[ + ('requirements.txt', Markup('Fichier requirements.txt')), + ('manual_list', 'Liste manuelle')], + default='requirements.txt', validators=[DataRequired(), Optional()]) + python_requirements = TextAreaField("La liste de dépendances inclue dans le fichier requirements.txt :", + render_kw={"class": "form-control", + "style": "width: 50%;height:5.5em;min-height: 5.5em; max-height: 55em;flex-grow: 1;box-sizing: border-box;", + "title": "Lister les dépendances à installer, une par ligne, avec un numéro de version derrière.\nEx: 'dépendance==1.0'.", + "placeholder": "tensorflow==2.3.1 \nuvicorn==0.12.2 \nfastapi==0.63.0"}) + python_dependencies_list = StringField("Liste de dépendances python :", + render_kw={"placeholder": "tensorflow uvicorn fastapi", + "class": "form-control", + "title": "Lister les dépendances à installer, séparées d'un espace."}) + + +## Manifest form +# Dependencies form +class DependenciesForm(FlaskForm): + auto_update = BooleanField("Activer le robot de mise à jour automatiques  :", default=False, + render_kw={ + "title": "Si le logiciel est disponible sur github et publie des releases ou des tags pour ses nouvelles versions, un robot proposera automatiquement des mises à jours."}) + + ## TODO + # These infos are used by https://github.com/YunoHost/apps/blob/master/tools/autoupdate_app_sources/autoupdate_app_sources.py + # to auto-update the previous asset urls and sha256sum + manifest version + # assuming the upstream's code repo is on github and relies on tags or releases + # See the 'sources' resource documentation for more details + + # autoupdate.strategy = "latest_github_tag" + + dependencies = StringField("Dépendances de l'application (liste des paquets apt) à installer :", + render_kw={"placeholder": "foo foo2.1-ext somerandomdep", "class": "form-control", + "title": "Lister les paquets dont dépend l'application, séparés par un espace."}) + + use_db = SelectField("Configurer une base de données :", choices=[ + ('false', "Non"), + ('mysql', "MySQL/MariaDB"), + ('postgresql', "PostgreSQL")], + default='false', + render_kw={"title": "L'application nécessite-t-elle une base de données ?"}) + + +# manifest +class manifestForm(DependenciesForm): + version = StringField('Version', validators=[Regexp('\d{1,4}.\d{1,4}(.\d{1,4})?(.\d{1,4})?~ynh\d+')], + render_kw={"class": "form-control", + "placeholder": "1.0~ynh1"}) + description_en = TextAreaField("Description en quelques lignes de l'application, en anglais :", + validators=[DataRequired()], + render_kw={"class": "form-control", "style": "resize: none;", + "title": "Explain in *a few (10~15) words* the purpose of the app \\" + "or what it actually does (it is meant to give a rough idea to users browsing a catalog of 100+ apps)"}) + description_fr = TextAreaField("Description en quelques lignes de l'application :", validators=[DataRequired()], + render_kw={"class": "form-control", "style": "resize: none;", + "title": "Expliquez en *quelques* (10~15) mots l'utilité de l'app \\" + "ou ce qu'elle fait (l'objectif est de donner une idée grossière pour des utilisateurs qui naviguent dans un catalogue de 100+ apps)"}) + + # TODO : handle multiple names separated by commas (.split(',') ? + maintainers = StringField('Mainteneurs et mainteneuses', render_kw={"class": "form-control", + "placeholder": "Généralement vous mettez votre nom ici… Si vous êtes d'accord ;)"}) # TODO : Usually you put your name here… if you like ;) + architectures = SelectMultipleField('Architectures supportées :', choices=[ + ('all', 'Toutes les architectures'), + ('amd64', 'amd64'), + ('arm64', 'arm64'), + ('i386', 'i386'), + ('todo', 'TODO : list more architectures')], + default=['all'], validators=[DataRequired()]) + yunohost_required_version = StringField('Mainteneurs et mainteneuses', render_kw={"class": "form-control", + "placeholder": "11.1.21", + "title": "Version minimale de Yunohost pour que l'application fonctionne."}) + + multi_instance = BooleanField("Application multi-instance", default=False, + render_kw={"class": "", + "title": "Peux-t-on installer simultannément plusieurs fois l'application sur un même serveur ?"}) + + ldap = SelectField('Integrate with LDAP (user can login using Yunohost credentials :', choices=[ + ('false', 'False'), + ('true', 'True'), + ('not_relevant', 'Not relevant')], default='not_relevant', validators=[DataRequired()], render_kw={ + "title": """Not to confuse with the "sso" key: the "ldap" key corresponds to wether or not a user *can* login on the app using its YunoHost credentials."""}) + sso = SelectField('Integrate with Yunohost SingleSignOn (SSO) :', choices=[ + ('false', 'False'), + ('true', 'True'), + ('not_relevant', 'Not relevant')], default='not_relevant', validators=[DataRequired()], render_kw={ + "title": """Not to confuse with the "ldap" key: the "sso" key corresponds to wether or not a user is *automatically logged-in* on the app when logged-in on the YunoHost portal."""}) + + license = StringField('Licence', validators=[DataRequired()], render_kw={"class": "form-control", + "placeholder": "GPL"}) + + website = StringField('Site web', validators=[URL(), Optional()], render_kw={"class": "form-control", + "placeholder": "https://awesome-app-website.com"}) + demo = StringField('Site de démonstration', validators=[URL(), Optional()], render_kw={"class": "form-control", + "placeholder": "https://awesome-app-website.com/demo"}) + admindoc = StringField("Documentation d'aministration", validators=[URL(), Optional()], + render_kw={"class": "form-control", + "placeholder": "https://awesome-app-website.com/doc/admin"}) + userdoc = StringField("Documentation d'utilisation", validators=[URL(), Optional()], + render_kw={"class": "form-control", + "placeholder": "https://awesome-app-website.com/doc/user"}) + code = StringField('Dépôt de code', validators=[URL(), Optional()], render_kw={"class": "form-control", + "placeholder": "https://awesome-app-website.com/get-the-code"}) + + data_dir = BooleanField("L'application nécessite un répertoire dédié pour ses données", default=False, + render_kw={"title": "Faut-il créer un répertoire /home/yunohost.app/votreApplication ?"}) + data_subdirs = StringField('Si nécessaire, lister les sous-répertoires à configurer :', + validators=[Optional()], render_kw={"class": "form-control", + "placeholder": "data, uploads, themes"}) + use_whole_domain = BooleanField("L'application nécessite d'utiliser tout un domaine (installation à la racine) :", + default=False, + render_kw={ + "title": "Doit-on installer l'application à la racine du domaine ? Sinon, on pourra l'installer dans un sous-dossier, par exemple /mon_app."}) + supports_change_url = BooleanField( + "L'application autorise le changement d'adresse (changement de domaine ou de chemin)", default=True, + render_kw={"title": "Faut-il permettre le changement d'URL pour l'application ? (fichier change_url)"}) + + needs_admin = BooleanField("L'application nécessite de configurer un compte d'administration :", default=False, + render_kw={"class": "", + "title": "Faut-il configurer un compte admin à l'installation ?"}) + + # admin_password_help_message = BooleanField("TODO  :", default=False, + # render_kw={"class": "", + # "title": "TODO"}) + + language = SelectMultipleField('Langues supportées :', choices=[ + ('en', 'English'), + ('fr', 'Français'), + ('en', 'Spanish'), + ('it', 'Italian'), + ('de', 'German'), + ('zh', 'Chinese'), + ('jp', 'Japanese'), + ('da', 'Danish'), + ('pt', 'Portugese'), + ('nl', 'Dutch'), + ('ru', 'Russian')], + default=['en'], validators=[DataRequired()]) + + default_language = SelectField('Langues par défaut :', choices=[ + ('en', 'English'), + ('fr', 'Français'), + ('en', 'Spanish'), + ('it', 'Italian'), + ('zh', 'Chinese'), + ('jp', 'Japanese'), + ('da', 'Danish'), + ('pt', 'Portugese'), + ('nl', 'Dutch'), + ('ru', 'Russian')], + default=['en']) + + visibility = RadioField("Visibilité de l'application :", choices=[ + ('admin', "Administrateur/administratrice uniquement"), + ('all_users', "Personnes connectées"), + ('visitors', "Publique")], + default='all_users', validators=[DataRequired()]) + + source_url = StringField("Code source ou exécutable de l'application", validators=[DataRequired(), URL()], + render_kw={"class": "form-control", + "placeholder": "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz"}) # Application source code URL + sha256sum = StringField('Empreinte du code source (format sha256sum)', + validators=[DataRequired(), Length(min=64, max=64)], + render_kw={"class": "form-control", + "placeholder": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "title": "Sha256sum of the archive. Should be 64 characters-long."}) # Source code hash (sha256sum format) + + +## Main form + +class appGeneratorForm(manifestForm, DependenciesForm, Form_PHP, Form_NodeJS, Form_Python): + app_name = StringField("Nom de l'application :", validators=[DataRequired()], + render_kw={"placeholder": "My Great App", "class": "form-control", + "title": "Définir le nom de l'application, affiché dans l'interface"}) + app_id = StringField( + Markup( + """Identifiant (id) de l'application (en minuscule et sans espaces) :"""), + validators=[DataRequired(), Regexp("[a-z_1-9]+.*(?