Compare commits
2 commits
main
...
feature-co
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d709316183 | ||
![]() |
41d6bc9d11 |
37 changed files with 816 additions and 536 deletions
14
.github/workflows/main.yml
vendored
Normal file
14
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: docker
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build the Docker image
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
|
||||
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
|
||||
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -15,5 +15,5 @@ blog.sublime-project
|
|||
blog.sublime-workspace
|
||||
ssl/
|
||||
.python-version
|
||||
.local
|
||||
uv.lock
|
||||
poetry.toml
|
||||
.local
|
19
.travis.yml
Normal file
19
.travis.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
|
||||
install:
|
||||
- pip install commonmark coverage coveralls
|
||||
|
||||
script:
|
||||
- python -m unittest discover -bv
|
||||
- coverage run --branch --source=. -m unittest discover -bv
|
||||
- coverage report -m
|
||||
|
||||
after_success:
|
||||
- coveralls
|
31
Dockerfile
31
Dockerfile
|
@ -1,12 +1,35 @@
|
|||
FROM nginx:1.27.4-alpine-slim
|
||||
FROM nginx:1.19.0-alpine
|
||||
|
||||
RUN apk update && apk add --no-cache bash git python3 make tzdata curl py3-pip musl-locales
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && mv /root/.local/bin/uv /usr/local/bin
|
||||
RUN apk update
|
||||
RUN apk add --no-cache build-base bash git python3 make tzdata curl py3-pip libressl-dev musl-dev libffi-dev python3-dev cargo
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
# install poetry
|
||||
#ENV POETRY_HOME=/opt/poetry
|
||||
#RUN python3 -m venv $POETRY_HOME
|
||||
#RUN $POETRY_HOME/bin/pip install --upgrade pip
|
||||
#RUN $POETRY_HOME/bin/pip install setuptools_rust poetry==1.4.0
|
||||
#RUN $POETRY_HOME/bin/poetry --version
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# install locales
|
||||
ENV MUSL_LOCALE_DEPS cmake make musl-dev gcc gettext-dev libintl
|
||||
ENV MUSL_LOCPATH /usr/share/i18n/locales/musl
|
||||
|
||||
RUN apk add --no-cache \
|
||||
$MUSL_LOCALE_DEPS \
|
||||
&& wget https://gitlab.com/rilian-la-te/musl-locales/-/archive/master/musl-locales-master.zip \
|
||||
&& unzip musl-locales-master.zip \
|
||||
&& cd musl-locales-master \
|
||||
&& cmake -DLOCALE_PROFILE=OFF -D CMAKE_INSTALL_PREFIX:PATH=/usr . && make && make install \
|
||||
&& cd .. && rm -r musl-locales-master
|
||||
|
||||
# set timezone and locale
|
||||
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo "Europe/Paris" > /etc/timezone
|
||||
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime
|
||||
RUN echo "Europe/Paris" > /etc/timezone
|
||||
ENV TZ Europe/Paris
|
||||
ENV LANG fr_FR.UTF-8
|
||||
ENV LANGUAGE fr_FR.UTF-8
|
||||
|
|
52
Makefile
52
Makefile
|
@ -1,6 +1,5 @@
|
|||
# Makefile
|
||||
|
||||
.PHONY: build
|
||||
# Makefile
|
||||
#
|
||||
|
||||
# if a file .local exists run site locally
|
||||
ifeq ($(wildcard .local),)
|
||||
|
@ -9,7 +8,7 @@ else
|
|||
TARGET = site_local
|
||||
endif
|
||||
|
||||
site: site_local
|
||||
site: $(TARGET)
|
||||
echo $(TARGET)
|
||||
|
||||
site_remote:
|
||||
|
@ -18,14 +17,43 @@ site_remote:
|
|||
systemctl reload nginx
|
||||
|
||||
site_local:
|
||||
uv run python makesite.py --params params-local.json
|
||||
rye run python makesite.py --params params-local.json
|
||||
cd _site && python -m SimpleHTTPServer 2> /dev/null || python3 -m http.server
|
||||
|
||||
# docker build
|
||||
build:
|
||||
docker build -t source.madyanne.fr/yax/blog .
|
||||
|
||||
# docker publish
|
||||
publish:
|
||||
docker push source.madyanne.fr/yax/blog
|
||||
dock: site_local
|
||||
$(shell docker start --interactive bloglocal || docker run --name bloglocal -p 80:80 -p 443:443 -v `pwd`/_site:/usr/share/nginx/html:ro -v `pwd`/nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v `pwd`/nginx/dhparam.pem:/etc/nginx/dhparam.pem:ro -v `pwd`/ssl:/etc/nginx/ssl:ro nginx)
|
||||
|
||||
undock:
|
||||
docker stop bloglocal
|
||||
|
||||
certs:
|
||||
mkdir -p ssl
|
||||
cd ssl ; wget -N https://traefik.me/cert.pem
|
||||
cd ssl ; wget -N https://traefik.me/chain.pem
|
||||
cd ssl ; wget -N https://traefik.me/fullchain.pem
|
||||
cd ssl ; wget -N https://traefik.me/privkey.pem
|
||||
|
||||
venv2:
|
||||
virtualenv ~/.venv/makesite
|
||||
echo . ~/.venv/makesite/bin/activate > venv
|
||||
. ./venv && pip install commonmark coverage
|
||||
|
||||
venv: FORCE
|
||||
python3 -m venv ~/.venv/makesite
|
||||
echo . ~/.venv/makesite/bin/activate > venv
|
||||
. ./venv && pip install commonmark coverage
|
||||
|
||||
test: FORCE
|
||||
. ./venv && python -m unittest -bv
|
||||
|
||||
coverage:
|
||||
. ./venv && coverage run --branch --source=. -m unittest discover -bv; :
|
||||
. ./venv && coverage report -m
|
||||
. ./venv && coverage html
|
||||
|
||||
clean:
|
||||
find . -name "__pycache__" -exec rm -r {} +
|
||||
find . -name "*.pyc" -exec rm {} +
|
||||
rm -rf .coverage htmlcov
|
||||
|
||||
FORCE:
|
||||
|
|
|
@ -7,4 +7,3 @@ This static blog generator code is under MIT license.
|
|||
|
||||
"Blog du Yax" content is under [CC-BY-NC-SA](https://creativecommons.org/licenses/by-nc-sa/3.0)
|
||||
|
||||
Moved as private repository to gitea.zaclys.net on 2025-03-29.
|
||||
|
|
2
build.sh
Executable file
2
build.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
pyinstaller makesite.py --onefile
|
|
@ -1,19 +1,20 @@
|
|||
#!/bin/bash
|
||||
|
||||
python -V
|
||||
#export POETRY_HOME=/opt/poetry
|
||||
|
||||
# clone and build blog
|
||||
cd /
|
||||
rm -rf /blog
|
||||
git clone https://gitea.zaclys.com/yannic/blog.git
|
||||
git clone https://github.com/kianby/blog.git
|
||||
cd /blog
|
||||
uv python pin 3.12.9
|
||||
uv sync
|
||||
uv run python ./makesite.py
|
||||
#$POETRY_HOME/bin/poetry install
|
||||
#$POETRY_HOME/bin/poetry run make
|
||||
poetry install
|
||||
poetry run make
|
||||
|
||||
# nginx serve
|
||||
#nginx -g 'daemon off;'
|
||||
nginx
|
||||
|
||||
# exit on change in stacosys or Git repo
|
||||
uv run python monitor.py
|
||||
$POETRY_HOME/bin/poetry run python3 monitor.py
|
|
@ -45,15 +45,6 @@ http {
|
|||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# redirections to preserve inbound links
|
||||
rewrite ^/migration-du-blog-sous-pelican.html$ https://blogduyax.madyanne.fr/2013/migration-du-blog-sous-pelican/ redirect;
|
||||
rewrite ^/2017/nextcloud-securite/$ https://blogduyax.madyanne.fr/2017/securite-des-donnees-focus-sur-nextcloud/ redirect;
|
||||
rewrite ^/installation-de-shinken.html$ https://blogduyax.madyanne.fr/2014/installation-de-shinken/ redirect;
|
||||
rewrite ^/retour-dexperience-ubuntu-touch.html$ https://blogduyax.madyanne.fr/2015/retour-dexperience-ubuntu-touch/ redirect;
|
||||
rewrite ^/haute-disponibilite-avec-corosync-et-pacemaker.html$ https://blogduyax.madyanne.fr/2013/haute-disponibilite-avec-corosync-et-pacemaker/ redirect;
|
||||
rewrite ^/feeds/all.atom.xml$ https://blogduyax.madyanne.fr/rss.xml redirect;
|
||||
rewrite ^/feed.xml$ https://blogduyax.madyanne.fr/rss.xml redirect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
22
makesite.py
22
makesite.py
|
@ -60,6 +60,7 @@ class HighlightRenderer(mistune.HTMLRenderer):
|
|||
return highlight(code, lexer, formatter)
|
||||
return '<pre><code>' + mistune.escape(code) + '</code></pre>'
|
||||
|
||||
|
||||
markdown = mistune.create_markdown(renderer=HighlightRenderer())
|
||||
|
||||
|
||||
|
@ -79,9 +80,9 @@ def fwrite(filename, text):
|
|||
f.write(text)
|
||||
|
||||
|
||||
def log(msg, *args):
|
||||
def log(msg, *log_args):
|
||||
"""Log message with specified arguments."""
|
||||
sys.stderr.write(msg.format(*args) + "\n")
|
||||
sys.stderr.write(msg.format(*log_args) + "\n")
|
||||
|
||||
|
||||
def truncate(text, words=25):
|
||||
|
@ -99,10 +100,12 @@ def read_headers(text):
|
|||
|
||||
def rfc_2822_format(date_str):
|
||||
"""Convert yyyy-mm-dd date string to RFC 2822 format date string."""
|
||||
# Use datetime.datetime.strptime() with the correct format string
|
||||
d = datetime.datetime.strptime(date_str, "%Y-%m-%d")
|
||||
dtuple = d.timetuple()
|
||||
dtimestamp = time.mktime(dtuple)
|
||||
return utils.formatdate(dtimestamp)
|
||||
# Convert the timetuple to a timestamp
|
||||
timestamp = time.mktime(d.timetuple())
|
||||
# Return the formatted timestamp
|
||||
return utils.formatdate(timestamp)
|
||||
|
||||
|
||||
def slugify(value):
|
||||
|
@ -111,11 +114,10 @@ def slugify(value):
|
|||
underscores) and converts spaces to hyphens. Also strips leading and
|
||||
trailing whitespace.
|
||||
"""
|
||||
value = (
|
||||
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
|
||||
)
|
||||
value = re.sub("[^\w\s-]", "", value).strip().lower()
|
||||
return re.sub("[-\s]+", "-", value)
|
||||
value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
|
||||
value = re.sub(r"[^\w\s-]", "", value) # Remove non-word characters and spaces
|
||||
value = re.sub(r"\s+", "-", value) # Replace multiple spaces with a single hyphen
|
||||
return value.lower() # Convert to lowercase
|
||||
|
||||
|
||||
def read_content(filename):
|
||||
|
|
44
makesite.spec
Normal file
44
makesite.spec
Normal file
|
@ -0,0 +1,44 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['makesite.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='makesite',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
|
@ -12,10 +12,6 @@ Supprimer les dépendances des paquets orphelins
|
|||
|
||||
pacman -Rsn $(pacman -Qtdq)
|
||||
|
||||
Nettoyage du cache pacman des archives de +90 jours
|
||||
|
||||
find ./ -maxdepth 1 -type f -mtime +90 -print0 | xargs -0 sudo /bin/rm -f
|
||||
|
||||
## *Downgrader* des paquets
|
||||
|
||||
Récupérer la liste des upgraded ([source](https://wiki.archlinux.org/title/Downgrading_packages))
|
||||
|
@ -40,7 +36,6 @@ for i in $(cat /tmp/packages); do sudo pacman --noconfirm -U "$i"*.zst; done
|
|||
|
||||
(potentiel problème d'ordre, réarranger le fichier /tmp/packages en fonction des dépendances entre les paquets)
|
||||
|
||||
|
||||
# Fedora
|
||||
|
||||
Historique des transactions **dnf**
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<!-- title: Thème : cap au Nord -->
|
||||
<!-- category: GNU/Linux -->
|
||||
|
||||
Je ne pratique pas le *ricing* et je me contente d'un thème par défaut sur mes environnements de bureau, que ce soit Gnome, Mate ou XFCE, surtout que les thèmes fournis par défaut sont généralement bien soignés de nos jours. Mais il y a un domaine où j'ai mes exigences, c'est le terminal : c'est important de ne pas s'esquinter les yeux. Or une palette de couleur mal choisie peut compliquer la vie avec la coloration syntaxique à gogo. D'abord séduit par le classieux Solaris Dark je me suis tourné depuis deux ans vers le thème Nord. Moins contrasté et très homogène, je l'ai progressivement intégré à mes applications quotidiennes : Alacritty, Tmux, NeoVim, IntelliJ, Firefox, la page de recherche DuckDuckGo, le thème GTK. Bref je ne fais pas de *ricing* mais j'ai déjà abordé quelques applications de bureau.
|
||||
|
||||
Mes sources d'informations sont les suivantes :
|
||||
- Sven Greb, celui par qui [tout a commencé](https://github.com/orgs/nordtheme/discussions/183) et son site officiel [NordTheme](https://www.nordtheme.com/)
|
||||
- la communauté sur Reddit : [r/nordtheme](https://www.reddit.com/r/nordtheme/)
|
||||
|
||||
et la configuration de mes postes (toujours gérée par [ChezMoi](https://www.chezmoi.io/)) est sur [mon GitHub](https://github.com/kianby/dotfiles) et répliquée sur [mon Gitea](https://source.madyanne.fr/yax/dotfiles).
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<!-- title: Retour en auto-hébergement -->
|
||||
<!-- category: Hébergement -->
|
||||
|
||||
Après une dizaine d'années à héberger mes services chez différents hébergeurs je reviens à de l'auto-hébergement sur du matériel à la maison. Et quelle évolution au niveau matériel : en 2013 c'était un portable Céléron peut véloce dans un placard. En cette fin d'année j'ai acquis un mini-pc sur les conseils avisés de [MiniMachines](https://www.minimachines.net) : un Beelink Mini S12 Pro: pour la taille d'un Rubik's Cube on a un processeur Intel N100 (de la famille Alder Lake) appuyé par 16 Go de RAM et 512 Go de stockage en M.2. Ça coûte un peu plus de 200 euros et c'est très raisonnable par rapport à ce qu'on obtient en retour : une machine avec une consommation électrique sobre, quasi-silencieuse et des performances suffisantes pour des usages standards en auto-hébergement.
|
||||
|
||||
Pour moi l'objectif est double :
|
||||
- financier d'abord avec l'opportunité de réduire mes factures bien que je sois très satisfait de mes services externes : résilier le cloud Infomaniak et le petit VPS me fera économiser d'une bonne centaine d'euros par an.
|
||||
- apprendre en s'amusant : refaire de l'administration système, découvrir des nouveaux services à héberger, ça ouvre plein de possibilités.
|
||||
|
||||
Joli projet sur le papier mais moins simple qu'en 2012 car l'Internet est sacrément plus hostile et une grosse réflexion sur la sécurité s'est imposée avant d’ouvrir quoi que ce soit sur Internet. Finalement la solution sera basée sur l'hyperviseur Proxmox qui apporte une souplesse sur les types de déploiement en permettant de mixer des conteneurs LXC et des machines virtuelles KVM et d'apporter une brique de sécurité avec un pare-feu à multiple niveaux. Le but étant d'isoler autant que possible les parties exposées du réseau domestique. Les machines virtuelles [regrouperont des services Docker](https://github.com/kianby/selfhosting/tree/config-vm1) exposés indirectement par un proxy NginX.
|
||||
|
||||

|
||||
|
||||
Le proxy NginX est directement exposé sur Internet via une redirection des ports HTTP / HTTPs depuis la box internet. C'est un container LXC Alpine avec une installation de [Nginx Proxy Manager](https://nginxproxymanager.com/) modifiée pour que les services ne s'exécutent par avec le super-utilisateur *root*. Le minimum de paquets est installé (surtout pas de service SSHD ni de SSH) et il s'administre par l'interface Web de Nginx Proxy Manager depuis le réseau local qui n'est évidemment pas exposée à l'extérieur. Il a deux cartes réseau virtuelles : une adresse sur le réseau local et l'autre sur le réseau privé constitué des machines virtuelles exécutant les services. Le proxy sert aussi de passerelle de sortie aux machines virtuelles : le routage est activé entre les deux interfaces et l'interface du réseau privé est *bridgée* sur l'interface réseau locale.
|
||||
|
||||
Les machines virtuelles exécutant les services appartiennent au réseau privé et le pare-feu de l'hyperviseur bloque le trafic pour qu'elles ne puisse communiquer qu'avec la passerelle. En cas de compromission elles n'ont accès ni au réseau local ni au bastion. Chaque machine virtuelle est accessible par SSH depuis le bastion, qui est un simple conteneur LXC Alpine avec deux cartes réseau qui permet par rebond d'accéder aux machines virtuelles depuis un PC du réseau local. Excepté la console Web de Proxmox c'est le seul moyen d'accéder aux machines virtuelles. Les accès SSH sont protégés par échange de clefs (aucun accès autorisé par mot de passe) et seul le bastion est autorisé. Corollaire : n'importe quelle machine du réseau local ayant un accès SSH au bastion peut accéder aux machines virtuelles.
|
||||
|
||||

|
||||
|
||||
Voilà c'est sûrement perfectible mais j'ai jugé la solution suffisamment sécurisée pour la mettre en service. La machine "vm2" avec Nextcloud et Immlich est encore un projet mais la machine "vm1" exécutant tous mes services de base est déjà opérationnelle.
|
||||
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<!-- title: Analyse de logs -->
|
||||
<!-- category: Hébergement -->
|
||||
|
||||
Après quelques semaines [sur ma nouvelle installation de serveur](/2024/retour-en-auto-hebergement/) j'ai eu besoin de visibilité sur les visites pour estimer si la sécurité devait être renforcée. De longue date j'avais mis en favori l'outil [GoAccess](https://goaccess.io) et il semblait correspondre à ce que je cherchais, à savoir un outil passif de génération de rapports basé sur les logs d'accès du serveur HTTP : pas d'analyse des IP source mais des statistiques d'accès par site, une répartition des *User Agent* utilisés avec une reconnaissance des bots (ou crawlers) qui représentent le plus gros du trafic, les erreurs HTTP, les URI les plus recherchées ...
|
||||
|
||||
A ma grande satisfaction j'ai mis en place GoAccess en moins de deux heures avec deux étapes :
|
||||
1. l'installation sur le container LXC Alpine. GoAccess est écrit en langage C et a peu de dépendances pour la compilation. Avec deux ou trois dépendances de librairies ajoutées, la compilation sur le container a été simple.
|
||||
2. le travail principal consiste à décrire le format du log à analyser dans le formalisme de GoAccess
|
||||
|
||||
Nginx Proxy Manager utilise un format de log commun pour tous les proxy ce qui m'a simplifié la tâche. La documentation de GoAccess est exemplaire, notamment le formalisme des logs (https://goaccess.io/man#custom-log).
|
||||
|
||||
Pour ce format de log Nginx :
|
||||
|
||||
```
|
||||
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
|
||||
```
|
||||
|
||||
j'ai défini ces paramètres de lancement :
|
||||
|
||||
```
|
||||
--datetime-format='%d/%b/%Y:%H:%M:%S %z'
|
||||
--log-format='[%x] - %^ %s - %m %^ %v "%U" [Client %h] [Length %b] [Gzip %^] [Sent-to %^] "%u" "%R"'
|
||||
```
|
||||
|
||||
GoAccess peut générer son rapport HTML à partir d'une liste de fichiers de logs à la demande ou en temps-réel avec un lancement en tâche de fond (option --real-time-html --daemonize). L'option temps-réel est sympa avec une web socket qui rafraîchit la page du navigateur automatiquement mais c'est consommateur en CPU sur ce type d'installation en mini-pc où l'un des objectifs est la sobriété énergétique. J'ai préféré opter pour une génération horaire par une tâche planifiée avec CRON.
|
||||
|
||||
GoAccess répond à mon besoin et j'ai commencé à analyser les données pour préparer une phase de renforcement de la sécurité.
|
|
@ -1,8 +0,0 @@
|
|||
<!-- title: Est-ce que tout est politique ? -->
|
||||
<!-- category: Humeur -->
|
||||
|
||||
Le retour en auto-hébergement avec [l'achat d'un mini-pc](/2024/retour-en-auto-hebergement/) a été ma meilleure idée de fin d'année. Et ce n'est pas suffisant car j'ai encore beaucoup de dépendance à des services U.S. Même si je voyais d'un œil inquiet la réélection de Donald (pas le canard rigolo qui a bercé mon enfance... l'autre), jamais je n'aurais imaginé le système fasciste qui se met en place depuis le début d'année. Naïf, je pensais qu'il y avait des garde-fous, qu'on ne pouvait pas poignarder un système démocratique sans qu'une instance siffle un arrêt de jeu, mais je me trompais lourdement. Je ne rentrerais pas dans le détail des mesures appliquées par ce nouveau gouvernement sinon cet article va devenir trop triste ; j'ai juste le cœur qui saigne pour tous ceux qui en souffrent, sûrement une vilaine faiblesse de mon côté woke ;-)
|
||||
|
||||
Depuis tant d'années nous baignons dans ce monde du partage communautaire et multiculturel autour de Linux, des projets libres et opensource. Alors est-ce qu'on fait de la politique ? Pas forcément consciemment mais nous essayons de vivre en accord avec nos valeur et nos goûts reflètent une vision du monde (pas tel qu'il est malheureusement mais plutôt comme nous le rêvons). Le durcissement des gouvernements au détriment de l'humain (en piétinant d'abord les minorités) et de la nature, le repli en mode écureuil (tiens si je prends le Groenland je pourrais assurer le même train de vie pendant encore quelques dizaines d'année et faire croire que tout va bien ?) nous expose peu à peu. Si on n'est pas d'accord avec cela car nous avons d'autres valeurs plus humanistes alors je crains que nous ayons une posture politique à l'insu de nous-mêmes même :-) Le fait qu'on ait jamais voté ou qu'on soit fidèle aux urnes ne change rien à l'affaire : il y a une majorité silencieuse qui n'approuve pas, ne défile pas, du moins j'aime à le croire. Quelle est la limite qui fait basculer de l'indignation à l'action, je me pose la question... à moi-même en premier lieu.
|
||||
|
||||
L'année va être intéressante mais pas forcément joyeuse. Prenez soin de vous, mentalement aussi, car la période est malsaine.
|
|
@ -1,20 +0,0 @@
|
|||
<!-- title: Migration Git et optimisation Docker -->
|
||||
<!-- category: Hébergement -->
|
||||
|
||||
|
||||
|
||||
J'ai quitté GitHub (entre autres) ! Enfin presque car je n'ai pas encore recensé les sites où mon compte GitHub sert d'authentification mais j'ai fait le plus dur : supprimer tous mes projets et ne migrer que ceux encore actifs ; un peu dur car supprimer des projets, mêmes archivés, pour un développeur c'est un crève-cœur mais le spleen est vite passé avec l'engouement de repartir à zéro sur une plate-forme plus conforme à mes valeurs, la forge de [Zaclys](https://www.zaclys.com) et je les remercie grandement d'avoir ajouté ce service à leur offre "Famille".
|
||||
|
||||
Après migration, il me reste quatre projets actifs (hé oui il ne reste que ça) à savoir mes configurations (dotfiles), le déploiement par docker compose (selfhosting), le gestionnaire de commentaires (stacosys) et le blog. Vous avez noté la subtilité pour être retrouvé sur la [forge de Zaclys](https://gitea.zaclys.com) sans donner explicitement le lien de mon espace dans l'article ;-)
|
||||
|
||||
Sur mon infrastructure domestique (le mini-pc Beelink propulsé par l'hyperviseur Proxmox) j'ai déjà une instance Gitea qui réplique mes projets à l'extérieur, ça me garantit d'avoir une copie à jour en cas de gros pépin. Bref après avoir migré mes projets je me suis trouvé devant la problématique de construire et publier les images Docker de stacosys et du blog puisque cette tâche était dévolue aux actions GitHub. Je ne sais pas si la forge Zaclys propose un équivalent mais je trouve inefficace de publier ailleurs des images prêtes à l'emploi qui ne servent qu'à moi, pour ensuite les rapatrier en local. En plus dans la foulée de GitHub j'avais bien apprécié de supprimer mon compte Docker Hub enfin désactiver car la suppression ne semble pas possible :-(
|
||||
|
||||
En cherchant une alternative pour construire mes images localement et les transférer vers mon infra, j'ai pensé à Gitea qui a aussi la fonction de "docker registry" appelée *packages*. Je suis donc parti là dessus, une publication à la demande, pas de CI ; vu le peu d'activité des projets ça fait parfaitement l'affaire.
|
||||
|
||||
> C'est quand on paye de sa poche qu'on prend conscience de la valeur des choses
|
||||
|
||||
Après avoir mis ça en place... un changement d'URL dans 2 projets j'ai trouvé assez long la publication de l'image du blog dans Gitea alors que tout est sur le LAN. Et pour cause, elle faisait 1,2 Go alors que celle de stacosys tient dans 74 Mo. Évidemment, en tout automatisé dans une CI ça ne m'avait pas alerté auparavant. Me voilà donc à m'intéresser à un génial outil d'analyse d'images [Dive](https://github.com/wagoodman/dive) qu'on peut même exécuter en container,
|
||||
|
||||
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock docker.io/wagoodman/dive"
|
||||
|
||||
à lire un peu de littérature sur les build multi-stages et à analyser où ça pêche... Bon, ça pêche à plusieurs endroits : trop de couches, installation d'outils de développement pour construire les locales MUSL de la distribution Alpine alors que sur les versions récentes elles sont proposés dans les dépôts. Après une phase d'optimisation j'ai pu ramener l'image à 128 Mo, honnête à mon sens pour un container avec Nginx, Python et des traitements Bash qui font du "git clone".
|
|
@ -1,19 +1,28 @@
|
|||
[project]
|
||||
name = "blog"
|
||||
version = "1.2"
|
||||
version = "1.1"
|
||||
description = "Blog du Yax"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Yax" }
|
||||
]
|
||||
requires-python = ">=3.12.9"
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"pygments>=2.17.1",
|
||||
"mistune>=3.0.2",
|
||||
"pygments>=2.18.0",
|
||||
"requests>=2.32.3",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=24.10.0",
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"black>=23.10.1",
|
||||
"pyinstaller>=6.1.0",
|
||||
]
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
|
29
requirements-dev.lock
Normal file
29
requirements-dev.lock
Normal file
|
@ -0,0 +1,29 @@
|
|||
# generated by rye
|
||||
# use `rye lock` or `rye sync` to update this lockfile
|
||||
#
|
||||
# last locked with the following flags:
|
||||
# pre: false
|
||||
# features: []
|
||||
# all-features: false
|
||||
|
||||
-e file:.
|
||||
altgraph==0.17.4
|
||||
black==23.11.0
|
||||
certifi==2023.11.17
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
idna==3.4
|
||||
mistune==3.0.2
|
||||
mypy-extensions==1.0.0
|
||||
packaging==23.2
|
||||
pathspec==0.11.2
|
||||
platformdirs==4.0.0
|
||||
pygments==2.17.1
|
||||
pyinstaller==6.2.0
|
||||
pyinstaller-hooks-contrib==2023.10
|
||||
requests==2.31.0
|
||||
tomli==2.0.1
|
||||
typing-extensions==4.8.0
|
||||
urllib3==2.1.0
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
setuptools==68.2.2
|
16
requirements.lock
Normal file
16
requirements.lock
Normal file
|
@ -0,0 +1,16 @@
|
|||
# generated by rye
|
||||
# use `rye lock` or `rye sync` to update this lockfile
|
||||
#
|
||||
# last locked with the following flags:
|
||||
# pre: false
|
||||
# features: []
|
||||
# all-features: false
|
||||
|
||||
-e file:.
|
||||
certifi==2023.11.17
|
||||
charset-normalizer==3.3.2
|
||||
idna==3.4
|
||||
mistune==3.0.2
|
||||
pygments==2.17.1
|
||||
requests==2.31.0
|
||||
urllib3==2.1.0
|
|
@ -1,47 +0,0 @@
|
|||
vars: {
|
||||
d2-config: {
|
||||
layout-engine: elk
|
||||
# Terminal theme code
|
||||
theme-id: 300
|
||||
}
|
||||
}
|
||||
|
||||
dmz: {
|
||||
nginx proxy: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fnginx.svg
|
||||
shape: rectangle
|
||||
}
|
||||
}
|
||||
|
||||
private network: {
|
||||
label.near: bottom-center
|
||||
virtual machines: {
|
||||
icon: https://icons.terrastruct.com/gcp%2FProducts%20and%20services%2FNetworking%2FVirtual%20Private%20Cloud.svg
|
||||
style.multiple: true
|
||||
containers: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
style.multiple: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lan: {
|
||||
bastion: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fssh.svg
|
||||
}
|
||||
devices: {
|
||||
icon: https://icons.terrastruct.com/tech%2Flaptop.svg
|
||||
style.multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
internet box: {
|
||||
icon: https://icons.terrastruct.com/tech%2Frouter.svg
|
||||
width: 130
|
||||
}
|
||||
|
||||
internet box -> dmz.nginx proxy: http/https
|
||||
|
||||
lan.bastion -> private network.virtual machines: ssh
|
||||
|
||||
dmz.nginx proxy -> private network.virtual machines: http
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 50 KiB |
|
@ -1,76 +0,0 @@
|
|||
vars: {
|
||||
d2-config: {
|
||||
layout-engine: elk
|
||||
# Terminal theme code
|
||||
theme-id: 4
|
||||
}
|
||||
}
|
||||
|
||||
Proxmox: {
|
||||
icon: https://www.svgrepo.com/download/331552/proxmox.svg
|
||||
label: "Hyperviseur Proxmox"
|
||||
|
||||
Containers: {
|
||||
label: "Containers LXC"
|
||||
|
||||
Nginx_Proxy: {
|
||||
icon: https://upload.wikimedia.org/wikipedia/commons/d/dd/Linux_Containers_logo.svg
|
||||
label: "Nginx Proxy"
|
||||
}
|
||||
|
||||
Bastion: {
|
||||
icon: https://upload.wikimedia.org/wikipedia/commons/d/dd/Linux_Containers_logo.svg
|
||||
label: "Bastion"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vm1: {
|
||||
icon : https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Kvmbanner-logo2_1.png/320px-Kvmbanner-logo2_1.png
|
||||
label: "vm1"
|
||||
|
||||
Heimdall: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Heimdall"
|
||||
}
|
||||
|
||||
Blog: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Blog"
|
||||
}
|
||||
|
||||
Selfoss: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Selfoss"
|
||||
}
|
||||
|
||||
Wallabag: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Wallabag"
|
||||
}
|
||||
|
||||
Shaarli: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Shaarli"
|
||||
}
|
||||
}
|
||||
|
||||
vm2: {
|
||||
icon : https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Kvmbanner-logo2_1.png/320px-Kvmbanner-logo2_1.png
|
||||
label: "vm2"
|
||||
style: {
|
||||
stroke-dash: 3
|
||||
}
|
||||
|
||||
|
||||
Nextcloud: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Nextcloud"
|
||||
}
|
||||
|
||||
Immich: {
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
label: "Immich"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 177 KiB |
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
16
test/path.py
Normal file
16
test/path.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
def temppath(*paths):
|
||||
return os.path.join(tempfile.gettempdir(), *paths)
|
||||
|
||||
|
||||
def move(src, dst):
|
||||
if os.path.isfile(dst):
|
||||
os.remove(dst)
|
||||
elif os.path.isdir(dst):
|
||||
shutil.rmtree(dst)
|
||||
if os.path.exists(src):
|
||||
os.rename(src, dst)
|
104
test/test_content.py
Normal file
104
test/test_content.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
import unittest
|
||||
import shutil
|
||||
import os
|
||||
|
||||
import makesite
|
||||
from test import path
|
||||
|
||||
|
||||
class ContentTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.blog_path = path.temppath('blog')
|
||||
self.undated_path = os.path.join(self.blog_path, 'foo.txt')
|
||||
self.dated_path = os.path.join(self.blog_path, '2018-01-01-foo.txt')
|
||||
self.normal_post_path = os.path.join(self.blog_path, 'baz.txt')
|
||||
self.md_post_path = os.path.join(self.blog_path, 'qux.md')
|
||||
self.no_md_post_path = os.path.join(self.blog_path, 'qux.txt')
|
||||
|
||||
os.makedirs(self.blog_path)
|
||||
|
||||
with open(self.undated_path, 'w') as f:
|
||||
f.write('hello world')
|
||||
|
||||
with open(self.dated_path, 'w') as f:
|
||||
f.write('hello world')
|
||||
|
||||
with open(self.normal_post_path, 'w') as f:
|
||||
f.write('<!-- a: 1 -->\n<!-- b: 2 -->\nFoo')
|
||||
|
||||
with open(self.md_post_path, 'w') as f:
|
||||
f.write('*Foo*')
|
||||
|
||||
with open(self.no_md_post_path, 'w') as f:
|
||||
f.write('*Foo*')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.blog_path)
|
||||
|
||||
# Rudimentary mock because unittest.mock is unavailable in Python 2.7.
|
||||
def mock(self, *args):
|
||||
self.mock_args = args
|
||||
|
||||
def test_content_content(self):
|
||||
content = makesite.read_content(self.undated_path)
|
||||
self.assertEqual(content['content'], 'hello world')
|
||||
|
||||
def test_content_date(self):
|
||||
content = makesite.read_content(self.dated_path)
|
||||
self.assertEqual(content['date'], '2018-01-01')
|
||||
|
||||
def test_content_date_missing(self):
|
||||
content = makesite.read_content(self.undated_path)
|
||||
self.assertEqual(content['date'], '1970-01-01')
|
||||
|
||||
def test_content_slug_dated(self):
|
||||
content = makesite.read_content(self.dated_path)
|
||||
self.assertEqual(content['slug'], 'foo')
|
||||
|
||||
def test_content_slug_undated(self):
|
||||
content = makesite.read_content(self.undated_path)
|
||||
self.assertEqual(content['slug'], 'foo')
|
||||
|
||||
def test_content_headers(self):
|
||||
content = makesite.read_content(self.normal_post_path)
|
||||
self.assertEqual(content['a'], '1')
|
||||
self.assertEqual(content['b'], '2')
|
||||
self.assertEqual(content['content'], 'Foo')
|
||||
|
||||
def test_markdown_rendering(self):
|
||||
content = makesite.read_content(self.md_post_path)
|
||||
self.assertEqual(content['content'], '<p><em>Foo</em></p>\n')
|
||||
|
||||
def test_markdown_import_error(self):
|
||||
makesite._test = 'ImportError'
|
||||
original_log = makesite.log
|
||||
|
||||
makesite.log = self.mock
|
||||
self.mock_args = None
|
||||
content = makesite.read_content(self.md_post_path)
|
||||
|
||||
makesite._test = None
|
||||
makesite.log = original_log
|
||||
|
||||
self.assertEqual(content['content'], '*Foo*')
|
||||
self.assertEqual(self.mock_args,
|
||||
('WARNING: Cannot render Markdown in {}: {}',
|
||||
self.md_post_path, 'Error forced by test'))
|
||||
|
||||
def test_no_markdown_rendering(self):
|
||||
content = makesite.read_content(self.no_md_post_path)
|
||||
self.assertEqual(content['content'], '*Foo*')
|
||||
|
||||
def test_no_markdown_import_error(self):
|
||||
makesite._test = 'ImportError'
|
||||
original_log = makesite.log
|
||||
|
||||
makesite.log = self.mock
|
||||
self.mock_args = None
|
||||
content = makesite.read_content(self.no_md_post_path)
|
||||
|
||||
makesite._test = None
|
||||
makesite.log = original_log
|
||||
|
||||
self.assertEqual(content['content'], '*Foo*')
|
||||
self.assertIsNone(self.mock_args)
|
39
test/test_file_io.py
Normal file
39
test/test_file_io.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import makesite
|
||||
from test import path
|
||||
|
||||
|
||||
class FileIOTest(unittest.TestCase):
|
||||
"""Tests for file I/O functions."""
|
||||
|
||||
def test_fread(self):
|
||||
text = 'foo\nbar\n'
|
||||
filepath = path.temppath('foo.txt')
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(text)
|
||||
text_read = makesite.fread(filepath)
|
||||
os.remove(filepath)
|
||||
self.assertEqual(text_read, text)
|
||||
|
||||
def test_fwrite(self):
|
||||
text = 'baz\nqux\n'
|
||||
filepath = path.temppath('foo.txt')
|
||||
makesite.fwrite(filepath, text)
|
||||
with open(filepath) as f:
|
||||
text_read = f.read()
|
||||
os.remove(filepath)
|
||||
self.assertEqual(text_read, text)
|
||||
|
||||
def test_fwrite_makedir(self):
|
||||
text = 'baz\nqux\n'
|
||||
dirpath = path.temppath('foo', 'bar')
|
||||
filepath = os.path.join(dirpath, 'foo.txt')
|
||||
makesite.fwrite(filepath, text)
|
||||
with open(filepath) as f:
|
||||
text_read = f.read()
|
||||
self.assertTrue(os.path.isdir(dirpath))
|
||||
shutil.rmtree(path.temppath('foo'))
|
||||
self.assertEqual(text_read, text)
|
43
test/test_headers.py
Normal file
43
test/test_headers.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import unittest
|
||||
import makesite
|
||||
|
||||
|
||||
class HeaderTest(unittest.TestCase):
|
||||
"""Tests for read_headers() function."""
|
||||
|
||||
def test_single_header(self):
|
||||
text = '<!-- key1: val1 -->'
|
||||
headers = list(makesite.read_headers(text))
|
||||
self.assertEqual(headers, [('key1', 'val1', 19)])
|
||||
|
||||
def test_multiple_headers(self):
|
||||
text = '<!-- key1: val1 -->\n<!-- key2: val2-->'
|
||||
headers = list(makesite.read_headers(text))
|
||||
self.assertEqual(headers, [('key1', 'val1', 20), ('key2', 'val2', 38)])
|
||||
|
||||
def test_headers_and_text(self):
|
||||
text = '<!-- a: 1 -->\n<!-- b: 2 -->\nFoo\n<!-- c: 3 -->'
|
||||
headers = list(makesite.read_headers(text))
|
||||
self.assertEqual(headers, [('a', '1', 14), ('b', '2', 28)])
|
||||
|
||||
def test_headers_and_blank_line(self):
|
||||
text = '<!-- a: 1 -->\n<!-- b: 2 -->\n\n<!-- c: 3 -->\n'
|
||||
headers = list(makesite.read_headers(text))
|
||||
self.assertEqual(headers, [('a', '1', 14),
|
||||
('b', '2', 29),
|
||||
('c', '3', 43)])
|
||||
|
||||
def test_multiline_header(self):
|
||||
text = '<!--\na: 1 --><!-- b:\n2 -->\n<!-- c: 3\n-->'
|
||||
headers = list(makesite.read_headers(text))
|
||||
self.assertEqual(headers, [('a', '1', 13),
|
||||
('b', '2', 27),
|
||||
('c', '3', 40)])
|
||||
|
||||
def test_no_header(self):
|
||||
headers = list(makesite.read_headers('Foo'))
|
||||
self.assertEqual(headers, [])
|
||||
|
||||
def test_empty_string(self):
|
||||
headers = list(makesite.read_headers(''))
|
||||
self.assertEqual(headers, [])
|
46
test/test_list.py
Normal file
46
test/test_list.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
import unittest
|
||||
import shutil
|
||||
import os
|
||||
import makesite
|
||||
from test import path
|
||||
|
||||
class PagesTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.site_path = path.temppath('site')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.site_path)
|
||||
|
||||
def test_list(self):
|
||||
posts = [{'content': 'Foo'}, {'content': 'Bar'}]
|
||||
dst = os.path.join(self.site_path, 'list.txt')
|
||||
list_layout = '<div>{{ content }}</div>'
|
||||
item_layout = '<p>{{ content }}</p>'
|
||||
makesite.make_list(posts, dst, list_layout, item_layout)
|
||||
with open(os.path.join(self.site_path, 'list.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')
|
||||
|
||||
def test_list_params(self):
|
||||
posts = [{'content': 'Foo', 'title': 'foo'},
|
||||
{'content': 'Bar', 'title': 'bar'}]
|
||||
dst = os.path.join(self.site_path, 'list.txt')
|
||||
list_layout = '<div>{{ key }}:{{ title }}:{{ content }}</div>'
|
||||
item_layout = '<p>{{ key }}:{{ title }}:{{ content }}</p>'
|
||||
makesite.make_list(posts, dst, list_layout, item_layout,
|
||||
key='val', title='lorem')
|
||||
with open(os.path.join(self.site_path, 'list.txt')) as f:
|
||||
text = f.read()
|
||||
self.assertEqual(text,
|
||||
'<div>val:lorem:<p>val:foo:Foo</p><p>val:bar:Bar</p></div>')
|
||||
|
||||
def test_dst_params(self):
|
||||
posts = [{'content': 'Foo'}, {'content': 'Bar'}]
|
||||
dst = os.path.join(self.site_path, '{{ key }}.txt')
|
||||
list_layout = '<div>{{ content }}</div>'
|
||||
item_layout = '<p>{{ content }}</p>'
|
||||
makesite.make_list(posts, dst, list_layout, item_layout, key='val')
|
||||
|
||||
expected_path = os.path.join(self.site_path, 'val.txt')
|
||||
self.assertTrue(os.path.isfile(expected_path))
|
||||
with open(expected_path) as f:
|
||||
self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')
|
73
test/test_main.py
Normal file
73
test/test_main.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
import unittest
|
||||
import makesite
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
|
||||
from test import path
|
||||
|
||||
|
||||
class MainTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
path.move('_site', '_site.backup')
|
||||
path.move('params.json', 'params.json.backup')
|
||||
|
||||
def tearDown(self):
|
||||
path.move('_site.backup', '_site')
|
||||
path.move('params.json.backup', 'params')
|
||||
|
||||
def test_site_missing(self):
|
||||
makesite.main()
|
||||
|
||||
def test_site_exists(self):
|
||||
os.mkdir('_site')
|
||||
with open('_site/foo.txt', 'w') as f:
|
||||
f.write('foo')
|
||||
|
||||
self.assertTrue(os.path.isfile('_site/foo.txt'))
|
||||
makesite.main()
|
||||
self.assertFalse(os.path.isfile('_site/foo.txt'))
|
||||
|
||||
def test_default_params(self):
|
||||
makesite.main()
|
||||
|
||||
with open('_site/blog/proin-quam/index.html') as f:
|
||||
s1 = f.read()
|
||||
|
||||
with open('_site/blog/rss.xml') as f:
|
||||
s2 = f.read()
|
||||
|
||||
shutil.rmtree('_site')
|
||||
|
||||
self.assertIn('<a href="/">Home</a>', s1)
|
||||
self.assertIn('<title>Proin Quam - Lorem Ipsum</title>', s1)
|
||||
self.assertIn('Published on 2018-01-01 by <b>Admin</b>', s1)
|
||||
|
||||
self.assertIn('<link>http://localhost:8000/</link>', s2)
|
||||
self.assertIn('<link>http://localhost:8000/blog/proin-quam/</link>', s2)
|
||||
|
||||
def test_json_params(self):
|
||||
params = {
|
||||
'base_path': '/base',
|
||||
'subtitle': 'Foo',
|
||||
'author': 'Bar',
|
||||
'site_url': 'http://localhost/base'
|
||||
}
|
||||
with open('params.json', 'w') as f:
|
||||
json.dump(params, f)
|
||||
makesite.main()
|
||||
|
||||
with open('_site/blog/proin-quam/index.html') as f:
|
||||
s1 = f.read()
|
||||
|
||||
with open('_site/blog/rss.xml') as f:
|
||||
s2 = f.read()
|
||||
|
||||
shutil.rmtree('_site')
|
||||
|
||||
self.assertIn('<a href="/base/">Home</a>', s1)
|
||||
self.assertIn('<title>Proin Quam - Foo</title>', s1)
|
||||
self.assertIn('Published on 2018-01-01 by <b>Bar</b>', s1)
|
||||
|
||||
self.assertIn('<link>http://localhost/base/</link>', s2)
|
||||
self.assertIn('<link>http://localhost/base/blog/proin-quam/</link>', s2)
|
127
test/test_pages.py
Normal file
127
test/test_pages.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
import makesite
|
||||
from test import path
|
||||
|
||||
class PagesTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.blog_path = path.temppath('blog')
|
||||
self.site_path = path.temppath('site')
|
||||
os.makedirs(self.blog_path)
|
||||
|
||||
with open(os.path.join(self.blog_path, 'foo.txt'), 'w') as f:
|
||||
f.write('Foo')
|
||||
with open(os.path.join(self.blog_path, 'bar.txt'), 'w') as f:
|
||||
f.write('Bar')
|
||||
with open(os.path.join(self.blog_path, '2018-01-01-foo.txt'), 'w') as f:
|
||||
f.write('Foo')
|
||||
with open(os.path.join(self.blog_path, '2018-01-02-bar.txt'), 'w') as f:
|
||||
f.write('Bar')
|
||||
with open(os.path.join(self.blog_path, 'header-foo.txt'), 'w') as f:
|
||||
f.write('<!-- tag: foo -->Foo')
|
||||
with open(os.path.join(self.blog_path, 'header-bar.txt'), 'w') as f:
|
||||
f.write('<!-- title: bar -->Bar')
|
||||
with open(os.path.join(self.blog_path, 'placeholder-foo.txt'), 'w') as f:
|
||||
f.write('<!-- title: foo -->{{ title }}:{{ author }}:Foo')
|
||||
with open(os.path.join(self.blog_path, 'placeholder-bar.txt'), 'w') as f:
|
||||
f.write('<!-- title: bar --><!-- render: yes -->{{ title }}:{{ author }}:Bar')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.blog_path)
|
||||
shutil.rmtree(self.site_path)
|
||||
|
||||
def test_pages_undated(self):
|
||||
src = os.path.join(self.blog_path, '[fb]*.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ content }}</div>'
|
||||
makesite.make_pages(src, dst, tpl)
|
||||
with open(os.path.join(self.site_path, 'foo.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>Foo</div>')
|
||||
with open(os.path.join(self.site_path, 'bar.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>Bar</div>')
|
||||
|
||||
def test_pages_dated(self):
|
||||
src = os.path.join(self.blog_path, '2*.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ content }}</div>'
|
||||
makesite.make_pages(src, dst, tpl)
|
||||
with open(os.path.join(self.site_path, 'foo.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>Foo</div>')
|
||||
with open(os.path.join(self.site_path, 'bar.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>Bar</div>')
|
||||
|
||||
def test_pages_layout_params(self):
|
||||
src = os.path.join(self.blog_path, '2*.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ slug }}:{{ title }}:{{ date }}:{{ content }}</div>'
|
||||
makesite.make_pages(src, dst, tpl, title='Lorem')
|
||||
with open(os.path.join(self.site_path, 'foo.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>foo:Lorem:2018-01-01:Foo</div>')
|
||||
with open(os.path.join(self.site_path, 'bar.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>bar:Lorem:2018-01-02:Bar</div>')
|
||||
|
||||
def test_pages_return_value(self):
|
||||
src = os.path.join(self.blog_path, '2*.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ content }}</div>'
|
||||
posts = makesite.make_pages(src, dst, tpl)
|
||||
self.assertEqual(len(posts), 2)
|
||||
self.assertEqual(posts[0]['date'], '2018-01-02')
|
||||
self.assertEqual(posts[1]['date'], '2018-01-01')
|
||||
|
||||
def test_content_header_params(self):
|
||||
# Test that header params from one post is not used in another
|
||||
# post.
|
||||
src = os.path.join(self.blog_path, 'header*.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '{{ title }}:{{ tag }}:{{ content }}'
|
||||
makesite.make_pages(src, dst, tpl)
|
||||
with open(os.path.join(self.site_path, 'header-foo.txt')) as f:
|
||||
self.assertEqual(f.read(), '{{ title }}:foo:Foo')
|
||||
with open(os.path.join(self.site_path, 'header-bar.txt')) as f:
|
||||
self.assertEqual(f.read(), 'bar:{{ tag }}:Bar')
|
||||
|
||||
def test_content_no_rendering(self):
|
||||
# Test that placeholders are not populated in content rendering
|
||||
# by default.
|
||||
src = os.path.join(self.blog_path, 'placeholder-foo.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ content }}</div>'
|
||||
makesite.make_pages(src, dst, tpl, author='Admin')
|
||||
with open(os.path.join(self.site_path, 'placeholder-foo.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>{{ title }}:{{ author }}:Foo</div>')
|
||||
|
||||
def test_content_rendering_via_kwargs(self):
|
||||
# Test that placeholders are populated in content rendering when
|
||||
# requested in make_pages.
|
||||
src = os.path.join(self.blog_path, 'placeholder-foo.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ content }}</div>'
|
||||
makesite.make_pages(src, dst, tpl, author='Admin', render='yes')
|
||||
with open(os.path.join(self.site_path, 'placeholder-foo.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>foo:Admin:Foo</div>')
|
||||
|
||||
def test_content_rendering_via_header(self):
|
||||
# Test that placeholders are populated in content rendering when
|
||||
# requested in content header.
|
||||
src = os.path.join(self.blog_path, 'placeholder-bar.txt')
|
||||
dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
tpl = '<div>{{ content }}</div>'
|
||||
makesite.make_pages(src, dst, tpl, author='Admin')
|
||||
with open(os.path.join(self.site_path, 'placeholder-bar.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div>bar:Admin:Bar</div>')
|
||||
|
||||
def test_rendered_content_in_summary(self):
|
||||
# Test that placeholders are populated in summary if and only if
|
||||
# content rendering is enabled.
|
||||
src = os.path.join(self.blog_path, 'placeholder*.txt')
|
||||
post_dst = os.path.join(self.site_path, '{{ slug }}.txt')
|
||||
list_dst = os.path.join(self.site_path, 'list.txt')
|
||||
post_layout = ''
|
||||
list_layout = '<div>{{ content }}</div>'
|
||||
item_layout = '<p>{{ summary }}</p>'
|
||||
posts = makesite.make_pages(src, post_dst, post_layout, author='Admin')
|
||||
makesite.make_list(posts, list_dst, list_layout, item_layout)
|
||||
with open(os.path.join(self.site_path, 'list.txt')) as f:
|
||||
self.assertEqual(f.read(), '<div><p>{{ title }}:{{ author }}:Foo</p><p>bar:Admin:Bar</p></div>')
|
78
test/test_path.py
Normal file
78
test/test_path.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from test import path
|
||||
|
||||
class PathTest(unittest.TestCase):
|
||||
def test_temppath(self):
|
||||
self.assertTrue(path.temppath())
|
||||
|
||||
def test_move_existing_file(self):
|
||||
src = os.path.join(path.temppath(), 'foo.txt')
|
||||
dst = os.path.join(path.temppath(), 'bar.txt')
|
||||
with open(src, 'w') as f:
|
||||
f.write('foo')
|
||||
|
||||
path.move(src, dst)
|
||||
self.assertFalse(os.path.isfile(src))
|
||||
self.assertTrue(os.path.isfile(dst))
|
||||
|
||||
with open(dst) as f:
|
||||
text = f.read()
|
||||
|
||||
os.remove(dst)
|
||||
|
||||
self.assertEqual(text, 'foo')
|
||||
|
||||
def test_move_missing_file(self):
|
||||
src = os.path.join(path.temppath(), 'foo.txt')
|
||||
dst = os.path.join(path.temppath(), 'bar.txt')
|
||||
path.move(src, dst)
|
||||
self.assertFalse(os.path.isfile(src))
|
||||
self.assertFalse(os.path.isfile(dst))
|
||||
|
||||
def test_move_file_cleanup(self):
|
||||
src = os.path.join(path.temppath(), 'foo.txt')
|
||||
dst = os.path.join(path.temppath(), 'bar.txt')
|
||||
with open(dst, 'w') as f:
|
||||
f.write('foo')
|
||||
path.move(src, dst)
|
||||
self.assertFalse(os.path.isfile(src))
|
||||
self.assertFalse(os.path.isfile(dst))
|
||||
|
||||
def test_move_existing_dir(self):
|
||||
src = os.path.join(path.temppath(), 'foo')
|
||||
srcf = os.path.join(src, 'foo.txt')
|
||||
dst = os.path.join(path.temppath(), 'bar')
|
||||
dstf = os.path.join(dst, 'foo.txt')
|
||||
|
||||
os.makedirs(src)
|
||||
with open(srcf, 'w') as f:
|
||||
f.write('foo')
|
||||
|
||||
path.move(src, dst)
|
||||
self.assertFalse(os.path.isdir(src))
|
||||
self.assertTrue(os.path.isdir(dst))
|
||||
|
||||
with open(dstf) as f:
|
||||
text = f.read()
|
||||
|
||||
shutil.rmtree(dst)
|
||||
|
||||
self.assertEqual(text, 'foo')
|
||||
|
||||
def test_move_missing_dir(self):
|
||||
src = os.path.join(path.temppath(), 'foo')
|
||||
dst = os.path.join(path.temppath(), 'bar')
|
||||
path.move(src, dst)
|
||||
self.assertFalse(os.path.isdir(src))
|
||||
self.assertFalse(os.path.isdir(dst))
|
||||
|
||||
def test_move_dir_cleanup(self):
|
||||
src = os.path.join(path.temppath(), 'foo')
|
||||
dst = os.path.join(path.temppath(), 'bar')
|
||||
os.makedirs(dst)
|
||||
path.move(src, dst)
|
||||
self.assertFalse(os.path.isdir(src))
|
||||
self.assertFalse(os.path.isdir(dst))
|
25
test/test_render.py
Normal file
25
test/test_render.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import unittest
|
||||
import makesite
|
||||
|
||||
class RenderTest(unittest.TestCase):
|
||||
"""Tests for render() function."""
|
||||
|
||||
def test_oneline_template(self):
|
||||
tpl = 'foo {{ key1 }} baz {{ key2 }}'
|
||||
out = makesite.render(tpl, key1='bar', key2='qux')
|
||||
self.assertEqual(out, 'foo bar baz qux')
|
||||
|
||||
def test_multiline_template(self):
|
||||
tpl = 'foo {{ key1 }}\nbaz {{ key1 }}'
|
||||
out = makesite.render(tpl, key1='bar')
|
||||
self.assertEqual(out, 'foo bar\nbaz bar')
|
||||
|
||||
def test_repeated_key(self):
|
||||
tpl = 'foo {{ key1 }} baz {{ key1 }}'
|
||||
out = makesite.render(tpl, key1='bar')
|
||||
self.assertEqual(out, 'foo bar baz bar')
|
||||
|
||||
def test_multiline_placeholder(self):
|
||||
tpl = 'foo {{\nkey1\n}} baz {{\nkey2\n}}'
|
||||
out = makesite.render(tpl, key1='bar', key2='qux')
|
||||
self.assertEqual(out, 'foo bar baz qux')
|
13
test/test_rfc_2822_date.py
Normal file
13
test/test_rfc_2822_date.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import unittest
|
||||
import makesite
|
||||
|
||||
|
||||
class RFC822DateTest(unittest.TestCase):
|
||||
|
||||
def test_epoch(self):
|
||||
self.assertEqual(makesite.rfc_2822_format('1970-01-01'),
|
||||
'Thu, 01 Jan 1970 00:00:00 +0000')
|
||||
|
||||
def test_2018_06_16(self):
|
||||
self.assertEqual(makesite.rfc_2822_format('2018-06-16'),
|
||||
'Sat, 16 Jun 2018 00:00:00 +0000')
|
13
test/test_slugify.py
Normal file
13
test/test_slugify.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import unittest
|
||||
import makesite
|
||||
|
||||
|
||||
class SlugifyTest(unittest.TestCase):
|
||||
|
||||
def test_slugify(self):
|
||||
self.assertEqual(makesite.slugify('NginX est brillant'), 'nginx-est-brillant')
|
||||
self.assertEqual(makesite.slugify('Bilan hébergement 2023'), 'bilan-hebergement-2023')
|
||||
self.assertEqual(makesite.slugify('Sécurisation Docker : des pistes'), 'securisation-docker-des-pistes')
|
||||
self.assertEqual(makesite.slugify('Il court, il court, le furet'), 'il-court-il-court-le-furet')
|
||||
self.assertEqual(makesite.slugify('De GNU/Linux à gnuSystemlinuxdGnomeOs'), 'de-gnulinux-a-gnusystemlinuxdgnomeos')
|
||||
self.assertEqual(makesite.slugify('Au fait... mon téléphone'), 'au-fait-mon-telephone')
|
9
test/test_truncate.py
Normal file
9
test/test_truncate.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
import unittest
|
||||
import makesite
|
||||
|
||||
|
||||
class TruncateTest(unittest.TestCase):
|
||||
def test_truncate(self):
|
||||
long_text = ' \n'.join('word' + str(i) for i in range(50))
|
||||
expected_text = ' '.join('word' + str(i) for i in range(25))
|
||||
self.assertEqual(makesite.truncate(long_text), expected_text)
|
Loading…
Add table
Reference in a new issue