mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-07-13 21:50:33 +03:00
nixos/stash: init
This commit is contained in:
parent
8b1f4a0f08
commit
8814ab8ab5
6 changed files with 671 additions and 0 deletions
585
nixos/modules/services/web-apps/stash.nix
Normal file
585
nixos/modules/services/web-apps/stash.nix
Normal file
|
@ -0,0 +1,585 @@
|
|||
{
|
||||
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"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue