linkding: init at v1.45.0 (#509346)
This commit is contained in:
@@ -109,6 +109,8 @@
|
||||
|
||||
- [Ente Auth](https://ente.io/auth/), an open source 2FA authenticator, with end-to-end encrypted backups. Available as [programs.ente-auth](#opt-programs.ente-auth.enable).
|
||||
|
||||
- [linkding](https://linkding.link/), a self-hosted bookmark manager designed to be minimal, fast, and easy to set up. Available as [services.linkding](#opt-services.linkding.enable).
|
||||
|
||||
- [Tinyauth](https://tinyauth.app/), a simple authentication middleware for web apps, with OAuth and LDAP support. Available as [services.tinyauth](#opt-services.tinyauth.enable).
|
||||
|
||||
- [Strichliste](https://www.strichliste.org), a digital self-service tallysheet used in hackerspaces, clubs and offices. Available as [services.strichliste](#opt-services.strichliste.enable).
|
||||
|
||||
@@ -1698,6 +1698,7 @@
|
||||
./services/web-apps/librespeed.nix
|
||||
./services/web-apps/libretranslate.nix
|
||||
./services/web-apps/limesurvey.nix
|
||||
./services/web-apps/linkding.nix
|
||||
./services/web-apps/linkwarden.nix
|
||||
./services/web-apps/lubelogger.nix
|
||||
./services/web-apps/mainsail.nix
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
optionalAttrs
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.linkding;
|
||||
in
|
||||
{
|
||||
options.services.linkding = {
|
||||
enable = mkEnableOption "linkding, a self-hosted bookmark manager";
|
||||
|
||||
package = mkPackageOption pkgs "linkding" { };
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "linkding";
|
||||
description = ''
|
||||
User account under which linkding runs.
|
||||
|
||||
::: {.note}
|
||||
If left as the default value this user will automatically be created
|
||||
on system activation, otherwise you are responsible for ensuring the
|
||||
user exists before the linkding service starts.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "linkding";
|
||||
description = ''
|
||||
Group under which linkding runs.
|
||||
|
||||
::: {.note}
|
||||
If left as the default value this group will automatically be created
|
||||
on system activation, otherwise you are responsible for ensuring the
|
||||
group exists before the linkding service starts.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/linkding";
|
||||
description = "Directory used for all mutable state: SQLite database, secret key, favicons, previews, and assets.";
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Address on which linkding listens.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 9090;
|
||||
description = "Port on which linkding listens.";
|
||||
};
|
||||
|
||||
contextPath = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "linkding/";
|
||||
description = ''
|
||||
Configures a URL context path under which linkding is accessible.
|
||||
When set, linkding is available at `http://host:<port>/<contextPath>`.
|
||||
Must end with a `/` when non-empty.
|
||||
'';
|
||||
};
|
||||
|
||||
environmentFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/secrets/linkding.env";
|
||||
description = ''
|
||||
Path to an environment file loaded by all linkding services.
|
||||
Useful for injecting secrets that should not appear in the Nix store,
|
||||
such as `LD_DB_PASSWORD` or `LD_SUPERUSER_PASSWORD`.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
example = {
|
||||
LD_DISABLE_BACKGROUND_TASKS = "True";
|
||||
LD_DISABLE_URL_VALIDATION = "True";
|
||||
LD_ENABLE_OIDC = "True";
|
||||
};
|
||||
description = ''
|
||||
Additional environment variables passed to linkding.
|
||||
Refer to the [linkding documentation](https://linkding.link/options/)
|
||||
for the full list of supported `LD_*` options.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [
|
||||
"sqlite"
|
||||
"postgres"
|
||||
];
|
||||
default = "sqlite";
|
||||
description = "Database engine to use. Defaults to SQLite.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "PostgreSQL server host.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 5432;
|
||||
description = "PostgreSQL server port.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "linkding";
|
||||
description = "PostgreSQL database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "linkding";
|
||||
description = "PostgreSQL user name.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to automatically create a local PostgreSQL database and user.";
|
||||
};
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Open the linkding port in the firewall.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable (
|
||||
let
|
||||
pkg = cfg.package;
|
||||
|
||||
usePostgres = cfg.database.type == "postgres";
|
||||
|
||||
pythonPath =
|
||||
"${pkg.passthru.python.pkgs.makePythonPath pkg.passthru.dependencies}:${lib.getBin pkg}/${pkg.passthru.python.sitePackages}"
|
||||
+ lib.strings.optionalString usePostgres ":${pkg.passthru.python.pkgs.makePythonPath pkg.optional-dependencies.postgres}";
|
||||
|
||||
# Build the environment passed to every linkding process.
|
||||
environment = {
|
||||
DJANGO_SETTINGS_MODULE = "bookmarks.settings.prod";
|
||||
_NIXOS_LINKDING_DATA_DIR = cfg.dataDir;
|
||||
LD_SERVER_PORT = toString cfg.port;
|
||||
}
|
||||
// optionalAttrs (cfg.contextPath != "") {
|
||||
LD_CONTEXT_PATH = cfg.contextPath;
|
||||
}
|
||||
// optionalAttrs usePostgres {
|
||||
LD_DB_ENGINE = "postgres";
|
||||
LD_DB_DATABASE = cfg.database.name;
|
||||
LD_DB_USER = cfg.database.user;
|
||||
LD_DB_HOST = "/run/postgresql";
|
||||
}
|
||||
// optionalAttrs (usePostgres && !cfg.database.createLocally) {
|
||||
LD_DB_HOST = cfg.database.host;
|
||||
LD_DB_PORT = toString cfg.database.port;
|
||||
}
|
||||
// cfg.settings;
|
||||
|
||||
environmentFile = pkgs.writeText "linkding-environment" (lib.generators.toKeyValue { } environment);
|
||||
|
||||
# Generate a uwsgi.ini for the linkding instance, adapted for NixOS from
|
||||
# the upstream uwsgi.ini. The static-map entries serve pre-generated
|
||||
# static files from the Nix store as well as the mutable user-data
|
||||
# directories (favicons, previews) from the data directory.
|
||||
uwsgiIni = pkgs.writeText "linkding-uwsgi.ini" ''
|
||||
[uwsgi]
|
||||
plugins-dir = ${pkg.passthru.uwsgiWithPython}/lib/uwsgi
|
||||
plugin = python3
|
||||
module = bookmarks.wsgi:application
|
||||
env = DJANGO_SETTINGS_MODULE=bookmarks.settings.prod
|
||||
processes = 2
|
||||
threads = 2
|
||||
buffer-size = 8192
|
||||
die-on-term = true
|
||||
mime-file = ${pkgs.mailcap}/etc/mime.types
|
||||
http = ${cfg.address}:${toString cfg.port}
|
||||
static-map = /${cfg.contextPath}static=${pkg}/${pkg.passthru.python.sitePackages}/bookmarks/static
|
||||
static-map = /${cfg.contextPath}static=${cfg.dataDir}/favicons
|
||||
static-map = /${cfg.contextPath}static=${cfg.dataDir}/previews
|
||||
static-map = /${cfg.contextPath}robots.txt=${pkg}/${pkg.passthru.python.sitePackages}/bookmarks/static/robots.txt
|
||||
|
||||
if-env = LD_REQUEST_TIMEOUT
|
||||
http-timeout = %(_)
|
||||
socket-timeout = %(_)
|
||||
harakiri = %(_)
|
||||
endif =
|
||||
|
||||
if-env = LD_REQUEST_MAX_CONTENT_LENGTH
|
||||
limit-post = %(_)
|
||||
endif =
|
||||
|
||||
if-env = LD_LOG_X_FORWARDED_FOR
|
||||
log-x-forwarded-for = %(_)
|
||||
endif =
|
||||
|
||||
if-env = LD_DISABLE_REQUEST_LOGS=true
|
||||
disable-logging = true
|
||||
log-4xx = true
|
||||
log-5xx = true
|
||||
endif =
|
||||
'';
|
||||
|
||||
# Manage wrapper script installed into the system PATH so administrators can
|
||||
# run Django management commands as the linkding service user.
|
||||
linkdingManageScript =
|
||||
let
|
||||
args = lib.escapeShellArgs (
|
||||
[
|
||||
"--uid=${cfg.user}"
|
||||
"--gid=${cfg.group}"
|
||||
"--working-directory=${cfg.dataDir}"
|
||||
"--property=EnvironmentFile=${environmentFile}"
|
||||
]
|
||||
++ lib.optional (cfg.environmentFile != null) "--property=EnvironmentFile=${cfg.environmentFile}"
|
||||
++ [
|
||||
"--property=ReadWritePaths=${cfg.dataDir}"
|
||||
"--setenv=PYTHONPATH=${pythonPath}"
|
||||
"--pty"
|
||||
"--wait"
|
||||
"--collect"
|
||||
"--service-type=exec"
|
||||
"--quiet"
|
||||
"--"
|
||||
"${lib.getExe' pkg "linkding"}"
|
||||
]
|
||||
);
|
||||
in
|
||||
pkgs.writeShellScriptBin "linkding-manage" ''
|
||||
exec ${lib.getExe' config.systemd.package "systemd-run"} ${args} "$@"
|
||||
'';
|
||||
|
||||
commonServiceConfig = {
|
||||
Slice = "system-linkding.slice";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
EnvironmentFile = [
|
||||
environmentFile
|
||||
]
|
||||
++ lib.optional (cfg.environmentFile != null) cfg.environmentFile;
|
||||
Environment = "PYTHONPATH=${pythonPath}";
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
StateDirectory = [
|
||||
"linkding"
|
||||
"linkding/favicons"
|
||||
"linkding/previews"
|
||||
"linkding/assets"
|
||||
];
|
||||
StateDirectoryMode = "0750";
|
||||
# Hardening
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ cfg.dataDir ];
|
||||
PrivateMounts = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
RemoveIPC = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.database.createLocally -> usePostgres;
|
||||
message = "services.linkding.database.createLocally requires services.linkding.database.type = \"postgres\"";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.database.createLocally -> cfg.database.host == "localhost" || cfg.database.host == "";
|
||||
message = "services.linkding.database.host should be empty or \"localhost\" when createLocally is enabled";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.database.createLocally
|
||||
-> cfg.database.user == cfg.user && cfg.database.user == cfg.database.name;
|
||||
message = "services.linkding.database.user must match services.linkding.user and services.linkding.database.name when createLocally is enabled";
|
||||
}
|
||||
{
|
||||
assertion = cfg.contextPath == "" || lib.hasSuffix "/" cfg.contextPath;
|
||||
message = "services.linkding.contextPath must end with \"/\" when non-empty";
|
||||
}
|
||||
];
|
||||
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
|
||||
environment.systemPackages = [ linkdingManageScript ];
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.dataDir;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
systemd.slices.system-linkding = {
|
||||
description = "linkding bookmark manager System Slice";
|
||||
documentation = [ "https://linkding.link/" ];
|
||||
};
|
||||
|
||||
# One-shot setup service: run database migrations and first-time
|
||||
# initialization steps taken from the upstream bootstrap.sh.
|
||||
systemd.services.linkding-setup = {
|
||||
description = "linkding database migrations and initialization";
|
||||
after = [
|
||||
"network.target"
|
||||
]
|
||||
++ lib.optionals (usePostgres && cfg.database.createLocally) [ "postgresql.target" ];
|
||||
requires = lib.optionals (usePostgres && cfg.database.createLocally) [ "postgresql.target" ];
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${lib.getExe' pkg "linkding-bootstrap"}";
|
||||
};
|
||||
};
|
||||
|
||||
# Main WSGI service — starts after setup completes.
|
||||
systemd.services.linkding = {
|
||||
description = "linkding bookmark manager";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "linkding-setup.service" ];
|
||||
requires = [ "linkding-setup.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 60;
|
||||
serviceConfig = commonServiceConfig // {
|
||||
Type = "exec";
|
||||
ExecStart = "${lib.getExe pkgs.uwsgi} --ini ${uwsgiIni}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
# Background task processor (Huey). Can be disabled via
|
||||
# services.linkding.settings.LD_DISABLE_BACKGROUND_TASKS = "True".
|
||||
systemd.services.linkding-background-tasks =
|
||||
mkIf ((cfg.settings.LD_DISABLE_BACKGROUND_TASKS or "False") != "True")
|
||||
{
|
||||
description = "linkding background task processor";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "linkding-setup.service" ];
|
||||
requires = [ "linkding-setup.service" ];
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
Type = "exec";
|
||||
ExecStart = "${lib.getExe' pkg "linkding"} run_huey -f";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
};
|
||||
};
|
||||
|
||||
# Automatically provision a local PostgreSQL database when requested.
|
||||
services.postgresql = mkIf cfg.database.createLocally {
|
||||
enable = true;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.database.user;
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ squat ];
|
||||
}
|
||||
@@ -891,6 +891,8 @@ in
|
||||
lighttpd = runTest ./lighttpd.nix;
|
||||
limesurvey = runTest ./limesurvey.nix;
|
||||
limine = import ./limine { inherit runTest; };
|
||||
linkding = runTest ./web-apps/linkding.nix;
|
||||
linkding-postgres = runTest ./web-apps/linkding-postgres.nix;
|
||||
linkwarden = runTest ./web-apps/linkwarden.nix;
|
||||
listmonk = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./listmonk.nix { };
|
||||
litellm = runTest ./litellm.nix;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "linkding-postgres";
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ squat ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
services.linkding = {
|
||||
enable = true;
|
||||
port = 9090;
|
||||
database = {
|
||||
createLocally = true;
|
||||
type = "postgres";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("linkding.service")
|
||||
machine.wait_for_open_port(9090)
|
||||
|
||||
with subtest("Login page loads"):
|
||||
machine.succeed(
|
||||
"curl -sSfL http://127.0.0.1:9090 | grep -i 'linkding'"
|
||||
)
|
||||
|
||||
with subtest("Health endpoint responds"):
|
||||
machine.succeed(
|
||||
"curl -sSf http://127.0.0.1:9090/health"
|
||||
)
|
||||
|
||||
with subtest("linkding-manage works"):
|
||||
machine.succeed(
|
||||
"linkding-manage version"
|
||||
)
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
name = "linkding";
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ squat ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
services.linkding = {
|
||||
enable = true;
|
||||
port = 9090;
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("linkding.service")
|
||||
machine.wait_for_open_port(9090)
|
||||
|
||||
with subtest("Login page loads"):
|
||||
machine.succeed(
|
||||
"curl -sSfL http://127.0.0.1:9090 | grep -i 'linkding'"
|
||||
)
|
||||
|
||||
with subtest("Health endpoint responds"):
|
||||
machine.succeed(
|
||||
"curl -sSf http://127.0.0.1:9090/health"
|
||||
)
|
||||
|
||||
with subtest("linkding-manage works"):
|
||||
machine.succeed(
|
||||
"linkding-manage version"
|
||||
)
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
{
|
||||
lib,
|
||||
buildNpmPackage,
|
||||
fetchFromGitHub,
|
||||
fetchurl,
|
||||
gcc,
|
||||
icu,
|
||||
nix-update-script,
|
||||
nixosTests,
|
||||
pkg-config,
|
||||
python3,
|
||||
sqlite,
|
||||
stdenv,
|
||||
uwsgi,
|
||||
}:
|
||||
let
|
||||
version = "1.45.0";
|
||||
|
||||
python = python3.override {
|
||||
self = python;
|
||||
packageOverrides = final: prev: {
|
||||
django = prev.django_6;
|
||||
};
|
||||
};
|
||||
|
||||
uwsgiWithPython = uwsgi.override {
|
||||
plugins = [ "python3" ];
|
||||
python3 = python;
|
||||
};
|
||||
|
||||
# Compile the SQLite ICU extension for case-insensitive search and ordering.
|
||||
# This mirrors the compile-icu stage in the upstream Dockerfile.
|
||||
icuExtension = stdenv.mkDerivation {
|
||||
pname = "linkding-sqlite-icu";
|
||||
inherit version;
|
||||
|
||||
src = fetchurl {
|
||||
url = "https://www.sqlite.org/src/raw/ext/icu/icu.c?name=91c021c7e3e8bbba286960810fa303295c622e323567b2e6def4ce58e4466e60";
|
||||
name = "icu.c";
|
||||
hash = "sha256-DkELE5p82yZVz0GVFdWWAEU8eo10ob0fqx66Q7rxv+U=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
gcc
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
icu.dev
|
||||
sqlite.dev
|
||||
];
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
gcc -fPIC -shared $src \
|
||||
-I${sqlite.dev}/include \
|
||||
$(pkg-config --libs --cflags icu-uc icu-io) \
|
||||
-o libicu.so
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -Dm755 libicu.so $out/lib/libicu.so
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
in
|
||||
python.pkgs.buildPythonApplication (finalAttrs: {
|
||||
pname = "linkding";
|
||||
inherit version;
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "sissbruecker";
|
||||
repo = "linkding";
|
||||
tag = "v${finalAttrs.version}";
|
||||
hash = "sha256-iGvUKmOPL0akfR52hzSGH6wu06/WP9ygiQ/HxsmrYWg=";
|
||||
};
|
||||
|
||||
__structuredAttrs = true;
|
||||
|
||||
build-system = with python.pkgs; [
|
||||
setuptools
|
||||
];
|
||||
|
||||
dependencies = with python.pkgs; [
|
||||
beautifulsoup4
|
||||
bleach
|
||||
bleach-allowlist
|
||||
django
|
||||
djangorestframework
|
||||
huey
|
||||
markdown
|
||||
mozilla-django-oidc
|
||||
requests
|
||||
waybackpy
|
||||
];
|
||||
|
||||
optional-dependencies = {
|
||||
postgres = with python.pkgs; [ psycopg ];
|
||||
};
|
||||
|
||||
dontCheckRuntimeDeps = true;
|
||||
# Django's runserver re-executes sys.argv[0] via the Python interpreter,
|
||||
# so manage.py must remain a valid Python script and cannot be wrapped in bash.
|
||||
dontWrapPythonPrograms = true;
|
||||
|
||||
pyprojectAppendix = ''
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["bookmarks*"]
|
||||
[tool.setuptools.package-data]
|
||||
bookmarks = ["static/**/*", "styles/**/*", "templates/**/*", "version.txt"]
|
||||
'';
|
||||
|
||||
ui = buildNpmPackage {
|
||||
inherit (finalAttrs) version;
|
||||
|
||||
pname = "${finalAttrs.pname}-ui";
|
||||
src = finalAttrs.src;
|
||||
|
||||
npmDepsHash = "sha256-zUMgl+h0BPm9QzGi1WZG8f0tDoYk8p+Al3q6uEKXqLk=";
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out/bookmarks
|
||||
mv bookmarks/static $out/bookmarks
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
|
||||
postPatch = ''
|
||||
echo "$pyprojectAppendix" >> pyproject.toml
|
||||
|
||||
# Point the SQLite ICU extension to its store path so it is found
|
||||
# regardless of the working directory at runtime.
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'SQLITE_ICU_EXTENSION_PATH = "./libicu.so"' \
|
||||
'SQLITE_ICU_EXTENSION_PATH = "${icuExtension}/lib/libicu.so"'
|
||||
|
||||
# Allow overriding the data directory via an internal environment variable
|
||||
# so that the NixOS module can point it at the mutable state directory
|
||||
# (/var/lib/linkding). The variable name is intentionally NixOS-specific
|
||||
# and not a real linkding option to avoid confusing users.
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))' \
|
||||
'BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))''\nDATA_DIR = os.getenv("_NIXOS_LINKDING_DATA_DIR", os.path.join(os.getcwd(), "data"))'
|
||||
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'"filename": os.path.join(BASE_DIR, "data", "tasks.sqlite3"),' \
|
||||
'"filename": os.path.join(DATA_DIR, "tasks.sqlite3"),'
|
||||
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'"NAME": os.path.join(BASE_DIR, "data", "db.sqlite3"),' \
|
||||
'"NAME": os.path.join(DATA_DIR, "db.sqlite3"),'
|
||||
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'LD_FAVICON_FOLDER = os.path.join(BASE_DIR, "data", "favicons")' \
|
||||
'LD_FAVICON_FOLDER = os.path.join(DATA_DIR, "favicons")'
|
||||
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'LD_PREVIEW_FOLDER = os.path.join(BASE_DIR, "data", "previews")' \
|
||||
'LD_PREVIEW_FOLDER = os.path.join(DATA_DIR, "previews")'
|
||||
|
||||
substituteInPlace bookmarks/settings/base.py \
|
||||
--replace-fail \
|
||||
'LD_ASSET_FOLDER = os.path.join(BASE_DIR, "data", "assets")' \
|
||||
'LD_ASSET_FOLDER = os.path.join(DATA_DIR, "assets")'
|
||||
|
||||
substituteInPlace bookmarks/utils.py \
|
||||
--replace-fail \
|
||||
'import datetime' \
|
||||
'import datetime''\nimport os'
|
||||
|
||||
substituteInPlace bookmarks/utils.py \
|
||||
--replace-fail \
|
||||
'with open("version.txt") as f:' \
|
||||
'with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.txt")) as f:'
|
||||
|
||||
substituteInPlace bookmarks/settings/prod.py \
|
||||
--replace-fail \
|
||||
'with open(os.path.join(BASE_DIR, "data", "secretkey.txt")) as f:' \
|
||||
'with open(os.path.join(DATA_DIR, "secretkey.txt")) as f:'
|
||||
|
||||
substituteInPlace bookmarks/settings/dev.py \
|
||||
--replace-fail \
|
||||
'os.path.join(BASE_DIR, "data", "favicons"),' \
|
||||
'os.path.join(DATA_DIR, "favicons"),'
|
||||
|
||||
substituteInPlace bookmarks/settings/dev.py \
|
||||
--replace-fail \
|
||||
'os.path.join(BASE_DIR, "data", "previews"),' \
|
||||
'os.path.join(DATA_DIR, "previews"),'
|
||||
|
||||
# Patch management commands that bypass Django settings and use hardcoded
|
||||
# relative "data/" paths. Replace them with paths derived from DATA_DIR
|
||||
# via django.conf.settings so the data directory is always correct.
|
||||
substituteInPlace bookmarks/management/commands/generate_secret_key.py \
|
||||
--replace-fail \
|
||||
'from django.core.management.utils import get_random_secret_key' \
|
||||
'from django.conf import settings''\nfrom django.core.management.utils import get_random_secret_key'
|
||||
substituteInPlace bookmarks/management/commands/generate_secret_key.py \
|
||||
--replace-fail \
|
||||
'secret_key_file = os.path.join("data", "secretkey.txt")' \
|
||||
'secret_key_file = os.path.join(settings.DATA_DIR, "secretkey.txt")'
|
||||
substituteInPlace bookmarks/management/commands/migrate_tasks.py \
|
||||
--replace-fail \
|
||||
'import sqlite3' \
|
||||
'import sqlite3''\nfrom django.conf import settings'
|
||||
substituteInPlace bookmarks/management/commands/migrate_tasks.py \
|
||||
--replace-fail \
|
||||
'db = sqlite3.connect(os.path.join("data", "db.sqlite3"))' \
|
||||
'db = sqlite3.connect(os.path.join(settings.DATA_DIR, "db.sqlite3"))'
|
||||
|
||||
# Place the version.txt file inside of the bookmarks package
|
||||
# so that it can be installed by setuptools alongside the package
|
||||
# in the Nix store.
|
||||
mv version.txt bookmarks/version.txt
|
||||
'';
|
||||
|
||||
preBuild = ''
|
||||
cp -r ${finalAttrs.ui}/bookmarks/static/* bookmarks/static
|
||||
'';
|
||||
|
||||
# Collect static files at build time so the result is a pure store path
|
||||
# that can be served directly by the NixOS module without any runtime step.
|
||||
# STATIC_ROOT in base.py defaults to $PWD/static, so the collected files
|
||||
# land there and are picked up by postInstall.
|
||||
postBuild = ''
|
||||
mkdir data
|
||||
${python.interpreter} manage.py collectstatic --no-input
|
||||
'';
|
||||
|
||||
postInstall =
|
||||
let
|
||||
pythonPath = python.pkgs.makePythonPath finalAttrs.passthru.dependencies;
|
||||
in
|
||||
''
|
||||
mkdir $out/bin
|
||||
|
||||
cp -r static/* $out/${python.sitePackages}/bookmarks/static
|
||||
|
||||
cp ./manage.py $out/bin/.manage.py
|
||||
chmod +x $out/bin/.manage.py
|
||||
|
||||
makeWrapper $out/bin/.manage.py $out/bin/linkding \
|
||||
--prefix PYTHONPATH : "${pythonPath}:$out/${python.sitePackages}"
|
||||
|
||||
# Bootstrap script mirroring the upstream bootstrap.sh: creates data
|
||||
# directories and runs Django management commands to initialize a fresh
|
||||
# linkding installation. The linkding binary is referenced by its
|
||||
# absolute store path so the script works without PATH manipulation.
|
||||
cat > $out/bin/linkding-bootstrap << EOF
|
||||
#!/bin/sh
|
||||
DATA_DIR="\''${_NIXOS_LINKDING_DATA_DIR:-data}"
|
||||
mkdir -p "\$DATA_DIR"/favicons "\$DATA_DIR"/previews "\$DATA_DIR"/assets
|
||||
$out/bin/linkding generate_secret_key
|
||||
$out/bin/linkding migrate
|
||||
$out/bin/linkding enable_wal
|
||||
$out/bin/linkding create_initial_superuser
|
||||
$out/bin/linkding migrate_tasks
|
||||
EOF
|
||||
chmod +x $out/bin/linkding-bootstrap
|
||||
'';
|
||||
|
||||
passthru = {
|
||||
inherit
|
||||
python
|
||||
icuExtension
|
||||
uwsgiWithPython
|
||||
;
|
||||
tests = {
|
||||
inherit (nixosTests) linkding linkding-postgres;
|
||||
};
|
||||
updateScript = nix-update-script {
|
||||
extraArgs = [
|
||||
"--subpackage"
|
||||
"ui"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Self-hosted bookmark manager designed to be minimal, fast, and easy to set up";
|
||||
homepage = "https://linkding.link/";
|
||||
changelog = "https://github.com/sissbruecker/linkding/releases/tag/v${finalAttrs.version}";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [
|
||||
squat
|
||||
];
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
})
|
||||
Reference in New Issue
Block a user