Compare commits

..

2 Commits

Author SHA1 Message Date
17013e425e
modules/koillection: init
Some checks failed
CI / checks (nur) (push) Successful in 3m6s
CI / build-and-update (xeals, xeals) (push) Failing after 4m15s
Currently fails to build as the frontend can't be built (due to using
Yarn's v2 lockfile format).
2024-01-12 14:51:49 +11:00
e5934da311
koillection: init at 1.5.2 2024-01-12 14:48:30 +11:00
4 changed files with 413 additions and 0 deletions

View File

@ -4,6 +4,7 @@
amdgpu-pwm = ./services/hardware/amdgpu-pwm.nix; amdgpu-pwm = ./services/hardware/amdgpu-pwm.nix;
betanin = ./services/web-apps/betanin.nix; betanin = ./services/web-apps/betanin.nix;
dunst = ./services/x11/dunst.nix; dunst = ./services/x11/dunst.nix;
koillection = ./services/web-apps/koillection.nix;
porkbun-ddns = ./services/networking/porkbun-ddns.nix; porkbun-ddns = ./services/networking/porkbun-ddns.nix;
radeon-profile-daemon = ./services/hardware/radeon-profile-daemon.nix; radeon-profile-daemon = ./services/hardware/radeon-profile-daemon.nix;
} }

View File

@ -0,0 +1,318 @@
{ config, lib, modulesPath, options, pkgs, ... }:
let
inherit (lib) literalExpression mkEnableOption mkIf mkOption optionalString types;
opt = options.services.koillection;
cfg = config.services.koillection;
db = cfg.database;
koillection = (pkgs.koillection or pkgs.callPackage ../../../pkgs/by-name/ko/koillection/package.nix { }).override {
dataDir = cfg.dataDir;
};
inherit (koillection.passthru) phpPackage;
in
{
options.services.koillection = {
enable = mkEnableOption "Koillection, a collection manager";
user = mkOption {
type = types.str;
default = "koillection";
description = lib.mdDoc "User Koillection runs as.";
};
group = mkOption {
type = types.str;
default = "koillection";
description = lib.mdDoc "Group Koillection runs as.";
};
hostName = lib.mkOption {
type = types.str;
default = config.networking.fqdnOrHostName;
defaultText = "config.networking.fqdnOrHostName";
example = "koillection.example.com";
description = lib.mdDoc "The hostname to serve Koillection on.";
};
dataDir = mkOption {
description = lib.mdDoc "Koillection data directory";
default = "/var/lib/koillection";
type = types.path;
};
database = {
host = mkOption {
type = types.str;
default = if db.createLocally then "/run/postgresql" else null;
defaultText = literalExpression ''
if config.${opt.database.createLocally}
then "/run/postgresql"
else null
'';
example = "192.168.12.85";
description = lib.mdDoc "Database host address or unix socket.";
};
port = mkOption {
type = types.port;
default = 5432;
description = lib.mdDoc "Database host port.";
};
name = mkOption {
type = types.str;
default = "koillection";
description = lib.mdDoc "Database name.";
};
user = mkOption {
type = types.str;
default = cfg.user;
defaultText = literalExpression "user";
description = lib.mdDoc "Database username.";
};
passwordFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/run/keys/koillection-dbpassword";
description = lib.mdDoc ''
A file containing the password corresponding to
{option}`database.user`.
'';
};
createLocally = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc "Create the database and database user locally.";
};
};
poolConfig = mkOption {
type = with types; attrsOf (oneOf [ str int bool ]);
default = {
"pm" = "dynamic";
"pm.max_children" = 32;
"pm.start_servers" = 2;
"pm.min_spare_servers" = 2;
"pm.max_spare_servers" = 4;
"pm.max_requests" = 500;
};
description = lib.mdDoc ''
Options for the bookstack PHP pool. See the documentation on `php-fpm.conf`
for details on configuration directives.
'';
};
nginx = mkOption {
type = types.submodule (
lib.recursiveUpdate
(import "${modulesPath}/services/web-servers/nginx/vhost-options.nix" { inherit config lib; })
{ }
);
default = { };
example = literalExpression ''
{
serverAliases = [
"koillection.''${config.networking.domain}"
];
# To enable encryption and let let's encrypt take care of certificate
forceSSL = true;
enableACME = true;
}
'';
description = lib.mdDoc ''
With this option, you can customize the nginx virtualHost settings.
'';
};
config = mkOption {
type = with types;
attrsOf
(nullOr
(either
(oneOf [ bool int port path str ])
(submodule {
options = {
_secret = mkOption {
type = nullOr str;
description = lib.mdDoc ''
The path to a file containing the value the option should
be set to in the final configuration file.
'';
};
};
})));
default = { };
example = literalExpression ''
'';
};
};
config = mkIf cfg.enable {
services.koillection.config = {
APP_ENV = "prod";
# DB_DRIVER = "pdo_pgsql";
DB_USER = db.user;
DB_PASSWORD._secret = db.passwordFile;
DB_HOST = db.host;
DB_PORT = db.port;
DB_NAME = db.name;
# FIXME
# DB_VERSION = lib.versions.major config.services.postgresql.package.version;
DB_VERSION = "15";
PHP_TZ = config.time.timeZone;
};
services.postgresql = mkIf db.createLocally {
enable = true;
ensureDatabases = [ db.name ];
ensureUsers = [{
name = db.user;
ensureDBOwnership = true;
}];
};
services.phpfpm.pools.koillection = {
inherit (cfg) user group;
inherit phpPackage;
# Copied from docker/php.ini
phpOptions = ''
max_execution_time = 200
apc.enabled = 1
apc.shm_size = 64M
apc.ttl = 7200
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.max_wasted_percentage = 10
opcache.validate_timestamps = 0
opcache.preload = ${koillection}/share/php/koillection/config/preload.php
opcache.preload_user = ${cfg.user}
expose_php = Off
session.cookie_httponly = 1
realpath_cache_size = 4096K
realpath_cache_ttle = 600
'';
settings = {
"listen.mode" = "0660";
"listen.owner" = cfg.user;
"listen.group" = cfg.group;
} // cfg.poolConfig;
};
services.nginx = {
enable = lib.mkDefault true;
virtualHosts."${cfg.hostName}" = lib.mkMerge [
cfg.nginx
{
root = lib.mkForce "${koillection}/share/php/koillection/public";
extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
locations = {
"/" = {
index = "index.php";
extraConfig = ''try_files $uri $uri/ /index.php?query_string;'';
};
"~ \.php$" = {
extraConfig = ''
try_files $uri $uri/ /index.php?$query_string;
include ${config.services.nginx.package}/conf/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_pass unix:${config.services.phpfpm.pools."koillection".socket};
${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
'';
};
"~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
extraConfig = "expires 365d;";
};
};
}
];
};
systemd.services.koillection-setup = {
description = "Preparation tasks for koillection";
before = [ "phpfpm-koillection.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = cfg.user;
WorkingDirectory = koillection;
RuntimeDirectory = "koillection/cache";
RuntimeDirectoryMode = "0700";
};
path = [ pkgs.replace-secret ];
script =
let
isSecret = v: lib.isAttrs v && v ? _secret && lib.isString v._secret;
koillectionEnvVars = lib.generators.toKeyValue {
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
mkValueString = v: with builtins;
if isInt v then toString v
else if lib.isString v then v
else if true == v then "true"
else if false == v then "false"
else if isSecret v then hashString "sha256" v._secret
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
};
};
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
mkSecretReplacement = file: ''
replace-secret ${lib.escapeShellArgs [ ( builtins.hashString "sha256" file ) file "${cfg.dataDir}/.env" ]}
'';
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! builtins.elem v [{ } null])) cfg.config;
koillectionEnv = pkgs.writeText "koillection.env" (koillectionEnvVars filteredConfig);
in
''
# error handling
set -euo pipefail
# set permissions
umask 077
# create .env file
install -T -m 0600 -o ${cfg.user} ${koillectionEnv} "${cfg.dataDir}/.env"
${secretReplacements}
# prepend `base64:` if it does not exist in APP_KEY
if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
fi
# migrate db
${lib.getExe phpPackage} ${koillection}/share/php/koillection/bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
# dump translations
# TODO: Might need pointing somewhere else.
${lib.getExe phpPackage} ${koillection}/share/php/koillection/bin/console app:translations:dump
'';
};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0710 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/public 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/public/uploads 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/var 0700 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/var/cache 0700 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/var/cache/prod 0700 ${cfg.user} ${cfg.group} - -"
"d ${cfg.dataDir}/var/logs 0700 ${cfg.user} ${cfg.group} - -"
];
users = {
users = mkIf (cfg.user == "koillection") {
koillection = {
inherit (cfg) group;
isSystemUser = true;
};
"${config.services.nginx.user}".extraGroups = [ cfg.group ];
};
groups = mkIf (cfg.group == "koillection") {
koillection = { };
};
};
};
}

View File

@ -0,0 +1,25 @@
*** a/src/Kernel.php
--- b/src/Kernel.php
@@ -8,6 +8,21 @@ class Kernel extends BaseKernel
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
+
+ public function getDataDir(): string
+ {
+ return '@dataDir@';
+ }
+
+ public function getCacheDir(): string
+ {
+ return $this->getDataDir() . '/var/cache/' . $this->getEnvironment();
+ }
+
+ public function getLogDir(): string
+ {
+ return $this->getDataDir() . '/var/logs';
+ }
}

View File

@ -0,0 +1,69 @@
{ lib
, dataDir ? "/var/lib/koillection"
# REVIEW: This supposed to be aliased by the caller, which means it shouldn't
# go in by-name, I think.
, php83
, fetchFromGitHub
, mkYarnPackage
, fetchYarnDeps
}:
php83.buildComposerProject (finalAttrs: {
pname = "koillection";
version = "1.5.2";
src = fetchFromGitHub {
owner = "benjaminjonard";
repo = "koillection";
rev = finalAttrs.version;
hash = "sha256-r2rkHhp0F5QfwJuKeu4UdPoluDXxpyhYpie1zUk1h5c=";
};
frontend = mkYarnPackage {
inherit (finalAttrs) pname version;
src = "${finalAttrs.src}/assets";
offlineCache = fetchYarnDeps {
yarnLock = "${finalAttrs.src}/assets/yarn.lock";
hash = "";
};
};
patches = [
./koillection-dirs.patch
];
postPatch = ''
substituteInPlace src/Kernel.php \
--replace "@dataDir@" "${dataDir}"
'';
# Lock file uses exact constraints, which Composer doesn't like.
composerStrictValidation = false;
# Actually installs plugins, i.e., Symfony.
composerNoPlugins = false;
vendorHash = "sha256-LU9ZN4qUNUpSBGH6AChw3qU4RjgsoPJmLL01FS7UKRQ=";
postInstall = ''
local koillection_out=$out/share/php/koillection
rm -R $koillection_out/public/uploads
ln -s ${dataDir}/.env $koillection_out/.env.local
ln -s ${dataDir}/public/uploads $koillection_out/public/uploads
cp -r ${finalAttrs.frontend} assets/
'';
passthru = {
phpPackage = php83;
};
meta = {
description = "Self-hosted service allowing users to manage any kind of collections";
homepage = "https://github.com/benjaminjonard/koillection";
license = lib.licenses.mit;
platforms = lib.platforms.unix;
broken = true; # Blocked on NixOS/nixpkgs#254369
};
})