Merge pull request #278454 from litchipi/mealie

mealie: init at 1.2.0
This commit is contained in:
Atemu 2024-02-26 21:01:49 +00:00 committed by GitHub
commit f53c775852
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 433 additions and 0 deletions

View file

@ -11023,6 +11023,12 @@
githubId = 591860; githubId = 591860;
name = "Lionello Lunesu"; name = "Lionello Lunesu";
}; };
litchipi = {
email = "litchi.pi@proton.me";
github = "litchipi";
githubId = 61109829;
name = "Litchi Pi";
};
livnev = { livnev = {
email = "lev@liv.nev.org.uk"; email = "lev@liv.nev.org.uk";
github = "livnev"; github = "livnev";

View file

@ -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). - [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} ## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

View file

@ -1323,6 +1323,7 @@
./services/web-apps/mastodon.nix ./services/web-apps/mastodon.nix
./services/web-apps/matomo.nix ./services/web-apps/matomo.nix
./services/web-apps/mattermost.nix ./services/web-apps/mattermost.nix
./services/web-apps/mealie.nix
./services/web-apps/mediawiki.nix ./services/web-apps/mediawiki.nix
./services/web-apps/meme-bingo-web.nix ./services/web-apps/meme-bingo-web.nix
./services/web-apps/microbin.nix ./services/web-apps/microbin.nix

View file

@ -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";
};
};
};
}

View file

@ -516,6 +516,7 @@ in {
matrix-synapse = handleTest ./matrix/synapse.nix {}; matrix-synapse = handleTest ./matrix/synapse.nix {};
matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {}; matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {};
mattermost = handleTest ./mattermost.nix {}; mattermost = handleTest ./mattermost.nix {};
mealie = handleTest ./mealie.nix {};
mediamtx = handleTest ./mediamtx.nix {}; mediamtx = handleTest ./mediamtx.nix {};
mediatomb = handleTest ./mediatomb.nix {}; mediatomb = handleTest ./mediatomb.nix {};
mediawiki = handleTest ./mediawiki.nix {}; mediawiki = handleTest ./mediawiki.nix {};

24
nixos/tests/mealie.nix Normal file
View file

@ -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")
'';
})

View file

@ -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 ];
};
}

View file

@ -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

View file

@ -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";
};
}