diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index e89b81ec0e96..3f7fe614a7b1 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -11023,6 +11023,12 @@ githubId = 591860; name = "Lionello Lunesu"; }; + litchipi = { + email = "litchi.pi@proton.me"; + github = "litchipi"; + githubId = 61109829; + name = "Litchi Pi"; + }; livnev = { email = "lev@liv.nev.org.uk"; github = "livnev"; diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index b5973c19a2c4..91540e9a0ea5 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -97,6 +97,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [systemd-lock-handler](https://git.sr.ht/~whynothugo/systemd-lock-handler/), a bridge between logind D-Bus events and systemd targets. Available as [services.systemd-lock-handler.enable](#opt-services.systemd-lock-handler.enable). +- [Mealie](https://nightly.mealie.io/), a self-hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in NuxtJS for a pleasant user experience for the whole family. Available as [services.mealie](#opt-services.mealie.enable) + ## Backward Incompatibilities {#sec-release-24.05-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 8683af5ed439..1c0d256ea1b2 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1323,6 +1323,7 @@ ./services/web-apps/mastodon.nix ./services/web-apps/matomo.nix ./services/web-apps/mattermost.nix + ./services/web-apps/mealie.nix ./services/web-apps/mediawiki.nix ./services/web-apps/meme-bingo-web.nix ./services/web-apps/microbin.nix diff --git a/nixos/modules/services/web-apps/mealie.nix b/nixos/modules/services/web-apps/mealie.nix new file mode 100644 index 000000000000..8bb7542c6b56 --- /dev/null +++ b/nixos/modules/services/web-apps/mealie.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ...}: +let + cfg = config.services.mealie; + pkg = cfg.package; +in +{ + options.services.mealie = { + enable = lib.mkEnableOption "Mealie, a recipe manager and meal planner"; + + package = lib.mkPackageOption pkgs "mealie" { }; + + listenAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "Address on which the service should listen."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 9000; + description = "Port on which to serve the Mealie service."; + }; + + settings = lib.mkOption { + type = with lib.types; attrsOf anything; + default = {}; + description = lib.mdDoc '' + Configuration of the Mealie service. + + See [the mealie documentation](https://nightly.mealie.io/documentation/getting-started/installation/backend-config/) for available options and default values. + + In addition to the official documentation, you can set {env}`MEALIE_LOG_FILE`. + ''; + example = { + ALLOW_SIGNUP = "false"; + }; + }; + + credentialsFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + example = "/run/secrets/mealie-credentials.env"; + description = '' + File containing credentials used in mealie such as {env}`POSTGRES_PASSWORD` + or sensitive LDAP options. + + Expects the format of an `EnvironmentFile=`, as described by {manpage}`systemd.exec(5)`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.mealie = { + description = "Mealie, a self hosted recipe manager and meal planner"; + + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + PRODUCTION = "true"; + ALEMBIC_CONFIG_FILE="${pkg}/config/alembic.ini"; + API_PORT = toString cfg.port; + DATA_DIR = "/var/lib/mealie"; + CRF_MODEL_PATH = "/var/lib/mealie/model.crfmodel"; + } // (builtins.mapAttrs (_: val: toString val) cfg.settings); + + serviceConfig = { + DynamicUser = true; + User = "mealie"; + ExecStartPre = "${pkg}/libexec/init_db"; + ExecStart = "${lib.getExe pkg} -b ${cfg.listenAddress}:${builtins.toString cfg.port}"; + EnvironmentFile = lib.mkIf (cfg.credentialsFile != null) cfg.credentialsFile; + StateDirectory = "mealie"; + StandardOutput="journal"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 1144a5611dcf..47e1ad2ca0bb 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -516,6 +516,7 @@ in { matrix-synapse = handleTest ./matrix/synapse.nix {}; matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {}; mattermost = handleTest ./mattermost.nix {}; + mealie = handleTest ./mealie.nix {}; mediamtx = handleTest ./mediamtx.nix {}; mediatomb = handleTest ./mediatomb.nix {}; mediawiki = handleTest ./mediawiki.nix {}; diff --git a/nixos/tests/mealie.nix b/nixos/tests/mealie.nix new file mode 100644 index 000000000000..88f749c71294 --- /dev/null +++ b/nixos/tests/mealie.nix @@ -0,0 +1,24 @@ +import ./make-test-python.nix ({ pkgs, ...} : + +{ + name = "mealie"; + meta = with pkgs.lib.maintainers; { + maintainers = [ litchipi ]; + }; + + nodes = { + server = { + services.mealie = { + enable = true; + port = 9001; + }; + }; + }; + + testScript = '' + start_all() + server.wait_for_unit("mealie.service") + server.wait_for_open_port(9001) + server.succeed("curl --fail http://localhost:9001") + ''; +}) diff --git a/pkgs/by-name/me/mealie/mealie-frontend.nix b/pkgs/by-name/me/mealie/mealie-frontend.nix new file mode 100644 index 000000000000..5a7b677802ee --- /dev/null +++ b/pkgs/by-name/me/mealie/mealie-frontend.nix @@ -0,0 +1,51 @@ +src: version: +{ lib, fetchYarnDeps, nodejs_18, prefetch-yarn-deps, stdenv }: stdenv.mkDerivation { + name = "mealie-frontend"; + inherit version; + src = "${src}/frontend"; + + yarnOfflineCache = fetchYarnDeps { + yarnLock = "${src}/frontend/yarn.lock"; + hash = "sha256-zQUD/PQWzp2Q6fiVmLicvSusXffu6s9q3x/aAUnCN38="; + }; + + nativeBuildInputs = [ + prefetch-yarn-deps + nodejs_18 + nodejs_18.pkgs.yarn + ]; + + configurePhase = '' + runHook preConfigure + + export HOME=$(mktemp -d) + yarn config --offline set yarn-offline-mirror "$yarnOfflineCache" + fixup-yarn-lock yarn.lock + yarn install --frozen-lockfile --offline --no-progress --non-interactive + patchShebangs node_modules/ + + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + + export NUXT_TELEMETRY_DISABLED=1 + yarn --offline build + yarn --offline generate + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mv dist $out + runHook postInstall + ''; + + meta = with lib; { + description = "Frontend for Mealie"; + license = licenses.agpl3Only; + maintainers = with maintainers; [ litchipi ]; + }; +} diff --git a/pkgs/by-name/me/mealie/mealie-logs-to-stdout.patch b/pkgs/by-name/me/mealie/mealie-logs-to-stdout.patch new file mode 100644 index 000000000000..521c5cebcf74 --- /dev/null +++ b/pkgs/by-name/me/mealie/mealie-logs-to-stdout.patch @@ -0,0 +1,103 @@ +diff --git a/mealie/core/root_logger.py b/mealie/core/root_logger.py +index 29db504f..3da5ef84 100644 +--- a/mealie/core/root_logger.py ++++ b/mealie/core/root_logger.py +@@ -9,7 +9,6 @@ DATA_DIR = determine_data_dir() + + from .config import get_app_settings # noqa E402 + +-LOGGER_FILE = DATA_DIR.joinpath("mealie.log") + DATE_FORMAT = "%d-%b-%y %H:%M:%S" + LOGGER_FORMAT = "%(levelname)s: %(asctime)s \t%(message)s" + +@@ -40,19 +39,17 @@ def get_logger_config(): + level=log_level, + ) + +- output_file_handler = logging.FileHandler(LOGGER_FILE) + handler_format = logging.Formatter(LOGGER_FORMAT, datefmt=DATE_FORMAT) +- output_file_handler.setFormatter(handler_format) + + # Stdout + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setFormatter(handler_format) + + return LoggerConfig( +- handlers=[output_file_handler, stdout_handler], ++ handlers=[stdout_handler], + format="%(levelname)s: %(asctime)s \t%(message)s", + date_format="%d-%b-%y %H:%M:%S", +- logger_file=LOGGER_FILE, ++ logger_file=None, + level=log_level, + ) + +diff --git a/mealie/routes/admin/admin_log.py b/mealie/routes/admin/admin_log.py +index ac12c12e..0de98b99 100644 +--- a/mealie/routes/admin/admin_log.py ++++ b/mealie/routes/admin/admin_log.py +@@ -1,6 +1,5 @@ + from fastapi import APIRouter + +-from mealie.core.root_logger import LOGGER_FILE + from mealie.core.security import create_file_token + + router = APIRouter(prefix="/logs") +@@ -9,15 +8,13 @@ router = APIRouter(prefix="/logs") + @router.get("/{num}") + async def get_log(num: int): + """Doc Str""" +- with open(LOGGER_FILE, "rb") as f: +- log_text = tail(f, num) +- return log_text ++ return "" + + + @router.get("") + async def get_log_file(): + """Returns a token to download a file""" +- return {"fileToken": create_file_token(LOGGER_FILE)} ++ return {"fileToken": create_file_token("nofile")} + + + def tail(f, lines=20): +diff --git a/mealie/routes/admin/admin_maintenance.py b/mealie/routes/admin/admin_maintenance.py +index 23ef8369..322b580f 100644 +--- a/mealie/routes/admin/admin_maintenance.py ++++ b/mealie/routes/admin/admin_maintenance.py +@@ -6,7 +6,6 @@ from pathlib import Path + + from fastapi import APIRouter, HTTPException + +-from mealie.core.root_logger import LOGGER_FILE + from mealie.pkgs.stats import fs_stats + from mealie.routes._base import BaseAdminController, controller + from mealie.schema.admin import MaintenanceSummary +@@ -73,8 +72,6 @@ class AdminMaintenanceController(BaseAdminController): + Get the maintenance summary + """ + log_file_size = 0 +- with contextlib.suppress(FileNotFoundError): +- log_file_size = os.path.getsize(LOGGER_FILE) + + return MaintenanceSummary( + data_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.DATA_DIR)), +@@ -85,7 +82,7 @@ class AdminMaintenanceController(BaseAdminController): + + @router.get("/logs", response_model=MaintenanceLogs) + def get_logs(self, lines: int = 200): +- return MaintenanceLogs(logs=tail_log(LOGGER_FILE, lines)) ++ return MaintenanceLogs(logs="") + + @router.get("/storage", response_model=MaintenanceStorageDetails) + def get_storage_details(self): +@@ -137,9 +134,6 @@ class AdminMaintenanceController(BaseAdminController): + Purges the logs + """ + try: +- with contextlib.suppress(FileNotFoundError): +- os.remove(LOGGER_FILE) +- LOGGER_FILE.touch() + return SuccessResponse.respond("Logs cleaned") + except Exception as e: + raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean logs")) from e diff --git a/pkgs/by-name/me/mealie/package.nix b/pkgs/by-name/me/mealie/package.nix new file mode 100644 index 000000000000..2d1512f67ebc --- /dev/null +++ b/pkgs/by-name/me/mealie/package.nix @@ -0,0 +1,166 @@ +{ lib +, callPackage +, fetchFromGitHub +, fetchpatch +, makeWrapper +, nixosTests +, python3Packages +, stdenv +, writeShellScript +}: + +let + version = "1.2.0"; + src = fetchFromGitHub { + owner = "mealie-recipes"; + repo = "mealie"; + rev = "v${version}"; + sha256 = "sha256-Kc49XDWcZLeJaYgiAO2/mHeVSOLMeiPr3U32e0IYfdU="; + }; + + frontend = callPackage (import ./mealie-frontend.nix src version) { }; + + pythonpkgs = python3Packages.override { + overrides = self: super: { + pydantic = python3Packages.pydantic_1; + }; + }; + python = pythonpkgs.python; + + crfpp = stdenv.mkDerivation { + pname = "mealie-crfpp"; + version = "unstable-2024-02-12"; + src = fetchFromGitHub { + owner = "mealie-recipes"; + repo = "crfpp"; + rev = "c56dd9f29469c8a9f34456b8c0d6ae0476110516"; + hash = "sha256-XNps3ZApU8m07bfPEnvip1w+3hLajdn9+L5+IpEaP0c="; + }; + }; + + mealie_patch = { name, commit, hash }: fetchpatch { + inherit name hash; + url = "https://github.com/mealie-recipes/mealie/commit/${commit}.patch"; + }; + +in pythonpkgs.buildPythonPackage rec { + pname = "mealie"; + inherit version src; + pyproject = true; + + patches = [ + # See https://github.com/mealie-recipes/mealie/pull/3102 + # Replace hardcoded paths in code with environment variables (meant for inside Docker only) + # So we can configure easily where the data is stored on the server + (mealie_patch { + name = "model-path.patch"; + commit = "e445705c5d26b895d806b96b2f330d4e9aac3723"; + hash = "sha256-cf0MwvT81lNBTjvag8UUEbXkBu8Jyi/LFwUcs4lBVcY="; + }) + (mealie_patch { + name = "alembic-cfg-path.patch"; + commit = "06c528bfac0708af66aa0629f2e2232ddf07768f"; + hash = "sha256-IOgdZK7dmWeX2ox16J9v+bOS7nHgCMvCJy6RNJLj0p8="; + }) + ./mealie-logs-to-stdout.patch + ]; + + nativeBuildInputs = [ + pythonpkgs.poetry-core + pythonpkgs.pythonRelaxDepsHook + makeWrapper + ]; + + dontWrapPythonPrograms = true; + + doCheck = false; + pythonRelaxDeps = true; + + propagatedBuildInputs = with pythonpkgs; [ + aiofiles + alembic + aniso8601 + appdirs + apprise + bcrypt + extruct + fastapi + gunicorn + html2text + httpx + jinja2 + lxml + orjson + paho-mqtt + passlib + pillow + psycopg2 + pyhumps + pytesseract + python-dotenv + python-jose + python-ldap + python-multipart + python-slugify + pyyaml + rapidfuzz + recipe-scrapers + sqlalchemy + tzdata + uvicorn + ]; + + postPatch = '' + substituteInPlace mealie/__init__.py \ + --replace-fail '__version__ = ' '__version__ = "${version}" #' + ''; + + postInstall = let + start_script = writeShellScript "start-mealie" '' + ${lib.getExe pythonpkgs.gunicorn} "$@" -k uvicorn.workers.UvicornWorker mealie.app:app; + ''; + init_db = writeShellScript "init-mealie-db" '' + ${python.interpreter} $OUT/${python.sitePackages}/mealie/scripts/install_model.py + ${python.interpreter} $OUT/${python.sitePackages}/mealie/db/init_db.py + ''; + in '' + mkdir -p $out/config $out/bin $out/libexec + rm -f $out/bin/* + + substitute ${src}/alembic.ini $out/config/alembic.ini \ + --replace-fail 'script_location = alembic' 'script_location = ${src}/alembic' + + makeWrapper ${start_script} $out/bin/mealie \ + --set PYTHONPATH "$out/${python.sitePackages}:${python.pkgs.makePythonPath propagatedBuildInputs}" \ + --set LD_LIBRARY_PATH "${crfpp}/lib" \ + --set STATIC_FILES "${frontend}" \ + --set PATH "${lib.makeBinPath [ crfpp ]}" + + makeWrapper ${init_db} $out/libexec/init_db \ + --set PYTHONPATH "$out/${python.sitePackages}:${python.pkgs.makePythonPath propagatedBuildInputs}" \ + --set OUT "$out" + ''; + + checkInputs = with python.pkgs; [ + pytestCheckHook + ]; + + passthru.tests = { + inherit (nixosTests) mealie; + }; + + meta = with lib; { + description = "A self hosted recipe manager and meal planner"; + longDescription = '' + Mealie is a self hosted recipe manager and meal planner with a REST API and a reactive frontend + application built in NuxtJS for a pleasant user experience for the whole family. Easily add recipes into your + database by providing the URL and Mealie will automatically import the relevant data or add a family recipe with + the UI editor. + ''; + homepage = "https://mealie.io"; + changelog = "https://github.com/mealie-recipes/mealie/releases/tag/${src.rev}"; + license = licenses.agpl3Only; + maintainers = with maintainers; [ litchipi ]; + mainProgram = "mealie"; + }; +}