diff --git a/machines/newton/secrets.yaml b/machines/newton/secrets.yaml index 13d8dec..5e95808 100644 --- a/machines/newton/secrets.yaml +++ b/machines/newton/secrets.yaml @@ -13,6 +13,8 @@ freshrss: password: ENC[AES256_GCM,data:dUOKeRxovwIHIchkwMFxsQYEKrU2muY=,iv:OA1zbIiV3NBWIoJLpxpLBEjR/I6m5vzVKvzMEZYYE7Q=,tag:r4PbEbEkSH3bsJMamDuuFw==,type:str] nextcloud: password: ENC[AES256_GCM,data:uE507Ij34zJVYnd2YkNCGj8hpFpEM5w=,iv:x8BNCUaAas0poQ/Lo0izZApF6l52xal8DDrClIzWjvk=,tag:sA08dmcVQbKswX9hF/txag==,type:str] +photoprism: + password: ENC[AES256_GCM,data:FMf/JBsmn4G/EpZQGyEiYivUqw+13Lk=,iv:VEvbbei0Cc8j0J9OH/9cLXNIxvlLF7JsbLRzNVHEAug=,tag:MNHr6TpBsufXc5uGhCIZiw==,type:str] sso: auth-key: ENC[AES256_GCM,data:jFDeymziDiJMnoIGjYPMmnxTzKer1bFffGDaoHnbKlpMPslP/Bmtsc5kio2tbDBlxG0TCdf+ePirPPw2,iv:8wGHEp1gB/qgkSvqkqjb9zBnqkkl1+Ezm9tCFS8tL3w=,tag:tHIT9Iw29TUXJm2e7z3Z/A==,type:str] felix: @@ -44,8 +46,8 @@ sops: NmNwT3N5UEVabFdLTDhseFRjeVZaWFkKL3HGFqfttU1tXY4OhnIr1ABFsHB0R0CX s6wxb0ilut32ijjtnGXMIIa9y6XsMTpYskTb9FdRP9VnQQGVrMfdew== -----END AGE ENCRYPTED FILE----- - lastmodified: "2022-11-13T15:50:14Z" - mac: ENC[AES256_GCM,data:RmNsaye+hanRtzO1BNj6Q/LKS4ACRufzs7TGGcQHfVbi8QyrBqltGoox9ukgaN5PqBNR+uz3+Grpzkjj33xtdJuSRoHNk7aa/q2FHFHmJs+qIggf3HRzgfmBPkP0K9kJdFeOYvy0XoZWMdmaZ9H3fC8kqbEkQPMTrwnKEiDOx6M=,iv:ntjiRk8UUbsnPaKW1AxEoa8RRejA9LCKYNGD6s8dKwI=,tag:hKi3HZoMuOwtAcd7oyUZgw==,type:str] + lastmodified: "2022-12-22T22:12:03Z" + mac: ENC[AES256_GCM,data:Io0GbyRJh7w96fS8H0dWSwdmZuashwbBo5sWEjy7tb107wnNX1gO3fmewhd6D82ERj+qwRQgdxt/8q3wut08rzENepMszx1X45mpMHna/rGeNitBxJ6CAPrf/Bd7PVDabxW8hmSSTMQY94HE6SqLm2M/CRdK6o9vCUG8gRMCSFU=,iv:xQxMZFac5/hNXlHavJZ1srLv7oM5eF2U+M/jbOtY1zU=,tag:MQHXF1Tf/36cPKMvRKtXTQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.7.3 diff --git a/machines/newton/services.nix b/machines/newton/services.nix index 64640fe..1b01fa9 100644 --- a/machines/newton/services.nix +++ b/machines/newton/services.nix @@ -9,8 +9,10 @@ in sops.secrets."sso/felix/password-hash" = { }; sops.secrets."sso/felix/totp-secret" = { }; sops.secrets."paperless/password" = { }; - sops.secrets."nextcloud/password" = { }; - sops.secrets."nextcloud/password".owner = config.users.users.nextcloud.name; + sops.secrets."nextcloud/password" = { + owner = config.users.users.nextcloud.name; + }; + sops.secrets."photoprism/password" = { }; # List services that you want to enable: my.services = { @@ -62,6 +64,16 @@ in passworts = { enable = true; }; + # self-hosted photo gallery + photoprism = { + enable = true; + passwordFile = secrets."photoprism/password".path; + originalsPath = "/srv/data/photos"; + extraConfig = { + PHOTOPRISM_ADMIN_USER = "felix"; + PHOTOPRISM_READONLY = "true"; + }; + }; ssh-server = { enable = true; }; diff --git a/modules/services/default.nix b/modules/services/default.nix index e4d050d..9b4d766 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -16,6 +16,7 @@ ./octoprint ./paperless ./passworts + ./photoprism ./rss-bridge ./ssh-server ./tandoor-recipes diff --git a/modules/services/photoprism/default.nix b/modules/services/photoprism/default.nix new file mode 100644 index 0000000..9f8d826 --- /dev/null +++ b/modules/services/photoprism/default.nix @@ -0,0 +1,176 @@ +# self-hosted photo gallery +{ config, pkgs, lib, ... }: +let + cfg = config.my.services.photoprism; + domain = config.networking.domain; + + env = { + PHOTOPRISM_ORIGINALS_PATH = cfg.originalsPath; + PHOTOPRISM_STORAGE_PATH = cfg.storagePath; + PHOTOPRISM_IMPORT_PATH = cfg.importPath; + PHOTOPRISM_HTTP_HOST = cfg.address; + PHOTOPRISM_HTTP_PORT = toString cfg.port; + } // ( + lib.mapAttrs (_: toString) cfg.extraConfig + ); + + manage = + let + setupEnv = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "export ${name}=\"${val}\"") env); + in + pkgs.writeShellScript "manage" '' + ${setupEnv} + exec ${cfg.package}/bin/photoprism "$@" + ''; +in +{ + meta.maintainers = with lib.maintainers; [ stunkymonkey ]; + + options.my.services.photoprism = with lib; { + + enable = mkEnableOption (lib.mdDoc "Photoprism web server"); + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc "Admin password file."; + }; + + address = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Web interface address."; + }; + + port = mkOption { + type = types.port; + default = 2342; + description = lib.mdDoc "Web interface port."; + }; + + originalsPath = mkOption { + type = types.path; + default = null; + example = "/data/photos"; + description = lib.mdDoc "storage path of your original media files (photos and videos)."; + }; + + importPath = mkOption { + type = types.str; + default = "import"; + description = lib.mdDoc "relative or absolute to the `originalsPath` from where the files should be imported."; + }; + + storagePath = mkOption { + type = types.path; + default = "/var/lib/photoprism"; + description = lib.mdDoc "location for sidecar, cache, and database files."; + }; + + package = mkOption { + type = types.package; + default = pkgs.photoprism; + defaultText = literalExpression "pkgs.photoprism"; + description = lib.mdDoc "The Photoprism package to use."; + }; + + extraConfig = mkOption { + type = types.attrs; + default = { }; + description = lib.mdDoc '' + Extra photoprism config options. See [the getting-stated guide](https://docs.photoprism.app/getting-started/config-options/) for available options. + ''; + example = { + PHOTOPRISM_DEFAULT_LOCALE = "de"; + PHOTOPRISM_ADMIN_USER = "root"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.photoprism = { + description = "Photoprism server"; + + serviceConfig = { + Restart = "on-failure"; + User = "photoprism"; + Group = "photoprism"; + DynamicUser = true; + StateDirectory = "photoprism"; + WorkingDirectory = "/var/lib/photoprism"; + RuntimeDirectory = "photoprism"; + + LoadCredential = lib.optionalString (cfg.passwordFile != null) + "PHOTOPRISM_ADMIN_PASSWORD:${cfg.passwordFile}"; + + BindReadOnlyPaths = [ + "${config.environment.etc."ssl/certs/ca-certificates.crt".source}:/etc/ssl/certs/ca-certificates.crt" + builtins.storeDir + "-/etc/resolv.conf" + "-/etc/nsswitch.conf" + "-/etc/hosts" + "-/etc/localtime" + ]; + CapabilityBoundingSet = ""; + LockPersonality = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" "@resources" "@setuid" "@keyring" ]; + UMask = "0066"; + } // lib.optionalAttrs (cfg.port < 1024) { + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + }; + + wantedBy = [ "multi-user.target" ]; + environment = env; + + preStart = '' + ln -sf ${manage} photoprism-manage + ''; + + # wait for easier password configuration: https://github.com/photoprism/photoprism/pull/2302 + script = '' + ${lib.optionalString (cfg.passwordFile != null) '' + export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD") + ''} + ${cfg.package}/bin/photoprism migrations run -f + exec ${cfg.package}/bin/photoprism start + ''; + }; + + # Proxy to Photoprism + my.services.nginx.virtualHosts = [ + { + subdomain = "photos"; + inherit (cfg) port; + extraConfig = { + locations."/" = { + proxyWebsockets = true; + }; + }; + } + ]; + + webapps.apps.photoprism = { + dashboard = { + name = "Photos"; + category = "media"; + icon = "image"; + link = "https://photos.${domain}/library/login"; + method = "get"; + }; + }; + }; +}