diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 07bfe34b0cd4..984e63fd1115 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -6173,6 +6173,13 @@ githubId = 20759788; name = "JP Lippold"; }; + DrakeTDL = { + name = "Drake"; + email = "draketdl@mailbox.org"; + matrix = "@draketdl:matrix.org"; + github = "DrakeTDL"; + githubId = 22124013; + }; dramaturg = { email = "seb@ds.ag"; github = "dramaturg"; diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index d510c0773b6f..4b888f502d00 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -115,6 +115,8 @@ - [Zipline](https://zipline.diced.sh/), a ShareX/file upload server that is easy to use, packed with features, and with an easy setup. Available as [services.zipline](#opt-services.zipline.enable). +- [Stash](https://github.com/stashapp/stash), An organizer for your adult videos/images, written in Go. Available as [services.stash](#opt-services.stash.enable). + - [Fider](https://fider.io/), an open platform to collect and prioritize feedback. Available as [services.fider](#opt-services.fider.enable). - [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 4b8b3caecba6..26a2d23291bf 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1577,6 +1577,7 @@ ./services/web-apps/snipe-it.nix ./services/web-apps/sogo.nix ./services/web-apps/stirling-pdf.nix + ./services/web-apps/stash.nix ./services/web-apps/trilium.nix ./services/web-apps/tt-rss.nix ./services/web-apps/vikunja.nix diff --git a/nixos/modules/services/web-apps/stash.nix b/nixos/modules/services/web-apps/stash.nix new file mode 100644 index 000000000000..60c8ad8a8eb4 --- /dev/null +++ b/nixos/modules/services/web-apps/stash.nix @@ -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: ) { 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" + ]; + }; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5eb314dad4ea..93115adf7efe 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -982,6 +982,7 @@ in { stalwart-mail = handleTest ./stalwart-mail.nix {}; stargazer = runTest ./web-servers/stargazer.nix; starship = handleTest ./starship.nix {}; + stash = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./stash.nix {}; static-web-server = handleTest ./web-servers/static-web-server.nix {}; step-ca = handleTestOn ["x86_64-linux"] ./step-ca.nix {}; stratis = handleTest ./stratis {}; diff --git a/nixos/tests/stash.nix b/nixos/tests/stash.nix new file mode 100644 index 000000000000..838a5e8a43c6 --- /dev/null +++ b/nixos/tests/stash.nix @@ -0,0 +1,80 @@ +import ./make-test-python.nix ( + let + host = "127.0.0.1"; + port = 1234; + dataDir = "/stash"; + in + { pkgs, ... }: + { + name = "stash"; + meta.maintainers = pkgs.stash.meta.maintainers; + + nodes.machine = { + services.stash = { + inherit dataDir; + enable = true; + + username = "test"; + passwordFile = pkgs.writeText "stash-password" "MyPassword"; + + jwtSecretKeyFile = pkgs.writeText "jwt_secret_key" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + sessionStoreKeyFile = pkgs.writeText "session_store_key" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + + plugins = + let + src = pkgs.fetchFromGitHub { + owner = "stashapp"; + repo = "CommunityScripts"; + rev = "9b6fac4934c2fac2ef0859ea68ebee5111fc5be5"; + hash = "sha256-PO3J15vaA7SD4r/LyHlXjnpaeYAN9Q++O94bIWdz7OA="; + }; + in + [ + (pkgs.runCommand "stashNotes" { inherit src; } '' + mkdir -p $out/plugins + cp -r $src/plugins/stashNotes $out/plugins/stashNotes + '') + (pkgs.runCommand "Theme-Plex" { inherit src; } '' + mkdir -p $out/plugins + cp -r $src/themes/Theme-Plex $out/plugins/Theme-Plex + '') + ]; + + mutableScrapers = true; + scrapers = + let + src = pkgs.fetchFromGitHub { + owner = "stashapp"; + repo = "CommunityScrapers"; + rev = "2ece82d17ddb0952c16842b0775274bcda598d81"; + hash = "sha256-AEmnvM8Nikhue9LNF9dkbleYgabCvjKHtzFpMse4otM="; + }; + in + [ + (pkgs.runCommand "FTV" { inherit src; } '' + mkdir -p $out/scrapers/FTV + cp -r $src/scrapers/FTV.yml $out/scrapers/FTV + '') + ]; + + settings = { + inherit host port; + + stash = [ { path = "/srv"; } ]; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("stash.service") + machine.wait_for_open_port(${toString port}, "${host}") + machine.succeed("curl --fail http://${host}:${toString port}/") + + with subtest("Test plugins/scrapers"): + with subtest("mutable plugins directory should not exist"): + machine.fail("test -d ${dataDir}/plugins") + with subtest("mutable scrapers directory should exist and scraper FTV should be linked"): + machine.succeed("test -L ${dataDir}/scrapers/FTV") + ''; + } +) diff --git a/pkgs/by-name/st/stash/package.nix b/pkgs/by-name/st/stash/package.nix index 46c3df1a33d3..5088847950dd 100644 --- a/pkgs/by-name/st/stash/package.nix +++ b/pkgs/by-name/st/stash/package.nix @@ -1,50 +1,137 @@ { + buildGoModule, + fetchFromGitHub, + fetchYarnDeps, lib, + nixosTests, + nodejs, + stash, stdenv, - fetchurl, + testers, + yarnBuildHook, + yarnConfigHook, }: let - - version = "0.25.1"; - - sources = { - x86_64-linux = { - url = "https://github.com/stashapp/stash/releases/download/v${version}/stash-linux"; - hash = "sha256-Rb4x6iKx6T9NPuWWDbNaz+35XPzLqZzSm0psv+k2Gw4="; - }; - aarch64-linux = { - url = "https://github.com/stashapp/stash/releases/download/v${version}/stash-linux-arm64v8"; - hash = "sha256-6qPyIYKFkhmBNO47w9E91FSKlByepBOnl0MNJighGSc="; - }; - x86_64-darwin = { - url = "https://github.com/stashapp/stash/releases/download/v${version}/stash-macos"; - hash = "sha256-W8+rgqWUDTOB8ykGO2GL9tKEjaDXdx9LpFg0TAtJsxM="; - }; - }; -in -stdenv.mkDerivation (finalAttrs: { - inherit version; + inherit (lib.importJSON ./version.json) + gitHash + srcHash + vendorHash + version + yarnHash + ; pname = "stash"; - src = fetchurl { inherit (sources.${stdenv.system}) url hash; }; + src = fetchFromGitHub { + owner = "stashapp"; + repo = "stash"; + tag = "v${version}"; + hash = srcHash; + }; - dontUnpack = true; + frontend = stdenv.mkDerivation (final: { + inherit version; + pname = "${pname}-ui"; + src = "${src}/ui/v2.5"; - installPhase = '' - runHook preInstall + yarnOfflineCache = fetchYarnDeps { + yarnLock = "${final.src}/yarn.lock"; + hash = yarnHash; + }; - install -Dm755 $src $out/bin/stash + nativeBuildInputs = [ + yarnConfigHook + yarnBuildHook + # Needed for executing package.json scripts + nodejs + ]; - runHook postInstall + postPatch = '' + substituteInPlace codegen.ts \ + --replace-fail "../../graphql/" "${src}/graphql/" + ''; + + buildPhase = '' + runHook preBuild + + export HOME=$(mktemp -d) + export VITE_APP_DATE='1970-01-01 00:00:00' + export VITE_APP_GITHASH=${gitHash} + export VITE_APP_STASH_VERSION=v${version} + export VITE_APP_NOLEGACY=true + + yarn --offline run gqlgen + yarn --offline build + + mv build $out + + runHook postBuild + ''; + + dontInstall = true; + dontFixup = true; + }); +in +buildGoModule { + inherit + pname + src + version + vendorHash + ; + + ldflags = [ + "-s" + "-w" + "-X 'github.com/stashapp/stash/internal/build.buildstamp=1970-01-01 00:00:00'" + "-X 'github.com/stashapp/stash/internal/build.githash=${gitHash}'" + "-X 'github.com/stashapp/stash/internal/build.version=v${version}'" + "-X 'github.com/stashapp/stash/internal/build.officialBuild=false'" + ]; + tags = [ + "sqlite_stat4" + "sqlite_math_functions" + ]; + + subPackages = [ "cmd/stash" ]; + + preBuild = '' + cp -a ${frontend} ui/v2.5/build + # `go mod tidy` requires internet access and does nothing + echo "skip_mod_tidy: true" >> gqlgen.yml + # remove `-trimpath` fron `GOFLAGS` because `gqlgen` does not work with it + GOFLAGS="''${GOFLAGS/-trimpath/}" go generate ./cmd/stash ''; - meta = with lib; { - description = "Stash is a self-hosted porn app"; - homepage = "https://github.com/stashapp/stash"; - license = licenses.agpl3Only; - maintainers = with maintainers; [ Golo300 ]; - platforms = builtins.attrNames sources; - mainProgram = "stash"; + strictDeps = true; + + passthru = { + inherit frontend; + updateScript = ./update.py; + tests = { + inherit (nixosTests) stash; + version = testers.testVersion { + package = stash; + version = "v${version} (${gitHash}) - Unofficial Build - 1970-01-01 00:00:00"; + }; + }; }; -}) + + meta = { + mainProgram = "stash"; + description = "Organizer for your adult videos/images"; + license = lib.licenses.agpl3Only; + homepage = "https://stashapp.cc/"; + changelog = "https://github.com/stashapp/stash/blob/v${version}/ui/v2.5/src/docs/en/Changelog/v${lib.versions.major version}${lib.versions.minor version}0.md"; + maintainers = with lib.maintainers; [ + Golo300 + DrakeTDL + ]; + platforms = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + }; +} diff --git a/pkgs/by-name/st/stash/update.py b/pkgs/by-name/st/stash/update.py new file mode 100755 index 000000000000..85747eb654b2 --- /dev/null +++ b/pkgs/by-name/st/stash/update.py @@ -0,0 +1,82 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i python3 -p python3 prefetch-yarn-deps nix-prefetch-git nix-prefetch + +from pathlib import Path +from shutil import copyfile +from urllib.request import Request, urlopen +import json +import os +import subprocess + + +def run_external(args: list[str]): + proc = subprocess.run( + args, + check=True, + stdout=subprocess.PIPE, + ) + + return proc.stdout.strip().decode("utf8") + +def get_latest_release_tag(): + req = Request("https://api.github.com/repos/stashapp/stash/tags?per_page=1") + + if "GITHUB_TOKEN" in os.environ: + req.add_header("authorization", f"Bearer {os.environ['GITHUB_TOKEN']}") + + with urlopen(req) as resp: + return json.loads(resp.read())[0] + +def prefetch_github(rev: str): + print(f"Prefetching stashapp/stash({rev})") + + proc = run_external(["nix-prefetch-git", "--no-deepClone", "--rev", rev, f"https://github.com/stashapp/stash"]) + + return json.loads(proc) + +def prefetch_yarn(lock_file: str): + print(f"Prefetching yarn deps") + + hash = run_external(["prefetch-yarn-deps", lock_file]) + + return run_external(["nix", "hash", "convert", "--hash-algo", "sha256", hash]) + +def prefetch_go_modules(src: str, version: str): + print(f"Prefetching go modules") + expr = fr""" + {{ sha256 }}: (buildGoModule {{ + pname = "stash"; + src = {src}; + version = "{version}"; + vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }}).goModules.overrideAttrs (_: {{ modSha256 = sha256; }}) + """ + return run_external([ + "nix-prefetch", + "--option", + "extra-experimental-features", + "flakes", + expr + ]) + + +def save_version_json(version: dict[str, str]): + print("Writing version.json") + with open(Path(__file__).parent / "version.json", 'w') as f: + json.dump(version, f, indent=2) + f.write("\n") + +if __name__ == "__main__": + release = get_latest_release_tag() + + src = prefetch_github(release['name']) + + yarn_hash = prefetch_yarn(f"{src['path']}/ui/v2.5/yarn.lock") + + save_version_json({ + "version": release["name"][1:], + "gitHash": release["commit"]["sha"][:8], + "srcHash": src["hash"], + "yarnHash": yarn_hash, + "vendorHash": prefetch_go_modules(src["path"], release["name"][1:]) + }) diff --git a/pkgs/by-name/st/stash/version.json b/pkgs/by-name/st/stash/version.json new file mode 100644 index 000000000000..73be8439fba4 --- /dev/null +++ b/pkgs/by-name/st/stash/version.json @@ -0,0 +1,7 @@ +{ + "version": "0.27.2", + "gitHash": "76648fee", + "srcHash": "sha256-SMZBDKqgVdXf2abaSf/FuG2Vodav7SBu6onjHFZIZIM=", + "yarnHash": "sha256-ufGYQfEbcXO3XhpDQ3UTofS5B31L427KWy5NPbWhBJo=", + "vendorHash": "sha256-ZtKKs0JCEe4OpPulO74qYTYrZu2Ds3prWp5N8UP6z0g=" +}