mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-11 20:25:32 +03:00

Fixes services.stash.mutableSettings so that it makes sense. The logic was previously backwards and the settings would be overriden if mutableSettings was true.
585 lines
19 KiB
Nix
585 lines
19 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
let
|
|
inherit (lib)
|
|
getExe
|
|
literalExpression
|
|
mkEnableOption
|
|
mkIf
|
|
mkOption
|
|
mkPackageOption
|
|
optionalString
|
|
toUpper
|
|
types
|
|
;
|
|
|
|
cfg = config.services.stash;
|
|
|
|
stashType = types.submodule {
|
|
options = {
|
|
path = mkOption {
|
|
type = types.path;
|
|
description = "location of your media files";
|
|
};
|
|
excludevideo = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Whether to exclude video files from being scanned into Stash";
|
|
};
|
|
excludeimage = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Whether to exclude image files from being scanned into Stash";
|
|
};
|
|
};
|
|
};
|
|
stashBoxType = types.submodule {
|
|
options = {
|
|
name = mkOption {
|
|
type = types.str;
|
|
description = "The name of the Stash Box";
|
|
};
|
|
endpoint = mkOption {
|
|
type = types.str;
|
|
description = "URL to the Stash Box graphql api";
|
|
};
|
|
apikey = mkOption {
|
|
type = types.str;
|
|
description = "Stash Box API key";
|
|
};
|
|
};
|
|
};
|
|
|
|
recentlyReleased = mode: {
|
|
__typename = "CustomFilter";
|
|
message = {
|
|
id = "recently_released_objects";
|
|
values.objects = mode;
|
|
};
|
|
mode = toUpper mode;
|
|
sortBy = "date";
|
|
direction = "DESC";
|
|
};
|
|
recentlyAdded = mode: {
|
|
__typename = "CustomFilter";
|
|
message = {
|
|
id = "recently_added_objects";
|
|
values.objects = mode;
|
|
};
|
|
mode = toUpper mode;
|
|
sortBy = "created_at";
|
|
direction = "DESC";
|
|
};
|
|
uiPresets = {
|
|
recentlyReleasedScenes = recentlyReleased "Scenes";
|
|
recentlyAddedScenes = recentlyAdded "Scenes";
|
|
recentlyReleasedGalleries = recentlyReleased "Galleries";
|
|
recentlyAddedGalleries = recentlyAdded "Galleries";
|
|
recentlyAddedImages = recentlyAdded "Images";
|
|
recentlyReleasedMovies = recentlyReleased "Movies";
|
|
recentlyAddedMovies = recentlyAdded "Movies";
|
|
recentlyAddedStudios = recentlyAdded "Studios";
|
|
recentlyAddedPerformers = recentlyAdded "Performers";
|
|
};
|
|
|
|
settingsFormat = pkgs.formats.yaml { };
|
|
settingsFile = settingsFormat.generate "config.yml" cfg.settings;
|
|
settingsType = types.submodule {
|
|
freeformType = settingsFormat.type;
|
|
|
|
options = {
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
example = "::1";
|
|
description = "The ip address that Stash should bind to.";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 9999;
|
|
example = 1234;
|
|
description = "The port that Stash should listen on.";
|
|
};
|
|
|
|
stash = mkOption {
|
|
type = types.listOf stashType;
|
|
description = ''
|
|
Add directories containing your adult videos and images.
|
|
Stash will use these directories to find videos and/or images during scanning.
|
|
'';
|
|
example = literalExpression ''
|
|
{
|
|
stash = [
|
|
{
|
|
Path = "/media/drive/videos";
|
|
ExcludeImage = true;
|
|
}
|
|
];
|
|
}
|
|
'';
|
|
};
|
|
stash_boxes = mkOption {
|
|
type = types.listOf stashBoxType;
|
|
default = [ ];
|
|
description = ''Stash-box facilitates automated tagging of scenes and performers based on fingerprints and filenames'';
|
|
example = literalExpression ''
|
|
{
|
|
stash_boxes = [
|
|
{
|
|
name = "StashDB";
|
|
endpoint = "https://stashdb.org/graphql";
|
|
apikey = "aaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccc";
|
|
}
|
|
];
|
|
}
|
|
'';
|
|
};
|
|
ui.frontPageContent = mkOption {
|
|
description = "Search filters to display on the front page.";
|
|
type = types.either (types.listOf types.attrs) (types.functionTo (types.listOf types.attrs));
|
|
default = presets: [
|
|
presets.recentlyReleasedScenes
|
|
presets.recentlyAddedStudios
|
|
presets.recentlyReleasedMovies
|
|
presets.recentlyAddedPerformers
|
|
presets.recentlyReleasedGalleries
|
|
];
|
|
example = literalExpression ''
|
|
presets: [
|
|
# To get the savedFilterId, you can query `{ findSavedFilters(mode: <FilterMode>) { id name } }` on localhost:9999/graphql
|
|
{
|
|
__typename = "SavedFilter";
|
|
savedFilterId = 1;
|
|
}
|
|
# basic custom filter
|
|
{
|
|
__typename = "CustomFilter";
|
|
title = "Random Scenes";
|
|
mode = "SCENES";
|
|
sortBy = "random";
|
|
direction = "DESC";
|
|
}
|
|
presets.recentlyAddedImages
|
|
]
|
|
'';
|
|
apply = type: if builtins.isFunction type then (type uiPresets) else type;
|
|
};
|
|
blobs_path = mkOption {
|
|
type = types.path;
|
|
default = "${cfg.dataDir}/blobs";
|
|
description = "Path to blobs";
|
|
};
|
|
cache = mkOption {
|
|
type = types.path;
|
|
default = "${cfg.dataDir}/cache";
|
|
description = "Path to cache";
|
|
};
|
|
database = mkOption {
|
|
type = types.path;
|
|
default = "${cfg.dataDir}/go.sqlite";
|
|
description = "Path to the SQLite database";
|
|
};
|
|
generated = mkOption {
|
|
type = types.path;
|
|
default = "${cfg.dataDir}/generated";
|
|
description = "Path to generated files";
|
|
};
|
|
plugins_path = mkOption {
|
|
type = types.path;
|
|
default = "${cfg.dataDir}/plugins";
|
|
description = "Path to scrapers";
|
|
};
|
|
scrapers_path = mkOption {
|
|
type = types.path;
|
|
default = "${cfg.dataDir}/scrapers";
|
|
description = "Path to scrapers";
|
|
};
|
|
|
|
blobs_storage = mkOption {
|
|
type = types.enum [
|
|
"FILESYSTEM"
|
|
"DATABASE"
|
|
];
|
|
default = "FILESYSTEM";
|
|
description = "Where to store blobs";
|
|
};
|
|
calculate_md5 = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Whether to calculate MD5 checksums for scene video files";
|
|
};
|
|
create_image_clip_from_videos = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Create Image Clips from Video extensions when Videos are disabled in Library";
|
|
};
|
|
dangerous_allow_public_without_auth = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Learn more at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet/";
|
|
};
|
|
gallery_cover_regex = mkOption {
|
|
type = types.str;
|
|
default = "(poster|cover|folder|board)\\.[^.]+$";
|
|
description = "Regex used to identify images as gallery covers";
|
|
};
|
|
no_proxy = mkOption {
|
|
type = types.str;
|
|
default = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12";
|
|
description = "A list of domains for which the proxy must not be used";
|
|
};
|
|
nobrowser = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "If we should not auto-open a browser window on startup";
|
|
};
|
|
notifications_enabled = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "If we should send notifications to the desktop";
|
|
};
|
|
parallel_tasks = mkOption {
|
|
type = types.int;
|
|
default = 1;
|
|
description = "Number of parallel tasks to start during scan/generate";
|
|
};
|
|
preview_audio = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Include audio stream in previews";
|
|
};
|
|
preview_exclude_end = mkOption {
|
|
type = types.int;
|
|
default = 0;
|
|
description = "Duration of start of video to exclude when generating previews";
|
|
};
|
|
preview_exclude_start = mkOption {
|
|
type = types.int;
|
|
default = 0;
|
|
description = "Duration of end of video to exclude when generating previews";
|
|
};
|
|
preview_segment_duration = mkOption {
|
|
type = types.float;
|
|
default = 0.75;
|
|
description = "Preview segment duration, in seconds";
|
|
};
|
|
preview_segments = mkOption {
|
|
type = types.int;
|
|
default = 12;
|
|
description = "Number of segments in a preview file";
|
|
};
|
|
security_tripwire_accessed_from_public_internet = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = "";
|
|
description = "Learn more at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet/";
|
|
};
|
|
sequential_scanning = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Modifies behaviour of the scanning functionality to generate support files (previews/sprites/phash) at the same time as fingerprinting/screenshotting";
|
|
};
|
|
show_one_time_moved_notification = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Whether a small notification to inform the user that Stash will no longer show a terminal window, and instead will be available in the tray";
|
|
};
|
|
sound_on_preview = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Enable sound on mouseover previews";
|
|
};
|
|
theme_color = mkOption {
|
|
type = types.str;
|
|
default = "#202b33";
|
|
description = "Sets the `theme-color` property in the UI";
|
|
};
|
|
video_file_naming_algorithm = mkOption {
|
|
type = types.enum [
|
|
"OSHASH"
|
|
"MD5"
|
|
];
|
|
default = "OSHASH";
|
|
description = "Hash algorithm to use for generated file naming";
|
|
};
|
|
write_image_thumbnails = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Write image thumbnails to disk when generating on the fly";
|
|
};
|
|
};
|
|
};
|
|
|
|
pluginType =
|
|
kind:
|
|
mkOption {
|
|
type = types.listOf types.package;
|
|
default = [ ];
|
|
description = ''
|
|
The ${kind} Stash should be started with.
|
|
'';
|
|
apply =
|
|
srcs:
|
|
optionalString (srcs != [ ]) (
|
|
pkgs.runCommand "stash-${kind}"
|
|
{
|
|
inherit srcs;
|
|
nativeBuildInputs = [ pkgs.yq-go ];
|
|
preferLocalBuild = true;
|
|
}
|
|
''
|
|
find $srcs -mindepth 1 -name '*.yml' | while read plugin_file; do
|
|
grep -q "^#pkgignore" "$plugin_file" && continue
|
|
|
|
plugin_dir=$(dirname $plugin_file)
|
|
out_path=$out/$(basename $plugin_dir)
|
|
mkdir -p $out_path
|
|
ls $plugin_dir | xargs -I{} ln -sf "$plugin_dir/{}" $out_path
|
|
|
|
env \
|
|
plugin_id=$(basename $plugin_file .yml) \
|
|
plugin_name="$(yq '.name' $plugin_file)" \
|
|
plugin_description="$(yq '.description' $plugin_file)" \
|
|
plugin_version="$(yq '.version' $plugin_file)" \
|
|
plugin_files="$(find -L $out_path -mindepth 1 -type f -printf "%P\n")" \
|
|
yq -n '
|
|
.id = strenv(plugin_id) |
|
|
.name = strenv(plugin_name) |
|
|
(
|
|
strenv(plugin_description) as $desc |
|
|
with(select($desc == "null"); .metadata = {}) |
|
|
with(select($desc != "null"); .metadata.description = $desc)
|
|
) |
|
|
(
|
|
strenv(plugin_version) as $ver |
|
|
with(select($ver == "null"); .version = "Unknown") |
|
|
with(select($ver != "null"); .version = $ver)
|
|
) |
|
|
.date = (now | format_datetime("2006-01-02 15:04:05")) |
|
|
.files = (strenv(plugin_files) | split("\n"))
|
|
' > $out_path/manifest
|
|
done
|
|
''
|
|
);
|
|
};
|
|
in
|
|
{
|
|
meta = {
|
|
buildDocsInSandbox = false;
|
|
maintainers = with lib.maintainers; [ DrakeTDL ];
|
|
};
|
|
|
|
options = {
|
|
services.stash = {
|
|
enable = mkEnableOption "stash";
|
|
|
|
package = mkPackageOption pkgs "stash" { };
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "stash";
|
|
description = "User under which Stash runs.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "stash";
|
|
description = "Group under which Stash runs.";
|
|
};
|
|
|
|
dataDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/stash";
|
|
description = "The directory where Stash stores its files.";
|
|
};
|
|
|
|
openFirewall = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Open ports in the firewall for the Stash web interface.";
|
|
};
|
|
|
|
username = mkOption {
|
|
type = types.nullOr types.nonEmptyStr;
|
|
default = null;
|
|
example = "admin";
|
|
description = ''
|
|
Username for login.
|
|
|
|
::: {.warning}
|
|
This option takes precedence over {option}`services.stash.settings.username`
|
|
::
|
|
|
|
'';
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/path/to/password/file";
|
|
description = ''
|
|
Path to file containing password for login.
|
|
|
|
::: {.warning}
|
|
This option takes precedence over {option}`services.stash.settings.password`
|
|
::
|
|
|
|
'';
|
|
};
|
|
|
|
jwtSecretKeyFile = mkOption {
|
|
type = types.path;
|
|
description = "Path to file containing a secret used to sign JWT tokens.";
|
|
};
|
|
sessionStoreKeyFile = mkOption {
|
|
type = types.path;
|
|
description = "Path to file containing a secret for session store.";
|
|
};
|
|
|
|
mutableSettings = mkOption {
|
|
description = ''
|
|
Whether the Stash config.yml is writeable by Stash.
|
|
|
|
If `false`, Any config changes done from within Stash UI will be temporary and reset to those defined in {option}`services.stash.settings` upon `Stash.service` restart.
|
|
If `true`, the {option}`services.stash.settings` will only be used to initialize the Stash configuration if it does not exist, and are subsequently ignored.
|
|
'';
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
mutablePlugins = mkEnableOption "Whether plugins/themes can be installed, updated, uninstalled manually.";
|
|
mutableScrapers = mkEnableOption "Whether scrapers can be installed, updated, uninstalled manually.";
|
|
plugins = pluginType "plugins";
|
|
scrapers = pluginType "scrapers";
|
|
|
|
settings = mkOption {
|
|
type = settingsType;
|
|
description = "Stash configuration";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion =
|
|
!lib.xor (cfg.username != null || cfg.settings.username or null != null) (
|
|
cfg.passwordFile != null || cfg.settings.password or null != null
|
|
);
|
|
message = "You must set either both username and password, or neither.";
|
|
}
|
|
];
|
|
|
|
services.stash.settings = {
|
|
username = mkIf (cfg.username != null) cfg.username;
|
|
plugins_path = mkIf (!cfg.mutablePlugins) cfg.plugins;
|
|
scrapers_path = mkIf (!cfg.mutableScrapers) cfg.scrapers;
|
|
};
|
|
|
|
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.port ];
|
|
|
|
users.users.${cfg.user} = {
|
|
inherit (cfg) group;
|
|
isSystemUser = true;
|
|
home = cfg.dataDir;
|
|
};
|
|
users.groups.${cfg.group} = { };
|
|
|
|
systemd = {
|
|
tmpfiles.settings."10-stash-datadir".${cfg.dataDir}."d" = {
|
|
inherit (cfg) user group;
|
|
mode = "0755";
|
|
};
|
|
services.stash = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
path = with pkgs; [
|
|
ffmpeg-full
|
|
python3
|
|
ruby
|
|
];
|
|
environment.STASH_CONFIG_FILE = "${cfg.dataDir}/config.yml";
|
|
serviceConfig = {
|
|
DynamicUser = false;
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
Restart = "on-failure";
|
|
WorkingDirectory = cfg.dataDir;
|
|
StateDirectory = mkIf (cfg.dataDir == "/var/lib/stash") (baseNameOf cfg.dataDir);
|
|
ExecStartPre = pkgs.writers.writeBash "stash-setup.bash" (
|
|
''
|
|
install -d ${cfg.settings.generated}
|
|
if [[ -z "${toString cfg.mutableSettings}" || ! -f ${cfg.dataDir}/config.yml ]]; then
|
|
env \
|
|
password=$(< ${cfg.passwordFile}) \
|
|
jwtSecretKeyFile=$(< ${cfg.jwtSecretKeyFile}) \
|
|
sessionStoreKeyFile=$(< ${cfg.sessionStoreKeyFile}) \
|
|
${lib.getExe pkgs.yq-go} '
|
|
.jwt_secret_key = strenv(jwtSecretKeyFile) |
|
|
.session_store_key = strenv(sessionStoreKeyFile) |
|
|
(
|
|
strenv(password) as $password |
|
|
with(select($password != ""); .password = $password)
|
|
)
|
|
' ${settingsFile} > ${cfg.dataDir}/config.yml
|
|
fi
|
|
''
|
|
+ optionalString cfg.mutablePlugins ''
|
|
install -d ${cfg.settings.plugins_path}
|
|
ls ${cfg.plugins} | xargs -I{} ln -sf '${cfg.plugins}/{}' ${cfg.settings.plugins_path}
|
|
''
|
|
+ optionalString cfg.mutableScrapers ''
|
|
install -d ${cfg.settings.scrapers_path}
|
|
ls ${cfg.scrapers} | xargs -I{} ln -sf '${cfg.scrapers}/{}' ${cfg.settings.scrapers_path}
|
|
''
|
|
);
|
|
ExecStart = getExe cfg.package;
|
|
|
|
ProtectHome = "tmpfs";
|
|
BindReadOnlyPaths = mkIf (cfg.settings != { }) (map (stash: "${stash.path}") cfg.settings.stash);
|
|
|
|
# hardening
|
|
|
|
DevicePolicy = "auto"; # needed for hardware acceleration
|
|
PrivateDevices = false; # needed for hardware acceleration
|
|
AmbientCapabilities = [ "" ];
|
|
CapabilityBoundingSet = [ "" ];
|
|
ProtectSystem = "full";
|
|
LockPersonality = true;
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
PrivateUsers = true;
|
|
ProtectClock = true;
|
|
ProtectControlGroups = true;
|
|
ProtectHostname = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProcSubset = "pid";
|
|
ProtectProc = "invisible";
|
|
RemoveIPC = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_UNIX"
|
|
"AF_INET"
|
|
"AF_INET6"
|
|
];
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
MemoryDenyWriteExecute = true;
|
|
SystemCallArchitectures = "native";
|
|
SystemCallFilter = [
|
|
"~@cpu-emulation"
|
|
"~@debug"
|
|
"~@mount"
|
|
"~@obsolete"
|
|
"~@privileged"
|
|
];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|