diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fb9c8455c115..255c20796217 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1724,6 +1724,7 @@ ./services/web-apps/peertube.nix ./services/web-apps/pgpkeyserver-lite.nix ./services/web-apps/photoprism.nix + ./services/web-apps/photoview.nix ./services/web-apps/phylactery.nix ./services/web-apps/pict-rs.nix ./services/web-apps/pihole-web.nix diff --git a/nixos/modules/services/web-apps/photoview.nix b/nixos/modules/services/web-apps/photoview.nix new file mode 100644 index 000000000000..a2a8d62c29e8 --- /dev/null +++ b/nixos/modules/services/web-apps/photoview.nix @@ -0,0 +1,259 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.photoview; + + dbUrl = { + sqlite = "PHOTOVIEW_SQLITE_PATH=${cfg.dataDir}/photoview.db"; + mysql = "PHOTOVIEW_MYSQL_URL=${cfg.database.user}:$(cat $CREDENTIALS_DIRECTORY/db_password)@tcp(${cfg.database.host}:${toString cfg.database.port})/${cfg.database.name}"; + postgres = "PHOTOVIEW_POSTGRES_URL=postgres://${cfg.database.user}:$(cat $CREDENTIALS_DIRECTORY/db_password)@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}"; + }; +in + +{ + options.services.photoview = { + enable = lib.mkEnableOption "Photoview, a photo gallery for self-hosted personal servers"; + + package = lib.mkPackageOption pkgs "photoview" { }; + + user = lib.mkOption { + type = lib.types.str; + default = "photoview"; + description = "User account under which photoview runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "photoview"; + description = "Group under which photoview runs."; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/photoview"; + description = "Directory for photoview state, cache, and database."; + }; + + mediaPath = lib.mkOption { + type = lib.types.path; + description = '' + Path to the directory containing photos to be served. + This directory must be readable by the photoview user. + ''; + example = "/mnt/photos"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Address to listen on."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 4001; + description = "Port to listen on."; + }; + + database = { + type = lib.mkOption { + type = lib.types.enum [ + "sqlite" + "mysql" + "postgres" + ]; + default = "sqlite"; + description = "Database engine to use."; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "Database host address."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 5432; + description = "Database port."; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "photoview"; + description = "Database name."; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "photoview"; + description = "Database user."; + }; + + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a file containing the database password. + Required when using MySQL or PostgreSQL. + ''; + }; + }; + + settings = { + disableFaceRecognition = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Disable face recognition feature."; + }; + + disableVideoEncoding = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Disable video encoding with FFmpeg."; + }; + + disableRawProcessing = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Disable RAW photo processing."; + }; + + mapboxToken = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Mapbox API token for map features."; + }; + + videoEncoder = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "h264_qsv" + "h264_vaapi" + "h264_nvenc" + ] + ); + default = null; + description = "Hardware video encoder to use."; + }; + }; + + secretsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to an environment file containing secrets. + Can be used for MAPBOX_TOKEN or other sensitive settings. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.database.type == "sqlite" || cfg.database.passwordFile != null; + message = "services.photoview.database.passwordFile must be set when using MySQL or PostgreSQL."; + } + ]; + + users.users = lib.mkIf (cfg.user == "photoview") { + photoview = { + group = cfg.group; + home = cfg.dataDir; + isSystemUser = true; + }; + }; + + users.groups = lib.mkIf (cfg.group == "photoview") { + photoview = { }; + }; + + systemd.services.photoview = { + description = "Photoview - Photo gallery for self-hosted personal servers"; + documentation = [ "https://photoview.github.io/docs/" ]; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + ] + ++ lib.optional (cfg.database.type == "postgres") "postgresql.service" + ++ lib.optional (cfg.database.type == "mysql") "mysql.service"; + requires = + lib.optional (cfg.database.type == "postgres") "postgresql.service" + ++ lib.optional (cfg.database.type == "mysql") "mysql.service"; + + environment = { + PHOTOVIEW_DATABASE_DRIVER = cfg.database.type; + PHOTOVIEW_LISTEN_IP = cfg.host; + PHOTOVIEW_LISTEN_PORT = toString cfg.port; + PHOTOVIEW_MEDIA_CACHE = "/var/cache/photoview"; + PHOTOVIEW_DISABLE_FACE_RECOGNITION = toString cfg.settings.disableFaceRecognition; + PHOTOVIEW_DISABLE_VIDEO_ENCODING = toString cfg.settings.disableVideoEncoding; + PHOTOVIEW_DISABLE_RAW_PROCESSING = toString cfg.settings.disableRawProcessing; + } + // lib.optionalAttrs (cfg.settings.mapboxToken != null) { + MAPBOX_TOKEN = cfg.settings.mapboxToken; + } + // lib.optionalAttrs (cfg.settings.videoEncoder != null) { + PHOTOVIEW_VIDEO_ENCODER = cfg.settings.videoEncoder; + }; + + script = '' + export ${dbUrl.${cfg.database.type}} + exec ${lib.getExe cfg.package} + ''; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + StateDirectory = "photoview"; + StateDirectoryMode = "0750"; + CacheDirectory = "photoview"; + CacheDirectoryMode = "0750"; + WorkingDirectory = cfg.dataDir; + Restart = "on-failure"; + RestartSec = 5; + + # Read access to media directory + ReadOnlyPaths = [ cfg.mediaPath ]; + + # Secrets + LoadCredential = lib.optional ( + cfg.database.passwordFile != null + ) "db_password:${cfg.database.passwordFile}"; + EnvironmentFile = lib.optional (cfg.secretsFile != null) cfg.secretsFile; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + UMask = "0077"; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ nettika ]; +} diff --git a/pkgs/by-name/ph/photoview/package.nix b/pkgs/by-name/ph/photoview/package.nix new file mode 100644 index 000000000000..cc636254147b --- /dev/null +++ b/pkgs/by-name/ph/photoview/package.nix @@ -0,0 +1,81 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, + callPackage, + makeWrapper, + pkg-config, + # Native dependencies for image processing and face recognition + dlib, + libjpeg, + libheif, + blas, + lapack, + # Runtime dependencies + exiftool, + darktable, + ffmpeg, +}: + +buildGoModule (finalAttrs: { + pname = "photoview"; + version = "2.4.0"; + + src = fetchFromGitHub { + owner = "photoview"; + repo = "photoview"; + rev = "v${finalAttrs.version}"; + hash = "sha256-ZfvBdQlyqONsrviZGL22Kt+AiPaVWwdoREDUrHDYyIs="; + }; + + vendorHash = "sha256-Tn4OxSV41s/4n2Q3teJRJNc39s6eKW4xE9wW/CIR5Fg="; + + modRoot = "api"; + + nativeBuildInputs = [ + pkg-config + makeWrapper + ]; + + buildInputs = [ + dlib + libjpeg + libheif + blas + lapack + ]; + + postInstall = '' + # Install face recognition models + mkdir -p $out/share/photoview + cp -r ${finalAttrs.src}/api/data $out/share/photoview/ + + # Symlink the UI + ln -s ${finalAttrs.passthru.ui} $out/share/photoview/ui + + # Rename binary and wrap with runtime dependencies + mv $out/bin/api $out/bin/photoview + wrapProgram $out/bin/photoview \ + --set PHOTOVIEW_FACE_RECOGNITION_MODELS_PATH $out/share/photoview/data/models \ + --set PHOTOVIEW_UI_PATH $out/share/photoview/ui \ + --set PHOTOVIEW_SERVE_UI 1 \ + --prefix PATH : ${ + lib.makeBinPath [ + exiftool + darktable + ffmpeg + ] + } + ''; + + passthru.ui = callPackage ./ui.nix { inherit (finalAttrs) src version; }; + + meta = { + description = "Photo gallery for self-hosted personal servers"; + homepage = "https://photoview.github.io/"; + license = lib.licenses.agpl3Only; + maintainers = with lib.maintainers; [ nettika ]; + mainProgram = "photoview"; + platforms = lib.platforms.linux; + }; +}) diff --git a/pkgs/by-name/ph/photoview/ui.nix b/pkgs/by-name/ph/photoview/ui.nix new file mode 100644 index 000000000000..bbaa5d3e95a3 --- /dev/null +++ b/pkgs/by-name/ph/photoview/ui.nix @@ -0,0 +1,32 @@ +{ + src, + version, + lib, + buildNpmPackage, +}: + +buildNpmPackage { + pname = "photoview-ui"; + inherit version; + + src = "${src}/ui"; + + npmDepsHash = "sha256-wUbfq+7SuJUBxfy9TxHVda8A0g4mmYCbzJT64XBN2mI="; + + NODE_ENV = "production"; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -r dist/* $out/ + runHook postInstall + ''; + + meta = { + description = "Web UI for Photoview photo gallery"; + homepage = "https://photoview.github.io/"; + license = lib.licenses.agpl3Only; + maintainers = with lib.maintainers; [ nettika ]; + platforms = lib.platforms.all; + }; +}