diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-02-22 18:11:35 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-02-23 01:36:11 +0800 |
commit | 1e9b2436eaffa4130f6a69c3a108f6feb9dd4ac8 (patch) | |
tree | 585b6124b0100371b4bd8a291c4a59fbb5fbf1fe | |
parent | a931457d61b053682d5e89a0cfb411e43e5e21c7 (diff) | |
download | crupest-1e9b2436eaffa4130f6a69c3a108f6feb9dd4ac8.tar.gz crupest-1e9b2436eaffa4130f6a69c3a108f6feb9dd4ac8.tar.bz2 crupest-1e9b2436eaffa4130f6a69c3a108f6feb9dd4ac8.zip |
feat(services): refactor structure.
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | docker/git-server/Dockerfile | 24 | ||||
-rwxr-xr-x | docker/git-server/lighttpd-wrapper | 3 | ||||
-rw-r--r-- | services/.gitignore (renamed from tools/cru-py/.gitignore) | 2 | ||||
-rw-r--r-- | services/.python-version (renamed from .python-version) | 0 | ||||
-rw-r--r-- | services/base-config | 4 | ||||
-rw-r--r-- | services/common.bash | 5 | ||||
-rw-r--r-- | services/config.template | 10 | ||||
-rw-r--r-- | services/docker/auto-backup/Dockerfile (renamed from docker/auto-backup/Dockerfile) | 3 | ||||
-rwxr-xr-x | services/docker/auto-backup/daemon.bash (renamed from docker/auto-backup/daemon.bash) | 0 | ||||
-rw-r--r-- | services/docker/blog/Dockerfile (renamed from docker/blog/Dockerfile) | 0 | ||||
-rwxr-xr-x | services/docker/blog/daemon.bash (renamed from docker/blog/daemon.bash) | 0 | ||||
-rwxr-xr-x | services/docker/blog/install-hugo.bash (renamed from docker/blog/install-hugo.bash) | 0 | ||||
-rwxr-xr-x | services/docker/blog/update.bash (renamed from docker/blog/update.bash) | 0 | ||||
-rw-r--r-- | services/docker/debian-dev/Dockerfile (renamed from docker/debian-dev/Dockerfile) | 3 | ||||
-rwxr-xr-x | services/docker/debian-dev/bootstrap/extra/setup-cmake.bash (renamed from docker/debian-dev/bootstrap/extra/setup-cmake.bash) | 0 | ||||
-rwxr-xr-x | services/docker/debian-dev/bootstrap/extra/setup-dotnet.bash (renamed from docker/debian-dev/bootstrap/extra/setup-dotnet.bash) | 0 | ||||
-rwxr-xr-x | services/docker/debian-dev/bootstrap/extra/setup-llvm.bash (renamed from docker/debian-dev/bootstrap/extra/setup-llvm.bash) | 0 | ||||
-rw-r--r-- | services/docker/debian-dev/bootstrap/home/.bashrc (renamed from docker/debian-dev/bootstrap/home/.bashrc) | 0 | ||||
-rw-r--r-- | services/docker/debian-dev/bootstrap/home/.quiltrc-dpkg (renamed from docker/debian-dev/bootstrap/home/.quiltrc-dpkg) | 0 | ||||
-rw-r--r-- | services/docker/debian-dev/bootstrap/official.sources (renamed from docker/debian-dev/bootstrap/official.sources) | 0 | ||||
-rwxr-xr-x | services/docker/debian-dev/bootstrap/setup-apt.bash (renamed from docker/debian-dev/bootstrap/setup-apt.bash) | 0 | ||||
-rwxr-xr-x | services/docker/debian-dev/bootstrap/setup.bash (renamed from docker/debian-dev/bootstrap/setup.bash) | 0 | ||||
-rw-r--r-- | services/docker/git-server/Dockerfile | 11 | ||||
-rw-r--r-- | services/docker/git-server/git-auth.conf (renamed from docker/git-server/git-auth.conf) | 2 | ||||
-rw-r--r-- | services/docker/git-server/git-lighttpd.conf (renamed from docker/git-server/git-lighttpd.conf) | 7 | ||||
-rwxr-xr-x | services/docker/git-server/lighttpd-wrapper.bash | 8 | ||||
-rw-r--r-- | services/docker/nginx/Dockerfile (renamed from docker/nginx/Dockerfile) | 0 | ||||
-rw-r--r-- | services/docker/nginx/certbot.bash (renamed from docker/nginx/certbot.bash) | 0 | ||||
-rw-r--r-- | services/docker/nginx/nginx-wrapper.bash (renamed from docker/nginx/nginx-wrapper.bash) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/.dockerignore (renamed from docker/nginx/sites/www/.dockerignore) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/.gitignore (renamed from docker/nginx/sites/www/.gitignore) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/avatar.png (renamed from docker/nginx/sites/www/avatar.png) | bin | 12038 -> 12038 bytes | |||
-rw-r--r-- | services/docker/nginx/sites/www/favicon.ico (renamed from docker/nginx/sites/www/favicon.ico) | bin | 15406 -> 15406 bytes | |||
-rw-r--r-- | services/docker/nginx/sites/www/github-mark.png (renamed from docker/nginx/sites/www/github-mark.png) | bin | 6393 -> 6393 bytes | |||
-rw-r--r-- | services/docker/nginx/sites/www/index.html (renamed from docker/nginx/sites/www/index.html) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/package.json (renamed from docker/nginx/sites/www/package.json) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/pnpm-lock.yaml (renamed from docker/nginx/sites/www/pnpm-lock.yaml) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/src/main.ts (renamed from docker/nginx/sites/www/src/main.ts) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/src/style.css (renamed from docker/nginx/sites/www/src/style.css) | 0 | ||||
-rw-r--r-- | services/docker/nginx/sites/www/tsconfig.json (renamed from docker/nginx/sites/www/tsconfig.json) | 0 | ||||
-rw-r--r-- | services/docker/v2ray/Dockerfile (renamed from docker/v2ray/Dockerfile) | 0 | ||||
-rwxr-xr-x | services/gen-tplt | 7 | ||||
-rwxr-xr-x | services/git-add-user | 14 | ||||
-rwxr-xr-x | services/manage | 14 | ||||
-rw-r--r-- | services/manager/__init__.py (renamed from tools/cru-py/cru/__init__.py) | 0 | ||||
-rw-r--r-- | services/manager/_base.py (renamed from tools/cru-py/cru/_base.py) | 0 | ||||
-rw-r--r-- | services/manager/_const.py (renamed from tools/cru-py/cru/_const.py) | 0 | ||||
-rw-r--r-- | services/manager/_decorator.py (renamed from tools/cru-py/cru/_decorator.py) | 0 | ||||
-rw-r--r-- | services/manager/_error.py (renamed from tools/cru-py/cru/_error.py) | 0 | ||||
-rw-r--r-- | services/manager/_event.py (renamed from tools/cru-py/cru/_event.py) | 0 | ||||
-rw-r--r-- | services/manager/_func.py (renamed from tools/cru-py/cru/_func.py) | 0 | ||||
-rw-r--r-- | services/manager/_helper.py (renamed from tools/cru-py/cru/_helper.py) | 0 | ||||
-rw-r--r-- | services/manager/_iter.py (renamed from tools/cru-py/cru/_iter.py) | 0 | ||||
-rw-r--r-- | services/manager/_type.py (renamed from tools/cru-py/cru/_type.py) | 0 | ||||
-rw-r--r-- | services/manager/attr.py (renamed from tools/cru-py/cru/attr.py) | 0 | ||||
-rw-r--r-- | services/manager/config.py (renamed from tools/cru-py/cru/config.py) | 0 | ||||
-rw-r--r-- | services/manager/list.py (renamed from tools/cru-py/cru/list.py) | 0 | ||||
-rw-r--r-- | services/manager/parsing.py (renamed from tools/cru-py/cru/parsing.py) | 14 | ||||
-rw-r--r-- | services/manager/service/__init__.py (renamed from tools/cru-py/cru/service/__init__.py) | 0 | ||||
-rw-r--r-- | services/manager/service/__main__.py (renamed from tools/cru-py/cru/service/__main__.py) | 9 | ||||
-rw-r--r-- | services/manager/service/_app.py (renamed from tools/cru-py/cru/service/_app.py) | 4 | ||||
-rw-r--r-- | services/manager/service/_base.py (renamed from tools/cru-py/cru/service/_base.py) | 127 | ||||
-rw-r--r-- | services/manager/service/_external.py (renamed from tools/cru-py/cru/service/_external.py) | 0 | ||||
-rw-r--r-- | services/manager/service/_nginx.py (renamed from tools/cru-py/cru/service/_nginx.py) | 31 | ||||
-rw-r--r-- | services/manager/service/_template.py | 228 | ||||
-rw-r--r-- | services/manager/system.py (renamed from tools/cru-py/cru/system.py) | 0 | ||||
-rw-r--r-- | services/manager/template.py (renamed from tools/cru-py/cru/template.py) | 36 | ||||
-rw-r--r-- | services/manager/tool.py (renamed from tools/cru-py/cru/tool.py) | 0 | ||||
-rw-r--r-- | services/manager/value.py (renamed from tools/cru-py/cru/value.py) | 0 | ||||
-rw-r--r-- | services/poetry.lock (renamed from tools/cru-py/poetry.lock) | 0 | ||||
-rw-r--r-- | services/pyproject.toml (renamed from tools/cru-py/pyproject.toml) | 12 | ||||
-rw-r--r-- | services/templates/cgitrc.template (renamed from docker/git-server/cgitrc.template) | 0 | ||||
-rw-r--r-- | services/templates/disabled/docker-compose.yaml (renamed from templates/disabled/docker-compose.yaml) | 0 | ||||
-rw-r--r-- | services/templates/disabled/nginx/code.conf.template (renamed from templates/disabled/nginx/code.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/disabled/nginx/timeline.conf.template (renamed from templates/disabled/nginx/timeline.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/docker-compose.yaml.template (renamed from templates/docker-compose.yaml.template) | 75 | ||||
-rw-r--r-- | services/templates/mailserver.env (renamed from templates/mailserver.env) | 0 | ||||
-rw-r--r-- | services/templates/nginx/common/acme-challenge (renamed from templates/nginx/common/acme-challenge) | 0 | ||||
-rw-r--r-- | services/templates/nginx/common/http-listen (renamed from templates/nginx/common/http-listen) | 0 | ||||
-rw-r--r-- | services/templates/nginx/common/https-listen (renamed from templates/nginx/common/https-listen) | 0 | ||||
-rw-r--r-- | services/templates/nginx/common/https-redirect (renamed from templates/nginx/common/https-redirect) | 0 | ||||
-rw-r--r-- | services/templates/nginx/common/proxy-common (renamed from templates/nginx/common/proxy-common) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/code.conf.template (renamed from templates/nginx/conf.d/code.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/forbid_unknown_domain.conf (renamed from templates/nginx/conf.d/forbid_unknown_domain.conf) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/mail.conf.template (renamed from templates/nginx/conf.d/mail.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/root.conf.template (renamed from templates/nginx/conf.d/root.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/ssl.conf.template (renamed from templates/nginx/conf.d/ssl.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/timeline.conf.template (renamed from templates/nginx/conf.d/timeline.conf.template) | 0 | ||||
-rw-r--r-- | services/templates/nginx/conf.d/websocket.conf (renamed from templates/nginx/conf.d/websocket.conf) | 0 | ||||
-rw-r--r-- | services/templates/v2ray-config.json.template (renamed from templates/v2ray-config.json.template) | 0 | ||||
-rwxr-xr-x | services/update-blog | 5 | ||||
-rw-r--r-- | tools/cru-py/.python-version | 1 | ||||
-rw-r--r-- | tools/cru-py/cru/service/_config.py | 444 | ||||
-rw-r--r-- | tools/cru-py/cru/service/_template.py | 90 | ||||
-rw-r--r-- | tools/cru-py/www-dev | 8 | ||||
-rwxr-xr-x | tools/manage | 16 | ||||
-rw-r--r-- | tools/manage.cmd | 15 | ||||
-rwxr-xr-x | tools/update-blog | 5 |
99 files changed, 437 insertions, 805 deletions
@@ -1,7 +1,2 @@ /data -/log -/tmp -/backup -/generated - /docker-compose.yaml diff --git a/docker/git-server/Dockerfile b/docker/git-server/Dockerfile deleted file mode 100644 index 389b777..0000000 --- a/docker/git-server/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ - -FROM debian:latest AS lighttpd-config-generator -RUN apt-get update && apt-get install -y apache2-utils -RUN --mount=type=secret,id=git-server,required=true \ - . /run/secrets/git-server && \ - htpasswd -cb /user-info ${CRUPEST_GIT_SERVER_USERNAME} ${CRUPEST_GIT_SERVER_PASSWORD} -ARG ROOT_URL -ADD cgitrc.template /cgitrc.template -RUN sed "s|@@CRUPEST_ROOT_URL@@|${ROOT_URL}|g" /cgitrc.template > /cgitrc - -FROM debian:latest -RUN apt-get update && apt-get install -y \ - git cgit lighttpd apache2-utils python3-pygments python3-markdown \ - tar gzip bzip2 zip unzip tini && \ - rm -rf /var/lib/apt/lists/* - -COPY --from=lighttpd-config-generator /user-info /app/ -COPY --from=lighttpd-config-generator /cgitrc /etc/cgitrc -ADD git-lighttpd.conf git-auth.conf /app/ -ADD --chmod=755 lighttpd-wrapper /app/ - -VOLUME [ "/git" ] -ENTRYPOINT ["/usr/bin/tini", "--"] -CMD [ "/app/lighttpd-wrapper" ] diff --git a/docker/git-server/lighttpd-wrapper b/docker/git-server/lighttpd-wrapper deleted file mode 100755 index f071c13..0000000 --- a/docker/git-server/lighttpd-wrapper +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -exec 3>&1 -lighttpd -D -f /app/git-lighttpd.conf diff --git a/tools/cru-py/.gitignore b/services/.gitignore index f5833b1..b284dd9 100644 --- a/tools/cru-py/.gitignore +++ b/services/.gitignore @@ -1,3 +1,5 @@ __pycache__ .venv .mypy_cache + +/generated diff --git a/.python-version b/services/.python-version index 2c07333..2c07333 100644 --- a/.python-version +++ b/services/.python-version diff --git a/services/base-config b/services/base-config new file mode 100644 index 0000000..4c3d60f --- /dev/null +++ b/services/base-config @@ -0,0 +1,4 @@ +CRUPEST_DOMAIN=crupest.life +CRUPEST_EMAIL=crupest@crupest.life +CRUPEST_SERVICES_DIR=services +CRUPEST_DATA_DIR=data diff --git a/services/common.bash b/services/common.bash new file mode 100644 index 0000000..ad08a34 --- /dev/null +++ b/services/common.bash @@ -0,0 +1,5 @@ +# shellcheck disable=SC2046 +export $(xargs < "${script_dir:?}/base-config") + +CRUPEST_PROJECT_DIR="$(realpath "$script_dir/..")" +export CRUPEST_PROJECT_DIR diff --git a/services/config.template b/services/config.template new file mode 100644 index 0000000..9229dbb --- /dev/null +++ b/services/config.template @@ -0,0 +1,10 @@ +CRUPEST_MAIL_SERVER_DOMAIN=mail.@@CRUPEST_DOMAIN@@ +CRUPEST_ROOT_URL=https://@@CRUPEST_DOMAIN@@/ +CRUPEST_DOCKER_DIR=@@CRUPEST_SERVICES_DIR@@/docker +CRUPEST_DATA_SECRET_DIR=@@CRUPEST_DATA_DIR@@/secret +CRUPEST_DATA_CERTBOT_DIR=@@CRUPEST_DATA_DIR@@/certbot +CRUPEST_DATA_GIT_DIR=@@CRUPEST_DATA_DIR@@/git +CRUPEST_DATA_MAILSERVER_DIR=@@CRUPEST_DATA_DIR@@/dms +CRUPEST_DATA_ROUNDCUBE_DIR=@@CRUPEST_DATA_DIR@@/roundcube +CRUPEST_GENERATED_DIR=@@CRUPEST_SERVICES_DIR@@/generated +CRUPEST_GENERATED_NGINX_DIR=@@CRUPEST_GENERATED_DIR@@/nginx diff --git a/docker/auto-backup/Dockerfile b/services/docker/auto-backup/Dockerfile index 943c96f..6736077 100644 --- a/docker/auto-backup/Dockerfile +++ b/services/docker/auto-backup/Dockerfile @@ -11,5 +11,4 @@ ADD --chmod=755 daemon.bash /app/ VOLUME [ "/data" ] -ENTRYPOINT ["tini", "--"] -CMD [ "/app/daemon.bash" ] +CMD [ "tini", "--", "/app/daemon.bash" ] diff --git a/docker/auto-backup/daemon.bash b/services/docker/auto-backup/daemon.bash index 0c6beec..0c6beec 100755 --- a/docker/auto-backup/daemon.bash +++ b/services/docker/auto-backup/daemon.bash diff --git a/docker/blog/Dockerfile b/services/docker/blog/Dockerfile index 7414d4e..7414d4e 100644 --- a/docker/blog/Dockerfile +++ b/services/docker/blog/Dockerfile diff --git a/docker/blog/daemon.bash b/services/docker/blog/daemon.bash index 561a80a..561a80a 100755 --- a/docker/blog/daemon.bash +++ b/services/docker/blog/daemon.bash diff --git a/docker/blog/install-hugo.bash b/services/docker/blog/install-hugo.bash index a448138..a448138 100755 --- a/docker/blog/install-hugo.bash +++ b/services/docker/blog/install-hugo.bash diff --git a/docker/blog/update.bash b/services/docker/blog/update.bash index d4bcadc..d4bcadc 100755 --- a/docker/blog/update.bash +++ b/services/docker/blog/update.bash diff --git a/docker/debian-dev/Dockerfile b/services/docker/debian-dev/Dockerfile index 0629e37..8114c56 100644 --- a/docker/debian-dev/Dockerfile +++ b/services/docker/debian-dev/Dockerfile @@ -21,5 +21,4 @@ RUN --mount=type=secret,id=code-server-password,required=true,env=CRUPEST_CODE_S EXPOSE 4567 VOLUME [ "/home/${USER}" ] -ENTRYPOINT ["tini", "--"] -CMD [ "/usr/bin/code-server", "--bind-addr", "0.0.0.0:4567" ] +CMD [ "tini", "--", "/usr/bin/code-server", "--bind-addr", "0.0.0.0:4567" ] diff --git a/docker/debian-dev/bootstrap/extra/setup-cmake.bash b/services/docker/debian-dev/bootstrap/extra/setup-cmake.bash index 76c1ae4..76c1ae4 100755 --- a/docker/debian-dev/bootstrap/extra/setup-cmake.bash +++ b/services/docker/debian-dev/bootstrap/extra/setup-cmake.bash diff --git a/docker/debian-dev/bootstrap/extra/setup-dotnet.bash b/services/docker/debian-dev/bootstrap/extra/setup-dotnet.bash index 0ef7743..0ef7743 100755 --- a/docker/debian-dev/bootstrap/extra/setup-dotnet.bash +++ b/services/docker/debian-dev/bootstrap/extra/setup-dotnet.bash diff --git a/docker/debian-dev/bootstrap/extra/setup-llvm.bash b/services/docker/debian-dev/bootstrap/extra/setup-llvm.bash index 48dde86..48dde86 100755 --- a/docker/debian-dev/bootstrap/extra/setup-llvm.bash +++ b/services/docker/debian-dev/bootstrap/extra/setup-llvm.bash diff --git a/docker/debian-dev/bootstrap/home/.bashrc b/services/docker/debian-dev/bootstrap/home/.bashrc index 3646ee2..3646ee2 100644 --- a/docker/debian-dev/bootstrap/home/.bashrc +++ b/services/docker/debian-dev/bootstrap/home/.bashrc diff --git a/docker/debian-dev/bootstrap/home/.quiltrc-dpkg b/services/docker/debian-dev/bootstrap/home/.quiltrc-dpkg index e8fc3c5..e8fc3c5 100644 --- a/docker/debian-dev/bootstrap/home/.quiltrc-dpkg +++ b/services/docker/debian-dev/bootstrap/home/.quiltrc-dpkg diff --git a/docker/debian-dev/bootstrap/official.sources b/services/docker/debian-dev/bootstrap/official.sources index c9aa9a0..c9aa9a0 100644 --- a/docker/debian-dev/bootstrap/official.sources +++ b/services/docker/debian-dev/bootstrap/official.sources diff --git a/docker/debian-dev/bootstrap/setup-apt.bash b/services/docker/debian-dev/bootstrap/setup-apt.bash index 38cba05..38cba05 100755 --- a/docker/debian-dev/bootstrap/setup-apt.bash +++ b/services/docker/debian-dev/bootstrap/setup-apt.bash diff --git a/docker/debian-dev/bootstrap/setup.bash b/services/docker/debian-dev/bootstrap/setup.bash index 65aabbb..65aabbb 100755 --- a/docker/debian-dev/bootstrap/setup.bash +++ b/services/docker/debian-dev/bootstrap/setup.bash diff --git a/services/docker/git-server/Dockerfile b/services/docker/git-server/Dockerfile new file mode 100644 index 0000000..8a671d7 --- /dev/null +++ b/services/docker/git-server/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:latest +RUN apt-get update && apt-get install -y \ + git cgit lighttpd apache2-utils python3-pygments python3-markdown \ + tar gzip bzip2 zip unzip tini && \ + rm -rf /var/lib/apt/lists/* + +ADD git-lighttpd.conf git-auth.conf /app/ +ADD --chmod=755 lighttpd-wrapper.bash /app/ + +VOLUME [ "/git" ] +CMD [ "tini", "--", "/app/lighttpd-wrapper.bash" ] diff --git a/docker/git-server/git-auth.conf b/services/docker/git-server/git-auth.conf index 2908bec..1acb316 100644 --- a/docker/git-server/git-auth.conf +++ b/services/docker/git-server/git-auth.conf @@ -1,3 +1,3 @@ auth.backend = "htpasswd" -auth.backend.htpasswd.userfile = "/app/user-info" +auth.backend.htpasswd.userfile = "/git/private/user-info" auth.require = ( "" => ("method" => "basic", "realm" => "Git Access", "require" => "valid-user") ) diff --git a/docker/git-server/git-lighttpd.conf b/services/docker/git-server/git-lighttpd.conf index 5d946bc..ba8e592 100644 --- a/docker/git-server/git-lighttpd.conf +++ b/services/docker/git-server/git-lighttpd.conf @@ -1,5 +1,5 @@ server.modules += ("mod_accesslog") -server.modules += ("mod_auth", "mod_authn_file") +server.modules += ("mod_auth", "mod_authn_file", "mod_access") server.modules += ("mod_setenv", "mod_cgi", "mod_alias") server.document-root = "/var/www/html/" @@ -8,7 +8,10 @@ accesslog.filename = "/dev/fd/3" $HTTP["url"] =^ "/git" { mimetype.assign = ( ".css" => "text/css" ) - $HTTP["url"] =~ "^/git/.*/(HEAD|info/refs|objects/info/[^/]+|git-(upload|receive)-pack)$" { + $HTTP["url"] =^ "/git/private" { + url.access-deny = ("") + } + else $HTTP["url"] =~ "^/git/.*/(HEAD|info/refs|objects/info/[^/]+|git-(upload|receive)-pack)$" { $HTTP["querystring"] =~ "service=git-receive-pack" { include "git-auth.conf" } diff --git a/services/docker/git-server/lighttpd-wrapper.bash b/services/docker/git-server/lighttpd-wrapper.bash new file mode 100755 index 0000000..06dc78f --- /dev/null +++ b/services/docker/git-server/lighttpd-wrapper.bash @@ -0,0 +1,8 @@ +#!/usr/bin/bash + +set -e + +touch -a /git/private/user-info + +exec 3>&1 +exec lighttpd -D -f /app/git-lighttpd.conf diff --git a/docker/nginx/Dockerfile b/services/docker/nginx/Dockerfile index 67d41d1..67d41d1 100644 --- a/docker/nginx/Dockerfile +++ b/services/docker/nginx/Dockerfile diff --git a/docker/nginx/certbot.bash b/services/docker/nginx/certbot.bash index 0b8e3b7..0b8e3b7 100644 --- a/docker/nginx/certbot.bash +++ b/services/docker/nginx/certbot.bash diff --git a/docker/nginx/nginx-wrapper.bash b/services/docker/nginx/nginx-wrapper.bash index bd566aa..bd566aa 100644 --- a/docker/nginx/nginx-wrapper.bash +++ b/services/docker/nginx/nginx-wrapper.bash diff --git a/docker/nginx/sites/www/.dockerignore b/services/docker/nginx/sites/www/.dockerignore index ef718b9..ef718b9 100644 --- a/docker/nginx/sites/www/.dockerignore +++ b/services/docker/nginx/sites/www/.dockerignore diff --git a/docker/nginx/sites/www/.gitignore b/services/docker/nginx/sites/www/.gitignore index 0b1e50b..0b1e50b 100644 --- a/docker/nginx/sites/www/.gitignore +++ b/services/docker/nginx/sites/www/.gitignore diff --git a/docker/nginx/sites/www/avatar.png b/services/docker/nginx/sites/www/avatar.png Binary files differindex d890d8d..d890d8d 100644 --- a/docker/nginx/sites/www/avatar.png +++ b/services/docker/nginx/sites/www/avatar.png diff --git a/docker/nginx/sites/www/favicon.ico b/services/docker/nginx/sites/www/favicon.ico Binary files differindex 922a523..922a523 100644 --- a/docker/nginx/sites/www/favicon.ico +++ b/services/docker/nginx/sites/www/favicon.ico diff --git a/docker/nginx/sites/www/github-mark.png b/services/docker/nginx/sites/www/github-mark.png Binary files differindex 6cb3b70..6cb3b70 100644 --- a/docker/nginx/sites/www/github-mark.png +++ b/services/docker/nginx/sites/www/github-mark.png diff --git a/docker/nginx/sites/www/index.html b/services/docker/nginx/sites/www/index.html index c8d7947..c8d7947 100644 --- a/docker/nginx/sites/www/index.html +++ b/services/docker/nginx/sites/www/index.html diff --git a/docker/nginx/sites/www/package.json b/services/docker/nginx/sites/www/package.json index c5c5d4f..c5c5d4f 100644 --- a/docker/nginx/sites/www/package.json +++ b/services/docker/nginx/sites/www/package.json diff --git a/docker/nginx/sites/www/pnpm-lock.yaml b/services/docker/nginx/sites/www/pnpm-lock.yaml index 1d440a9..1d440a9 100644 --- a/docker/nginx/sites/www/pnpm-lock.yaml +++ b/services/docker/nginx/sites/www/pnpm-lock.yaml diff --git a/docker/nginx/sites/www/src/main.ts b/services/docker/nginx/sites/www/src/main.ts index 09e8661..09e8661 100644 --- a/docker/nginx/sites/www/src/main.ts +++ b/services/docker/nginx/sites/www/src/main.ts diff --git a/docker/nginx/sites/www/src/style.css b/services/docker/nginx/sites/www/src/style.css index 05c98a0..05c98a0 100644 --- a/docker/nginx/sites/www/src/style.css +++ b/services/docker/nginx/sites/www/src/style.css diff --git a/docker/nginx/sites/www/tsconfig.json b/services/docker/nginx/sites/www/tsconfig.json index 9d1434c..9d1434c 100644 --- a/docker/nginx/sites/www/tsconfig.json +++ b/services/docker/nginx/sites/www/tsconfig.json diff --git a/docker/v2ray/Dockerfile b/services/docker/v2ray/Dockerfile index 250a6b8..250a6b8 100644 --- a/docker/v2ray/Dockerfile +++ b/services/docker/v2ray/Dockerfile diff --git a/services/gen-tplt b/services/gen-tplt new file mode 100755 index 0000000..38ceb33 --- /dev/null +++ b/services/gen-tplt @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +script_dir="$(dirname "$0")" + +exec "$script_dir/manage" "template" "generate" "$@" diff --git a/services/git-add-user b/services/git-add-user new file mode 100755 index 0000000..2e500d2 --- /dev/null +++ b/services/git-add-user @@ -0,0 +1,14 @@ +#! /usr/bin/bash + +set -e + +script_dir="$(dirname "$0")" +. "$script_dir/common.bash" + +ps_dir="$CRUPEST_PROJECT_DIR/$CRUPEST_DATA_DIR/git/private" +ps_file="$ps_dir/user-info" +echo "Password file at $ps_file" +[[ -d "$ps_dir" ]] || mkdir -p "$ps_dir" +[[ -f "$ps_file" ]] || touch "$ps_file" + +exec docker run -it --rm -v "$ps_file:/user-info" httpd htpasswd "/user-info" "$1" diff --git a/services/manage b/services/manage new file mode 100755 index 0000000..01f3145 --- /dev/null +++ b/services/manage @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +python3 --version > /dev/null 2>&1 || ( + echo Error: failed to run Python with python3 --version. + exit 1 +) + +script_dir="$(dirname "$0")" +. "$script_dir/common.bash" + +export PYTHONPATH="$CRUPEST_PROJECT_DIR/$CRUPEST_SERVICES_DIR:$PYTHONPATH" +python3 -m manager.service "$@" diff --git a/tools/cru-py/cru/__init__.py b/services/manager/__init__.py index 17799a9..17799a9 100644 --- a/tools/cru-py/cru/__init__.py +++ b/services/manager/__init__.py diff --git a/tools/cru-py/cru/_base.py b/services/manager/_base.py index 2599d8f..2599d8f 100644 --- a/tools/cru-py/cru/_base.py +++ b/services/manager/_base.py diff --git a/tools/cru-py/cru/_const.py b/services/manager/_const.py index 8246b35..8246b35 100644 --- a/tools/cru-py/cru/_const.py +++ b/services/manager/_const.py diff --git a/tools/cru-py/cru/_decorator.py b/services/manager/_decorator.py index 137fc05..137fc05 100644 --- a/tools/cru-py/cru/_decorator.py +++ b/services/manager/_decorator.py diff --git a/tools/cru-py/cru/_error.py b/services/manager/_error.py index e53c787..e53c787 100644 --- a/tools/cru-py/cru/_error.py +++ b/services/manager/_error.py diff --git a/tools/cru-py/cru/_event.py b/services/manager/_event.py index 51a794c..51a794c 100644 --- a/tools/cru-py/cru/_event.py +++ b/services/manager/_event.py diff --git a/tools/cru-py/cru/_func.py b/services/manager/_func.py index fc57802..fc57802 100644 --- a/tools/cru-py/cru/_func.py +++ b/services/manager/_func.py diff --git a/tools/cru-py/cru/_helper.py b/services/manager/_helper.py index 43baf46..43baf46 100644 --- a/tools/cru-py/cru/_helper.py +++ b/services/manager/_helper.py diff --git a/tools/cru-py/cru/_iter.py b/services/manager/_iter.py index f9683ca..f9683ca 100644 --- a/tools/cru-py/cru/_iter.py +++ b/services/manager/_iter.py diff --git a/tools/cru-py/cru/_type.py b/services/manager/_type.py index 1f81da3..1f81da3 100644 --- a/tools/cru-py/cru/_type.py +++ b/services/manager/_type.py diff --git a/tools/cru-py/cru/attr.py b/services/manager/attr.py index d4cc86a..d4cc86a 100644 --- a/tools/cru-py/cru/attr.py +++ b/services/manager/attr.py diff --git a/tools/cru-py/cru/config.py b/services/manager/config.py index 0f6f0d0..0f6f0d0 100644 --- a/tools/cru-py/cru/config.py +++ b/services/manager/config.py diff --git a/tools/cru-py/cru/list.py b/services/manager/list.py index 216a561..216a561 100644 --- a/tools/cru-py/cru/list.py +++ b/services/manager/list.py diff --git a/tools/cru-py/cru/parsing.py b/services/manager/parsing.py index c31ce35..0e9239d 100644 --- a/tools/cru-py/cru/parsing.py +++ b/services/manager/parsing.py @@ -154,23 +154,23 @@ class Parser(Generic[_T], metaclass=ABCMeta): raise ParseError(f"Parser {self.name} failed{a}.", self, text, line_number) -class SimpleLineConfigParserEntry(NamedTuple): +class _SimpleLineVarParserEntry(NamedTuple): key: str value: str line_number: int | None = None -class SimpleLineConfigParserResult(CruIterable.IterList[SimpleLineConfigParserEntry]): +class _SimpleLineVarParserResult(CruIterable.IterList[_SimpleLineVarParserEntry]): pass -class SimpleLineConfigParser(Parser[SimpleLineConfigParserResult]): +class SimpleLineVarParser(Parser[_SimpleLineVarParserResult]): """ The parsing result is a list of tuples (key, value, line number). """ - Entry: TypeAlias = SimpleLineConfigParserEntry - Result: TypeAlias = SimpleLineConfigParserResult + Entry: TypeAlias = _SimpleLineVarParserEntry + Result: TypeAlias = _SimpleLineVarParserResult def __init__(self) -> None: super().__init__(type(self).__name__) @@ -188,10 +188,10 @@ class SimpleLineConfigParser(Parser[SimpleLineConfigParserResult]): key, value = line.split("=", 1) key = key.strip() value = value.strip() - callback(SimpleLineConfigParserEntry(key, value, line_number)) + callback(_SimpleLineVarParserEntry(key, value, line_number)) def parse(self, text: str) -> Result: - result = SimpleLineConfigParserResult() + result = _SimpleLineVarParserResult() self._parse(text, lambda item: result.append(item)) return result diff --git a/tools/cru-py/cru/service/__init__.py b/services/manager/service/__init__.py index e69de29..e69de29 100644 --- a/tools/cru-py/cru/service/__init__.py +++ b/services/manager/service/__init__.py diff --git a/tools/cru-py/cru/service/__main__.py b/services/manager/service/__main__.py index 1c10e82..6ea0a8a 100644 --- a/tools/cru-py/cru/service/__main__.py +++ b/services/manager/service/__main__.py @@ -1,4 +1,6 @@ -from cru import CruException +import sys + +from manager import CruException from ._app import create_app @@ -9,6 +11,11 @@ def main(): if __name__ == "__main__": + version_info = sys.version_info + if not (version_info.major == 3 and version_info.minor >= 11): + print("This application requires Python 3.11 or later.", file=sys.stderr) + sys.exit(1) + try: main() except CruException as e: diff --git a/tools/cru-py/cru/service/_app.py b/services/manager/service/_app.py index 6030dad..2304340 100644 --- a/tools/cru-py/cru/service/_app.py +++ b/services/manager/service/_app.py @@ -1,10 +1,8 @@ from ._base import ( AppBase, CommandDispatcher, - AppInitializer, PathCommandProvider, ) -from ._config import ConfigManager from ._template import TemplateManager from ._nginx import NginxManager from ._external import CliToolCommandProvider @@ -16,8 +14,6 @@ class App(AppBase): def __init__(self): super().__init__(APP_ID, f"{APP_ID}-service") self.add_feature(PathCommandProvider()) - self.add_feature(AppInitializer()) - self.add_feature(ConfigManager()) self.add_feature(TemplateManager()) self.add_feature(NginxManager()) self.add_feature(CliToolCommandProvider()) diff --git a/tools/cru-py/cru/service/_base.py b/services/manager/service/_base.py index ad813c9..783296c 100644 --- a/tools/cru-py/cru/service/_base.py +++ b/services/manager/service/_base.py @@ -7,7 +7,7 @@ import os from pathlib import Path from typing import TypeVar, overload -from cru import CruException, CruLogicError +from manager import CruException, CruLogicError _Feature = TypeVar("_Feature", bound="AppFeatureProvider") @@ -106,6 +106,12 @@ class AppPath(ABC): with open(self.full_path, "w") as f: f.write("") + def read_text(self) -> str: + if self.is_dir: + raise AppPathError("Can't read text of a dir.", self.full_path) + self.check_self() + return self.full_path.read_text() + def add_subpath( self, name: str, @@ -114,7 +120,7 @@ class AppPath(ABC): id: str | None = None, description: str = "", ) -> AppFeaturePath: - return self.app.add_path(name, is_dir, self, id, description) + return self.app._add_path(name, is_dir, self, id, description) @property def app_relative_path(self) -> Path: @@ -153,10 +159,10 @@ class AppFeaturePath(AppPath): class AppRootPath(AppPath): - def __init__(self, app: AppBase): - super().__init__("root", True, "Application root path.") + def __init__(self, app: AppBase, path: Path): + super().__init__(f"/{id}", True, f"Application {id} path.") self._app = app - self._full_path: Path | None = None + self._full_path = path.resolve() @property def parent(self) -> None: @@ -168,15 +174,8 @@ class AppRootPath(AppPath): @property def full_path(self) -> Path: - if self._full_path is None: - raise AppError("App root path is not set yet.") return self._full_path - def setup(self, path: os.PathLike) -> None: - if self._full_path is not None: - raise AppError("App root path is already set.") - self._full_path = Path(path).resolve() - class AppFeatureProvider(ABC): def __init__(self, name: str, /, app: AppBase | None = None): @@ -207,9 +206,6 @@ class AppCommandFeatureProvider(AppFeatureProvider): def run_command(self, args: Namespace) -> None: ... -DATA_DIR_NAME = "data" - - class PathCommandProvider(AppCommandFeatureProvider): def __init__(self) -> None: super().__init__("path-command-provider") @@ -237,33 +233,12 @@ class PathCommandProvider(AppCommandFeatureProvider): class CommandDispatcher(AppFeatureProvider): def __init__(self) -> None: super().__init__("command-dispatcher") - self._parsed_args: argparse.Namespace | None = None def setup_arg_parser(self) -> None: - epilog = """ -==> to start, -./tools/manage init -./tools/manage config init -ln -s generated/docker-compose.yaml . -# Then edit config file. - -==> to update -git pull -./tools/manage template generate --no-dry-run -docker compose up - """.strip() - self._map: dict[str, AppCommandFeatureProvider] = {} arg_parser = argparse.ArgumentParser( description="Service management", formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=epilog, - ) - arg_parser.add_argument( - "--project-dir", - help="The path of the project directory.", - required=True, - type=str, ) subparsers = arg_parser.add_subparsers( dest="command", @@ -279,54 +254,26 @@ docker compose up self._arg_parser = arg_parser def setup(self): - pass + self._parsed_args = self.arg_parser.parse_args() @property def arg_parser(self) -> argparse.ArgumentParser: return self._arg_parser @property - def map(self) -> dict[str, AppCommandFeatureProvider]: + def command_map(self) -> dict[str, AppCommandFeatureProvider]: return self._map - def get_program_parsed_args(self) -> argparse.Namespace: - if self._parsed_args is None: - self._parsed_args = self.arg_parser.parse_args() + @property + def program_args(self) -> argparse.Namespace: return self._parsed_args - def run_command(self, args: argparse.Namespace | None = None) -> None: - real_args = args or self.get_program_parsed_args() - if real_args.command is None: + def run_command(self) -> None: + args = self.program_args + if args.command is None: self.arg_parser.print_help() return - self.map[real_args.command].run_command(real_args) - - -class AppInitializer(AppCommandFeatureProvider): - def __init__(self) -> None: - super().__init__("app-initializer") - - def _init_app(self) -> bool: - if self.app.app_initialized: - return False - self.app.data_dir.ensure() - return True - - def setup(self): - pass - - def get_command_info(self): - return ("init", "Initialize the app.") - - def setup_arg_parser(self, arg_parser): - pass - - def run_command(self, args): - init = self._init_app() - if init: - print("App initialized successfully.") - else: - print("App is already initialized. Do nothing.") + self.command_map[args.command].run_command(args) class AppBase: @@ -342,16 +289,19 @@ class AppBase: AppBase._instance = self self._app_id = app_id self._name = name - self._root = AppRootPath(self) - self._paths: list[AppFeaturePath] = [] self._features: list[AppFeatureProvider] = [] + self._paths: list[AppFeaturePath] = [] def setup(self) -> None: command_dispatcher = self.get_feature(CommandDispatcher) command_dispatcher.setup_arg_parser() - program_args = command_dispatcher.get_program_parsed_args() - self.setup_root(program_args.project_dir) - self._data_dir = self.add_path(DATA_DIR_NAME, True, id="data") + self._root = AppRootPath(self, Path(self._ensure_env("CRUPEST_PROJECT_DIR"))) + self._data_dir = self._root.add_subpath( + self._ensure_env("CRUPEST_DATA_DIR"), True, id="data" + ) + self._services_dir = self._root.add_subpath( + self._ensure_env("CRUPEST_SERVICES_DIR"), True, id="CRUPEST_SERVICES_DIR" + ) for feature in self.features: feature.setup() for path in self.paths: @@ -365,29 +315,28 @@ class AppBase: def name(self) -> str: return self._name + def _ensure_env(self, env_name: str) -> str: + value = os.getenv(env_name) + if value is None: + raise AppError(f"Environment variable {env_name} not set") + return value + @property def root(self) -> AppRootPath: return self._root - def setup_root(self, path: os.PathLike) -> None: - self._root.setup(path) - @property def data_dir(self) -> AppFeaturePath: return self._data_dir @property + def services_dir(self) -> AppFeaturePath: + return self._services_dir + + @property def app_initialized(self) -> bool: return self.data_dir.check_self() - def ensure_app_initialized(self) -> AppRootPath: - if not self.app_initialized: - raise AppError( - user_message="Root directory does not exist. " - "Please run 'init' to create one." - ) - return self.root - @property def features(self) -> list[AppFeatureProvider]: return self._features @@ -405,7 +354,7 @@ class AppBase: self._features.append(feature) return feature - def add_path( + def _add_path( self, name: str, is_dir: bool, diff --git a/tools/cru-py/cru/service/_external.py b/services/manager/service/_external.py index 2347e95..2347e95 100644 --- a/tools/cru-py/cru/service/_external.py +++ b/services/manager/service/_external.py diff --git a/tools/cru-py/cru/service/_nginx.py b/services/manager/service/_nginx.py index 6c77971..5dfc3ab 100644 --- a/tools/cru-py/cru/service/_nginx.py +++ b/services/manager/service/_nginx.py @@ -4,10 +4,9 @@ import re import subprocess from typing import TypeAlias -from cru import CruInternalError +from manager import CruInternalError from ._base import AppCommandFeatureProvider -from ._config import ConfigManager from ._template import TemplateManager @@ -29,12 +28,12 @@ class NginxManager(AppCommandFeatureProvider): pass @property - def _config_manager(self) -> ConfigManager: - return self.app.get_feature(ConfigManager) + def _template_manager(self) -> TemplateManager: + return self.app.get_feature(TemplateManager) @property def root_domain(self) -> str: - return self._config_manager.get_domain_value_str() + return self._template_manager.get_domain() @property def domains(self) -> list[str]: @@ -47,10 +46,6 @@ class NginxManager(AppCommandFeatureProvider): suffix = "." + self.root_domain return [d[: -len(suffix)] for d in self.domains if d.endswith(suffix)] - @property - def _domain_config_name(self) -> str: - return self._config_manager.domain_item_name - def _get_domains_from_text(self, text: str) -> set[str]: domains: set[str] = set() regex = re.compile(r"server_name\s+(\S+)\s*;") @@ -59,15 +54,15 @@ class NginxManager(AppCommandFeatureProvider): return domains def _join_generated_nginx_conf_text(self) -> str: - text = "" - template_manager = self.app.get_feature(TemplateManager) - for nginx_conf in template_manager.generate(): - text += nginx_conf[1] - return text + result = "" + for path, text in self._template_manager.generate(): + if path.parents[-1] == "nginx": + result += text + return result def _get_domains(self) -> list[str]: text = self._join_generated_nginx_conf_text() - domains = list(self._get_domains_from_text(text)) + domains = self._get_domains_from_text(text) domains.remove(self.root_domain) return [self.root_domain, *domains] @@ -155,7 +150,7 @@ class NginxManager(AppCommandFeatureProvider): self._certbot_command( CertbotAction.CREATE, test, - email=self._config_manager.get_email_value_str_optional(), + email=self._template_manager.get_email(), ) ) print() @@ -164,7 +159,7 @@ class NginxManager(AppCommandFeatureProvider): self._certbot_command( CertbotAction.EXPAND, test, - email=self._config_manager.get_email_value_str_optional(), + email=self._template_manager.get_email(), ) ) print() @@ -173,7 +168,7 @@ class NginxManager(AppCommandFeatureProvider): self._certbot_command( CertbotAction.RENEW, test, - email=self._config_manager.get_email_value_str_optional(), + email=self._template_manager.get_email(), ) ) diff --git a/services/manager/service/_template.py b/services/manager/service/_template.py new file mode 100644 index 0000000..90c19ec --- /dev/null +++ b/services/manager/service/_template.py @@ -0,0 +1,228 @@ +from argparse import Namespace +from pathlib import Path +import shutil +from typing import NamedTuple +import graphlib + +from manager import CruException +from manager.parsing import SimpleLineVarParser +from manager.template import TemplateTree, CruStrWrapperTemplate + +from ._base import AppCommandFeatureProvider, AppFeaturePath + + +class _Config(NamedTuple): + text: str + config: dict[str, str] + + +class _GeneratedConfig(NamedTuple): + base: _Config + private: _Config + merged: _Config + + +class _PreConfig(NamedTuple): + base: _Config + private: _Config + config: dict[str, str] + + @staticmethod + def create(base: _Config, private: _Config) -> "_PreConfig": + return _PreConfig(base, private, {**base.config, **private.config}) + + def _merge(self, generated: _Config): + text = ( + "\n".join( + [ + self.private.text.strip(), + self.base.text.strip(), + generated.text.strip(), + ] + ) + + "\n" + ) + config = {**self.config, **generated.config} + return _GeneratedConfig(self.base, self.private, _Config(text, config)) + + +class _Template(NamedTuple): + config: CruStrWrapperTemplate + config_vars: set[str] + tree: TemplateTree + + +class TemplateManager(AppCommandFeatureProvider): + def __init__(self): + super().__init__("template-manager") + + def setup(self) -> None: + self._base_config_file = self.app.services_dir.add_subpath("base-config", False) + self._private_config_file = self.app.data_dir.add_subpath("config", False) + self._template_config_file = self.app.services_dir.add_subpath( + "config.template", False + ) + self._templates_dir = self.app.services_dir.add_subpath("templates", True) + self._generated_dir = self.app.services_dir.add_subpath("generated", True) + + self._config_parser = SimpleLineVarParser() + + def _read_pre(app_path: AppFeaturePath) -> _Config: + text = app_path.read_text() + config = self._read_config(text) + return _Config(text, config) + + base = _read_pre(self._base_config_file) + private = _read_pre(self._private_config_file) + self._preconfig = _PreConfig.create(base, private) + + self._generated: _GeneratedConfig | None = None + + template_config_text = self._template_config_file.read_text() + self._template_config = self._read_config(template_config_text) + + self._template = _Template( + CruStrWrapperTemplate(template_config_text), + set(self._template_config.keys()), + TemplateTree( + lambda text: CruStrWrapperTemplate(text), + self.templates_dir.full_path_str, + ), + ) + + self._real_required_vars = ( + self._template.config_vars | self._template.tree.variables + ) - self._template.config_vars + lacks = self._real_required_vars - self._preconfig.config.keys() + self._lack_vars = lacks if len(lacks) > 0 else None + + def _read_config_entry_names(self, text: str) -> set[str]: + return set(entry.key for entry in self._config_parser.parse(text)) + + def _read_config(self, text: str) -> dict[str, str]: + return {entry.key: entry.value for entry in self._config_parser.parse(text)} + + @property + def templates_dir(self) -> AppFeaturePath: + return self._templates_dir + + @property + def generated_dir(self) -> AppFeaturePath: + return self._generated_dir + + def get_domain(self) -> str: + return self._preconfig.config["CRUPEST_DOMAIN"] + + def get_email(self) -> str: + return self._preconfig.config["CRUPEST_EMAIL"] + + def _generate_template_config(self, config: dict[str, str]) -> dict[str, str]: + entry_templates = { + key: CruStrWrapperTemplate(value) + for key, value in self._template_config.items() + } + sorter = graphlib.TopologicalSorter( + config + | {key: template.variables for key, template in entry_templates.items()} + ) + + vars: dict[str, str] = config.copy() + for _ in sorter.static_order(): + del_keys = [] + for key, template in entry_templates.items(): + new = template.generate_partial(vars) + if not new.has_variables: + vars[key] = new.generate({}) + del_keys.append(key) + else: + entry_templates[key] = new + for key in del_keys: + del entry_templates[key] + assert len(entry_templates) == 0 + return {key: value for key, value in vars.items() if key not in config} + + def _generate_config(self) -> _GeneratedConfig: + if self._generated is not None: + return self._generated + if self._lack_vars is not None: + raise CruException(f"Required vars are not defined: {self._lack_vars}.") + config = self._generate_template_config(self._preconfig.config) + text = self._template.config.generate(self._preconfig.config | config) + self._generated = self._preconfig._merge(_Config(text, config)) + return self._generated + + def generate(self) -> list[tuple[Path, str]]: + config = self._generate_config() + return [ + (Path("config"), config.merged.text), + *self._template.tree.generate(config.merged.config), + ] + + def _generate_files(self, dry_run: bool) -> None: + result = self.generate() + if not dry_run: + if self.generated_dir.full_path.exists(): + shutil.rmtree(self.generated_dir.full_path) + for path, text in result: + des = self.generated_dir.full_path / path + des.parent.mkdir(parents=True, exist_ok=True) + with open(des, "w") as f: + f.write(text) + + def get_command_info(self): + return ("template", "Manage templates.") + + def _print_file_lists(self) -> None: + print(f"[{self._template.config.variable_count}]", "config") + for path, template in self._template.tree.templates: + print(f"[{template.variable_count}]", path.as_posix()) + + def _print_vars(self, required: bool) -> None: + for var in self._template.config.variables: + print(f"[config] {var}") + for var in self._template.tree.variables: + if not (required and var in self._template.config_vars): + print(f"[template] {var}") + + def _run_check_vars(self) -> None: + if self._lack_vars is not None: + print("Lacks:") + for var in self._lack_vars: + print(var) + + def setup_arg_parser(self, arg_parser): + subparsers = arg_parser.add_subparsers( + dest="template_command", required=True, metavar="TEMPLATE_COMMAND" + ) + _list_parser = subparsers.add_parser("list", help="list templates") + vars_parser = subparsers.add_parser( + "vars", help="list variables used in all templates" + ) + vars_parser.add_argument( + "-r", + "--required", + help="only list really required one.", + action="store_true", + ) + _check_vars_parser = subparsers.add_parser( + "check-vars", + help="check if required vars are set", + ) + generate_parser = subparsers.add_parser("generate", help="generate templates") + generate_parser.add_argument( + "--no-dry-run", action="store_true", help="generate and write target files" + ) + + def run_command(self, args: Namespace) -> None: + if args.template_command == "list": + self._print_file_lists() + elif args.template_command == "vars": + self._print_vars(args.required) + elif args.template_command == "generate": + dry_run = not args.no_dry_run + self._generate_files(dry_run) + if dry_run: + print("Dry run successfully.") + print( + f"Will delete dir {self.generated_dir.full_path_str} if it exists." + ) diff --git a/tools/cru-py/cru/system.py b/services/manager/system.py index f321717..f321717 100644 --- a/tools/cru-py/cru/system.py +++ b/services/manager/system.py diff --git a/tools/cru-py/cru/template.py b/services/manager/template.py index 35d68ac..3a70337 100644 --- a/tools/cru-py/cru/template.py +++ b/services/manager/template.py @@ -2,7 +2,7 @@ from abc import ABCMeta, abstractmethod from collections.abc import Callable, Mapping from pathlib import Path from string import Template -from typing import Generic, TypeVar +from typing import Generic, Self, TypeVar from ._iter import CruIterator from ._error import CruException @@ -45,14 +45,27 @@ class CruTemplateBase(metaclass=ABCMeta): def _do_generate(self, mapping: dict[str, str]) -> str: raise NotImplementedError() - def generate(self, mapping: Mapping[str, str], allow_extra: bool = True) -> str: + def _generate_partial( + self, mapping: Mapping[str, str], allow_unused: bool = True + ) -> str: values = dict(mapping) - if not self.variables <= set(values.keys()): - raise CruTemplateError("Missing variables.") - if not allow_extra and not set(values.keys()) <= self.variables: - raise CruTemplateError("Extra variables.") + if not allow_unused and not len(set(values.keys() - self.variables)) != 0: + raise CruTemplateError("Unused variables.") return self._do_generate(values) + def generate_partial( + self, mapping: Mapping[str, str], allow_unused: bool = True + ) -> Self: + return self.__class__(self._generate_partial(mapping, allow_unused)) + + def generate(self, mapping: Mapping[str, str], allow_unused: bool = True) -> str: + values = dict(mapping) + if len(self.variables - values.keys()) != 0: + raise CruTemplateError( + f"Missing variables: {self.variables - values.keys()} ." + ) + return self._generate_partial(values, allow_unused) + class CruTemplate(CruTemplateBase): def __init__(self, prefix: str, text: str): @@ -194,14 +207,3 @@ class TemplateTree(Generic[_Template]): text = template.generate(variables) result.append((path, text)) return result - - def generate_to( - self, destination: str, variables: Mapping[str, str], dry_run: bool - ) -> None: - generated = self.generate(variables) - if not dry_run: - for path, text in generated: - des = Path(destination) / path - des.parent.mkdir(parents=True, exist_ok=True) - with open(des, "w") as f: - f.write(text) diff --git a/tools/cru-py/cru/tool.py b/services/manager/tool.py index 377f5d7..377f5d7 100644 --- a/tools/cru-py/cru/tool.py +++ b/services/manager/tool.py diff --git a/tools/cru-py/cru/value.py b/services/manager/value.py index 9c03219..9c03219 100644 --- a/tools/cru-py/cru/value.py +++ b/services/manager/value.py diff --git a/tools/cru-py/poetry.lock b/services/poetry.lock index 4338200..4338200 100644 --- a/tools/cru-py/poetry.lock +++ b/services/poetry.lock diff --git a/tools/cru-py/pyproject.toml b/services/pyproject.toml index 0ce2c60..960e161 100644 --- a/tools/cru-py/pyproject.toml +++ b/services/pyproject.toml @@ -1,19 +1,11 @@ [project] -name = "cru-py" +name = "cru-service-manager" version = "0.1.0" requires-python = ">=3.11" +license = "MIT" [tool.poetry] package-mode = false -name = "cru" -version = "0.1.0" -description = "" -authors = ["Yuqian Yang <crupest@crupest.life>"] -license = "MIT" -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.11" [tool.poetry.group.dev.dependencies] mypy = "^1.13.0" diff --git a/docker/git-server/cgitrc.template b/services/templates/cgitrc.template index f3c61eb..f3c61eb 100644 --- a/docker/git-server/cgitrc.template +++ b/services/templates/cgitrc.template diff --git a/templates/disabled/docker-compose.yaml b/services/templates/disabled/docker-compose.yaml index 565ca49..565ca49 100644 --- a/templates/disabled/docker-compose.yaml +++ b/services/templates/disabled/docker-compose.yaml diff --git a/templates/disabled/nginx/code.conf.template b/services/templates/disabled/nginx/code.conf.template index 0abe042..0abe042 100644 --- a/templates/disabled/nginx/code.conf.template +++ b/services/templates/disabled/nginx/code.conf.template diff --git a/templates/disabled/nginx/timeline.conf.template b/services/templates/disabled/nginx/timeline.conf.template index ce7341b..ce7341b 100644 --- a/templates/disabled/nginx/timeline.conf.template +++ b/services/templates/disabled/nginx/timeline.conf.template diff --git a/templates/docker-compose.yaml.template b/services/templates/docker-compose.yaml.template index ef103f4..d6640ef 100644 --- a/templates/docker-compose.yaml.template +++ b/services/templates/docker-compose.yaml.template @@ -3,7 +3,7 @@ services: blog: pull_policy: build build: - context: ./docker/blog + context: ./@@CRUPEST_DOCKER_DIR@@/blog dockerfile: Dockerfile pull: true volumes: @@ -13,7 +13,7 @@ services: nginx: pull_policy: build build: - context: ./docker/nginx + context: ./@@CRUPEST_DOCKER_DIR@@/nginx dockerfile: Dockerfile pull: true ports: @@ -21,37 +21,40 @@ services: - "443:443" - "443:443/udp" volumes: - - "./generated/nginx/conf.d:/etc/nginx/conf.d:ro" - - "./generated/nginx/common:/etc/nginx/common:ro" - - "./data/certbot/certs:/etc/letsencrypt" - - "./data/certbot/webroot:/srv/acme:ro" - - "./data/certbot/data:/var/lib/letsencrypt" - - "./data/certbot/webroot:/var/www/certbot" + - "./@@CRUPEST_GENERATED_NGINX_DIR@@/conf.d:/etc/nginx/conf.d:ro" + - "./@@CRUPEST_GENERATED_NGINX_DIR@@/common:/etc/nginx/common:ro" + - "./@@CRUPEST_DATA_CERTBOT_DIR@@/certs:/etc/letsencrypt" + - "./@@CRUPEST_DATA_CERTBOT_DIR@@/webroot:/srv/acme:ro" + - "./@@CRUPEST_DATA_CERTBOT_DIR@@/data:/var/lib/letsencrypt" + - "./@@CRUPEST_DATA_CERTBOT_DIR@@/webroot:/var/www/certbot" - "blog-public:/srv/www/blog:ro" restart: on-failure:3 v2ray: pull_policy: build build: - context: ./docker/v2ray + context: ./@@CRUPEST_DOCKER_DIR@@/v2ray dockerfile: Dockerfile pull: true hostname: v2ray command: [ "run", "-c", "/etc/v2fly/config.json" ] volumes: - - "./generated/v2ray-config.json:/etc/v2fly/config.json:ro" + - "./@@CRUPEST_GENERATED_DIR@@/v2ray-config.json:/etc/v2fly/config.json:ro" restart: on-failure:3 auto-backup: pull_policy: build build: - context: ./docker/auto-backup + context: ./@@CRUPEST_DOCKER_DIR@@/auto-backup dockerfile: Dockerfile pull: true + environment: + - "CRUPEST_AUTO_BACKUP_COS_ENDPOINT=@@CRUPEST_AUTO_BACKUP_COS_ENDPOINT@@" + - "CRUPEST_AUTO_BACKUP_COS_BUCKET=@@CRUPEST_AUTO_BACKUP_COS_BUCKET@@" + - "CRUPEST_AUTO_BACKUP_COS_SECRET_ID=@@CRUPEST_AUTO_BACKUP_COS_SECRET_ID@@" + - "CRUPEST_AUTO_BACKUP_COS_SECRET_KEY=@@CRUPEST_AUTO_BACKUP_COS_SECRET_KEY@@" volumes: - "./data:/data" - secrets: - - auto-backup restart: on-failure:3 mailserver: @@ -59,7 +62,7 @@ services: pull_policy: always container_name: mailserver hostname: mail.@@CRUPEST_DOMAIN@@ - env_file: generated/mailserver.env + env_file: ./@@CRUPEST_GENERATED_DIR@@/mailserver.env # More information about the mail-server ports: # https://docker-mailserver.github.io/docker-mailserver/edge/config/security/understanding-the-ports/ # To avoid conflicts with yaml base-60 float, DO NOT remove the quotation marks. @@ -71,11 +74,11 @@ services: - "993:993" # IMAP4 (implicit TLS) - "4190:4190" # manage sieve protocol volumes: - - ./data/dms/mail-data/:/var/mail/ - - ./data/dms/mail-state/:/var/mail-state/ - - ./data/dms/mail-logs/:/var/log/mail/ - - ./data/dms/config/:/tmp/docker-mailserver/ - - ./data/certbot/certs:/etc/letsencrypt + - ./@@CRUPEST_DATA_MAILSERVER_DIR@@/mail-data/:/var/mail/ + - ./@@CRUPEST_DATA_MAILSERVER_DIR@@/mail-state/:/var/mail-state/ + - ./@@CRUPEST_DATA_MAILSERVER_DIR@@/mail-logs/:/var/log/mail/ + - ./@@CRUPEST_DATA_MAILSERVER_DIR@@/config/:/tmp/docker-mailserver/ + - ./@@CRUPEST_DATA_CERTBOT_DIR@@/certs:/etc/letsencrypt - /etc/localtime:/etc/localtime:ro restart: on-failure:3 stop_grace_period: 1m @@ -87,16 +90,13 @@ services: git-server: pull_policy: build build: - context: ./docker/git-server + context: ./@@CRUPEST_DOCKER_DIR@@/git-server dockerfile: Dockerfile - secrets: - - "git-server" pull: true - args: - - ROOT_URL=https://@@CRUPEST_DOMAIN@@/git hostname: git-server volumes: - - "./data/git:/git" + - "./@@CRUPEST_DATA_GIT_DIR@@:/git" + - "./@@CRUPEST_GENERATED_DIR@@/cgitrc:/etc/cgitrc:ro" restart: on-failure:3 roundcubemail: @@ -104,15 +104,15 @@ services: pull_policy: always hostname: roundcubemail volumes: - - ./data/secret/gnupg:/gnupg - - ./data/roundcube/www/html:/var/www/html - - ./data/roundcube/db:/var/roundcube/db - - ./data/roundcube/config:/var/roundcube/config + - ./@@CRUPEST_DATA_SECRET_DIR@@/gnupg:/gnupg + - ./@@CRUPEST_DATA_ROUNDCUBE_DIR@@/www/html:/var/www/html + - ./@@CRUPEST_DATA_ROUNDCUBE_DIR@@/db:/var/roundcube/db + - ./@@CRUPEST_DATA_ROUNDCUBE_DIR@@/config:/var/roundcube/config - roundcubemail-temp:/tmp/roundcube-temp environment: - - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mail.crupest.life + - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://@@CRUPEST_MAIL_SERVER_DOMAIN@@ - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=ssl://mail.crupest.life + - ROUNDCUBEMAIL_SMTP_SERVER=ssl://@@CRUPEST_MAIL_SERVER_DOMAIN@@ - ROUNDCUBEMAIL_SMTP_PORT=465 - ROUNDCUBEMAIL_DB_TYPE=sqlite - ROUNDCUBEMAIL_PLUGINS=archive,enigma,jqueryui,newmail_notifier,show_additional_headers,userinfo,zipdownload,managesieve @@ -127,12 +127,12 @@ services: environment: - APP_NAME=2FAuth-crupest - APP_TIMEZONE=UTC - - SITE_OWNER=crupest@crupest.life + - SITE_OWNER=@@CRUPEST_EMAIL@@ - APP_KEY=@@CRUPEST_2FAUTH_APP_KEY@@ - - APP_URL=https://@@CRUPEST_DOMAIN@@/2fa + - APP_URL=@@CRUPEST_ROOT_URL@@2fa - APP_SUBDIRECTORY=2fa - MAIL_MAILER=smtp - - MAIL_HOST=mail.crupest.life + - MAIL_HOST=@@CRUPEST_MAIL_SERVER_DOMAIN@@ - MAIL_PORT=465 - MAIL_USERNAME=@@CRUPEST_2FAUTH_MAIL_USERNAME@@ - MAIL_PASSWORD=@@CRUPEST_2FAUTH_MAIL_PASSWORD@@ @@ -144,10 +144,3 @@ services: volumes: blog-public: roundcubemail-temp: - -secrets: - auto-backup: - file: data/config - - git-server: - file: data/config diff --git a/templates/mailserver.env b/services/templates/mailserver.env index 9b12dfe..9b12dfe 100644 --- a/templates/mailserver.env +++ b/services/templates/mailserver.env diff --git a/templates/nginx/common/acme-challenge b/services/templates/nginx/common/acme-challenge index 26054b8..26054b8 100644 --- a/templates/nginx/common/acme-challenge +++ b/services/templates/nginx/common/acme-challenge diff --git a/templates/nginx/common/http-listen b/services/templates/nginx/common/http-listen index 76cb18d..76cb18d 100644 --- a/templates/nginx/common/http-listen +++ b/services/templates/nginx/common/http-listen diff --git a/templates/nginx/common/https-listen b/services/templates/nginx/common/https-listen index db2f68e..db2f68e 100644 --- a/templates/nginx/common/https-listen +++ b/services/templates/nginx/common/https-listen diff --git a/templates/nginx/common/https-redirect b/services/templates/nginx/common/https-redirect index 56d095d..56d095d 100644 --- a/templates/nginx/common/https-redirect +++ b/services/templates/nginx/common/https-redirect diff --git a/templates/nginx/common/proxy-common b/services/templates/nginx/common/proxy-common index 4193548..4193548 100644 --- a/templates/nginx/common/proxy-common +++ b/services/templates/nginx/common/proxy-common diff --git a/templates/nginx/conf.d/code.conf.template b/services/templates/nginx/conf.d/code.conf.template index 35f74d8..35f74d8 100644 --- a/templates/nginx/conf.d/code.conf.template +++ b/services/templates/nginx/conf.d/code.conf.template diff --git a/templates/nginx/conf.d/forbid_unknown_domain.conf b/services/templates/nginx/conf.d/forbid_unknown_domain.conf index 515942b..515942b 100644 --- a/templates/nginx/conf.d/forbid_unknown_domain.conf +++ b/services/templates/nginx/conf.d/forbid_unknown_domain.conf diff --git a/templates/nginx/conf.d/mail.conf.template b/services/templates/nginx/conf.d/mail.conf.template index 2eb53d7..2eb53d7 100644 --- a/templates/nginx/conf.d/mail.conf.template +++ b/services/templates/nginx/conf.d/mail.conf.template diff --git a/templates/nginx/conf.d/root.conf.template b/services/templates/nginx/conf.d/root.conf.template index 8cd9174..8cd9174 100644 --- a/templates/nginx/conf.d/root.conf.template +++ b/services/templates/nginx/conf.d/root.conf.template diff --git a/templates/nginx/conf.d/ssl.conf.template b/services/templates/nginx/conf.d/ssl.conf.template index 181a1af..181a1af 100644 --- a/templates/nginx/conf.d/ssl.conf.template +++ b/services/templates/nginx/conf.d/ssl.conf.template diff --git a/templates/nginx/conf.d/timeline.conf.template b/services/templates/nginx/conf.d/timeline.conf.template index df4edf8..df4edf8 100644 --- a/templates/nginx/conf.d/timeline.conf.template +++ b/services/templates/nginx/conf.d/timeline.conf.template diff --git a/templates/nginx/conf.d/websocket.conf b/services/templates/nginx/conf.d/websocket.conf index 32af4c3..32af4c3 100644 --- a/templates/nginx/conf.d/websocket.conf +++ b/services/templates/nginx/conf.d/websocket.conf diff --git a/templates/v2ray-config.json.template b/services/templates/v2ray-config.json.template index c10eac2..c10eac2 100644 --- a/templates/v2ray-config.json.template +++ b/services/templates/v2ray-config.json.template diff --git a/services/update-blog b/services/update-blog new file mode 100755 index 0000000..d85acc1 --- /dev/null +++ b/services/update-blog @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +exec docker compose exec -it blog /scripts/update.bash diff --git a/tools/cru-py/.python-version b/tools/cru-py/.python-version deleted file mode 100644 index 2c07333..0000000 --- a/tools/cru-py/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/tools/cru-py/cru/service/_config.py b/tools/cru-py/cru/service/_config.py deleted file mode 100644 index cbb9533..0000000 --- a/tools/cru-py/cru/service/_config.py +++ /dev/null @@ -1,444 +0,0 @@ -from collections.abc import Iterable -from typing import Any, Literal, overload - -from cru import CruException, CruNotFound -from cru.config import Configuration, ConfigItem -from cru.value import ( - INTEGER_VALUE_TYPE, - TEXT_VALUE_TYPE, - CruValueTypeError, - RandomStringValueGenerator, - UuidValueGenerator, -) -from cru.parsing import ParseError, SimpleLineConfigParser - -from ._base import AppFeaturePath, AppCommandFeatureProvider - - -class AppConfigError(CruException): - def __init__( - self, message: str, configuration: Configuration, *args, **kwargs - ) -> None: - super().__init__(message, *args, **kwargs) - self._configuration = configuration - - @property - def configuration(self) -> Configuration: - return self._configuration - - -class AppConfigFileError(AppConfigError): - def __init__( - self, - message: str, - configuration: Configuration, - *args, - **kwargs, - ) -> None: - super().__init__(message, configuration, *args, **kwargs) - - -class AppConfigFileNotFoundError(AppConfigFileError): - def __init__( - self, - message: str, - configuration: Configuration, - file_path: str, - *args, - **kwargs, - ) -> None: - super().__init__(message, configuration, *args, **kwargs) - self._file_path = file_path - - @property - def file_path(self) -> str: - return self._file_path - - -class AppConfigFileParseError(AppConfigFileError): - def __init__( - self, - message: str, - configuration: Configuration, - file_content: str, - *args, - **kwargs, - ) -> None: - super().__init__(message, configuration, *args, **kwargs) - self._file_content = file_content - self.__cause__: ParseError - - @property - def file_content(self) -> str: - return self._file_content - - def get_user_message(self) -> str: - return f"Error while parsing config file at line {self.__cause__.line_number}." - - -class AppConfigFileEntryError(AppConfigFileError): - def __init__( - self, - message: str, - configuration: Configuration, - entries: Iterable[SimpleLineConfigParser.Entry], - *args, - **kwargs, - ) -> None: - super().__init__(message, configuration, *args, **kwargs) - self._entries = list(entries) - - @property - def error_entries(self) -> list[SimpleLineConfigParser.Entry]: - return self._entries - - @staticmethod - def entries_to_friendly_message( - entries: Iterable[SimpleLineConfigParser.Entry], - ) -> str: - return "\n".join( - f"line {entry.line_number}: {entry.key}={entry.value}" for entry in entries - ) - - @property - def friendly_message_head(self) -> str: - return "Error entries found in config file" - - def get_user_message(self) -> str: - return ( - f"{self.friendly_message_head}:\n" - f"{self.entries_to_friendly_message(self.error_entries)}" - ) - - -class AppConfigDuplicateEntryError(AppConfigFileEntryError): - @property - def friendly_message_head(self) -> str: - return "Duplicate entries found in config file" - - -class AppConfigEntryValueFormatError(AppConfigFileEntryError): - @property - def friendly_message_head(self) -> str: - return "Invalid value format for entries" - - -class AppConfigItemNotSetError(AppConfigError): - def __init__( - self, - message: str, - configuration: Configuration, - items: list[ConfigItem], - *args, - **kwargs, - ) -> None: - super().__init__(message, configuration, *args, **kwargs) - self._items = items - - -class ConfigManager(AppCommandFeatureProvider): - def __init__(self) -> None: - super().__init__("config-manager") - configuration = Configuration() - self._configuration = configuration - self._loaded: bool = False - self._init_app_defined_items() - - def _init_app_defined_items(self) -> None: - prefix = self.config_name_prefix - - def _add_text(name: str, description: str) -> ConfigItem: - item = ConfigItem(f"{prefix}_{name}", description, TEXT_VALUE_TYPE) - self.configuration.add(item) - return item - - def _add_uuid(name: str, description: str) -> ConfigItem: - item = ConfigItem( - f"{prefix}_{name}", - description, - TEXT_VALUE_TYPE, - default=UuidValueGenerator(), - ) - self.configuration.add(item) - return item - - def _add_random_string( - name: str, description: str, length: int = 32, secure: bool = True - ) -> ConfigItem: - item = ConfigItem( - f"{prefix}_{name}", - description, - TEXT_VALUE_TYPE, - default=RandomStringValueGenerator(length, secure), - ) - self.configuration.add(item) - return item - - def _add_int(name: str, description: str) -> ConfigItem: - item = ConfigItem(f"{prefix}_{name}", description, INTEGER_VALUE_TYPE) - self.configuration.add(item) - return item - - self._domain = _add_text("DOMAIN", "domain name") - self._email = _add_text("EMAIL", "admin email address") - _add_text( - "AUTO_BACKUP_COS_SECRET_ID", - "access key id for Tencent COS, used for auto backup", - ) - _add_text( - "AUTO_BACKUP_COS_SECRET_KEY", - "access key secret for Tencent COS, used for auto backup", - ) - _add_text( - "AUTO_BACKUP_COS_ENDPOINT", - "endpoint (cos.*.myqcloud.com) for Tencent COS, used for auto backup", - ) - _add_text( - "AUTO_BACKUP_COS_BUCKET", - "bucket name for Tencent COS, used for auto backup", - ) - _add_uuid("V2RAY_TOKEN", "v2ray user id") - _add_uuid("V2RAY_PATH", "v2ray path, which will be prefixed by _") - _add_random_string("2FAUTH_APP_KEY", "2FAuth App Key") - _add_text("2FAUTH_MAIL_USERNAME", "2FAuth SMTP user") - _add_text("2FAUTH_MAIL_PASSWORD", "2FAuth SMTP password") - _add_text("GIT_SERVER_USERNAME", "Git server username") - _add_text("GIT_SERVER_PASSWORD", "Git server password") - - def setup(self) -> None: - self._config_file_path = self.app.data_dir.add_subpath( - "config", False, description="Configuration file path." - ) - - @property - def config_name_prefix(self) -> str: - return self.app.app_id.upper() - - @property - def configuration(self) -> Configuration: - return self._configuration - - @property - def config_file_path(self) -> AppFeaturePath: - return self._config_file_path - - @property - def all_set(self) -> bool: - return self.configuration.all_set - - def get_item(self, name: str) -> ConfigItem[Any]: - if not name.startswith(self.config_name_prefix + "_"): - name = f"{self.config_name_prefix}_{name}" - - item = self.configuration.get_or(name, None) - if item is None: - raise AppConfigError(f"Config item '{name}' not found.", self.configuration) - return item - - @overload - def get_item_value_str(self, name: str) -> str: ... - - @overload - def get_item_value_str(self, name: str, ensure_set: Literal[True]) -> str: ... - - @overload - def get_item_value_str(self, name: str, ensure_set: bool = True) -> str | None: ... - - def get_item_value_str(self, name: str, ensure_set: bool = True) -> str | None: - self.load_config_file() - item = self.get_item(name) - if not item.is_set: - if ensure_set: - raise AppConfigItemNotSetError( - f"Config item '{name}' is not set.", self.configuration, [item] - ) - return None - return item.value_str - - def get_str_dict(self, ensure_all_set: bool = True) -> dict[str, str]: - self.load_config_file() - if ensure_all_set and not self.configuration.all_set: - raise AppConfigItemNotSetError( - "Some config items are not set.", - self.configuration, - self.configuration.get_unset_items(), - ) - return self.configuration.to_str_dict() - - @property - def domain_item_name(self) -> str: - return self._domain.name - - def get_domain_value_str(self) -> str: - return self.get_item_value_str(self._domain.name) - - def get_email_value_str_optional(self) -> str | None: - return self.get_item_value_str(self._email.name, ensure_set=False) - - def _set_with_default(self) -> None: - if not self.configuration.all_not_set: - raise AppConfigError( - "Config is not clean. " - "Some config items are already set. " - "Can't set again with default value.", - self.configuration, - ) - for item in self.configuration: - if item.can_generate_default: - item.set_value(item.generate_default_value()) - - def _to_config_file_content(self) -> str: - content = "".join( - [ - f"{item.name}={item.value_str if item.is_set else ''}\n" - for item in self.configuration - ] - ) - return content - - def _create_init_config_file(self) -> None: - if self.config_file_path.check_self(): - raise AppConfigError( - "Config file already exists.", - self.configuration, - user_message=f"The config file at " - f"{self.config_file_path.full_path_str} already exists.", - ) - self._set_with_default() - self.config_file_path.ensure() - with open( - self.config_file_path.full_path, "w", encoding="utf-8", newline="\n" - ) as file: - file.write(self._to_config_file_content()) - - def _parse_config_file(self) -> SimpleLineConfigParser.Result: - if not self.config_file_path.check_self(): - raise AppConfigFileNotFoundError( - "Config file not found.", - self.configuration, - self.config_file_path.full_path_str, - user_message=f"The config file at " - f"{self.config_file_path.full_path_str} does not exist. " - f"You can create an initial one with 'init' command.", - ) - - text = self.config_file_path.full_path.read_text() - try: - parser = SimpleLineConfigParser() - return parser.parse(text) - except ParseError as e: - raise AppConfigFileParseError( - "Failed to parse config file.", self.configuration, text - ) from e - - def _parse_and_print_config_file(self) -> None: - parse_result = self._parse_config_file() - for entry in parse_result: - print(f"{entry.key}={entry.value}") - - def _check_duplicate( - self, - parse_result: dict[str, list[SimpleLineConfigParser.Entry]], - ) -> dict[str, SimpleLineConfigParser.Entry]: - entry_dict: dict[str, SimpleLineConfigParser.Entry] = {} - duplicate_entries: list[SimpleLineConfigParser.Entry] = [] - for key, entries in parse_result.items(): - entry_dict[key] = entries[0] - if len(entries) > 1: - duplicate_entries.extend(entries) - if len(duplicate_entries) > 0: - raise AppConfigDuplicateEntryError( - "Duplicate entries found.", self.configuration, duplicate_entries - ) - - return entry_dict - - def _check_type( - self, entry_dict: dict[str, SimpleLineConfigParser.Entry] - ) -> dict[str, Any]: - value_dict: dict[str, Any] = {} - error_entries: list[SimpleLineConfigParser.Entry] = [] - errors: list[CruValueTypeError] = [] - for key, entry in entry_dict.items(): - try: - if entry.value == "": - value_dict[key] = None - else: - value = entry.value - config_item = self.configuration.get_or(key) - if config_item is not CruNotFound.VALUE: - value = config_item.value_type.convert_str_to_value(value) - value_dict[key] = value - except CruValueTypeError as e: - error_entries.append(entry) - errors.append(e) - if len(error_entries) > 0: - raise AppConfigEntryValueFormatError( - "Entry value format is not correct.", - self.configuration, - error_entries, - ) from ExceptionGroup("Multiple format errors occurred.", errors) - return value_dict - - def _read_config_file(self) -> dict[str, Any]: - parsed = self._parse_config_file() - entry_groups = parsed.cru_iter().group_by(lambda e: e.key) - entry_dict = self._check_duplicate(entry_groups) - value_dict = self._check_type(entry_dict) - return value_dict - - def _real_load_config_file(self) -> None: - self.configuration.reset_all() - value_dict = self._read_config_file() - for key, value in value_dict.items(): - if value is None: - continue - self.configuration.set_config_item(key, value) - - def load_config_file(self, force=False) -> None: - if force or not self._loaded: - self._real_load_config_file() - self._loaded = True - - def _print_app_config_info(self): - for item in self.configuration: - print(item.description_str) - - def get_command_info(self): - return "config", "Manage configuration." - - def setup_arg_parser(self, arg_parser) -> None: - subparsers = arg_parser.add_subparsers( - dest="config_command", required=True, metavar="CONFIG_COMMAND" - ) - _init_parser = subparsers.add_parser( - "init", help="create an initial config file" - ) - _print_app_parser = subparsers.add_parser( - "print-app", - help="print information of the config items defined by app", - ) - _print_parser = subparsers.add_parser("print", help="print current config") - _check_config_parser = subparsers.add_parser( - "check", - help="check the validity of the config file", - ) - _check_config_parser.add_argument( - "-f", - "--format-only", - action="store_true", - help="only check content format, not app config item requirements.", - ) - - def run_command(self, args) -> None: - if args.config_command == "init": - self._create_init_config_file() - elif args.config_command == "print-app": - self._print_app_config_info() - elif args.config_command == "print": - self._parse_and_print_config_file() - elif args.config_command == "check": - if args.format_only: - self._parse_config_file() - else: - self._read_config_file() diff --git a/tools/cru-py/cru/service/_template.py b/tools/cru-py/cru/service/_template.py deleted file mode 100644 index 1381700..0000000 --- a/tools/cru-py/cru/service/_template.py +++ /dev/null @@ -1,90 +0,0 @@ -from argparse import Namespace -from pathlib import Path -import shutil - -from cru.template import TemplateTree, CruStrWrapperTemplate - -from ._base import AppCommandFeatureProvider, AppFeaturePath -from ._config import ConfigManager - - -class TemplateManager(AppCommandFeatureProvider): - def __init__(self, prefix: str | None = None): - super().__init__("template-manager") - self._prefix = prefix or self.app.app_id.upper() - - def setup(self) -> None: - self._templates_dir = self.app.add_path("templates", True) - self._generated_dir = self.app.add_path("generated", True) - self._template_tree: TemplateTree[CruStrWrapperTemplate] | None = None - - @property - def prefix(self) -> str: - return self._prefix - - @property - def templates_dir(self) -> AppFeaturePath: - return self._templates_dir - - @property - def generated_dir(self) -> AppFeaturePath: - return self._generated_dir - - @property - def template_tree(self) -> TemplateTree[CruStrWrapperTemplate]: - if self._template_tree is None: - return self.reload() - return self._template_tree - - def reload(self) -> TemplateTree: - self._template_tree = TemplateTree( - lambda text: CruStrWrapperTemplate(text), self.templates_dir.full_path_str - ) - return self._template_tree - - def _print_file_lists(self) -> None: - for path, template in self.template_tree.templates: - print(f"[{template.variable_count}]", path.as_posix()) - - def generate(self) -> list[tuple[Path, str]]: - config_manager = self.app.get_feature(ConfigManager) - return self.template_tree.generate(config_manager.get_str_dict()) - - def _generate_files(self, dry_run: bool) -> None: - config_manager = self.app.get_feature(ConfigManager) - if not dry_run and self.generated_dir.full_path.exists(): - shutil.rmtree(self.generated_dir.full_path) - self.template_tree.generate_to( - self.generated_dir.full_path_str, config_manager.get_str_dict(), dry_run - ) - - def get_command_info(self): - return ("template", "Manage templates.") - - def setup_arg_parser(self, arg_parser): - subparsers = arg_parser.add_subparsers( - dest="template_command", required=True, metavar="TEMPLATE_COMMAND" - ) - _list_parser = subparsers.add_parser("list", help="list templates") - _variables_parser = subparsers.add_parser( - "variables", help="list variables used in all templates" - ) - generate_parser = subparsers.add_parser("generate", help="generate templates") - generate_parser.add_argument( - "--no-dry-run", action="store_true", help="generate and write target files" - ) - - def run_command(self, args: Namespace) -> None: - if args.template_command == "list": - self._print_file_lists() - elif args.template_command == "variables": - for var in self.template_tree.variables: - print(var) - elif args.template_command == "generate": - dry_run = not args.no_dry_run - self._generate_files(dry_run) - if dry_run: - print("Dry run successfully.") - print( - f"Will delete dir {self.generated_dir.full_path_str} if it exists." - ) diff --git a/tools/cru-py/www-dev b/tools/cru-py/www-dev deleted file mode 100644 index f56d679..0000000 --- a/tools/cru-py/www-dev +++ /dev/null @@ -1,8 +0,0 @@ -#! /usr/bin/env sh - -set -e - -cd "$(dirname "$0")/../.." - -exec tmux new-session 'cd docker/crupest-nginx/sites/www && pnpm start' \; \ - split-window -h 'cd docker/crupest-api/CrupestApi/CrupestApi && dotnet run --launch-profile dev' diff --git a/tools/manage b/tools/manage deleted file mode 100755 index dc7f64b..0000000 --- a/tools/manage +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -e - -python3.11 --version > /dev/null 2>&1 || ( - echo Error: failed to run Python with python3.11 --version. - exit 1 -) - -script_dir=$(dirname "$0") -project_dir=$(realpath "$script_dir/..") - -cd "$project_dir" - -export PYTHONPATH="$project_dir/tools/cru-py:$PYTHONPATH" -python3.11 -m cru.service --project-dir "$project_dir" "$@" diff --git a/tools/manage.cmd b/tools/manage.cmd deleted file mode 100644 index fce913d..0000000 --- a/tools/manage.cmd +++ /dev/null @@ -1,15 +0,0 @@ -@echo off - -set PYTHON=py -3 -%PYTHON% --version >NUL 2>&1 || ( - echo Error: failed to run Python with py -3 --version. - exit 1 -) - -set TOOLS_DIR=%~dp0 -set PROJECT_DIR=%TOOLS_DIR%.. - -cd /d "%PROJECT_DIR%" - -set PYTHONPATH=%PROJECT_DIR%\tools\cru-py;%PYTHONPATH% -%PYTHON% -m cru.service --project-dir "%PROJECT_DIR%" %* diff --git a/tools/update-blog b/tools/update-blog deleted file mode 100755 index 5314f47..0000000 --- a/tools/update-blog +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -exec docker exec -it blog /scripts/update.bash |