modules/bindmounts: add module for home bind mounts
This commit is contained in:
parent
3fb0e34ba0
commit
fec66eeb42
2 changed files with 286 additions and 3 deletions
|
@ -1,7 +1,33 @@
|
||||||
{ lib }:
|
{ lib }:
|
||||||
lib.makeExtensible (self:
|
lib.makeExtensible (self:
|
||||||
{
|
|
||||||
flattenListSet = imports: (lib.lists.flatten (builtins.concatLists (builtins.attrValues imports)));
|
with lib;
|
||||||
flattenSetList = attrSet: (builtins.mapAttrs (name: value: lib.lists.flatten value) attrSet);
|
rec {
|
||||||
|
flattenListSet = imports: (flatten (concatLists (attrValues imports)));
|
||||||
|
flattenSetList = attrSet: (mapAttrs (name: value: flatten value) attrSet);
|
||||||
|
|
||||||
|
|
||||||
|
# ["/home/user/" "/.screenrc"] -> ["home" "user" ".screenrc"]
|
||||||
|
splitPath = paths:
|
||||||
|
(filter
|
||||||
|
(s: builtins.typeOf s == "string" && s != "")
|
||||||
|
(concatMap (builtins.split "/") paths)
|
||||||
|
);
|
||||||
|
|
||||||
|
# ["home" "user" ".screenrc"] -> "home/user/.screenrc"
|
||||||
|
dirListToPath = dirList: (concatStringsSep "/" dirList);
|
||||||
|
|
||||||
|
# ["/home/user/" "/.screenrc"] -> "/home/user/.screenrc"
|
||||||
|
concatPaths = paths:
|
||||||
|
let
|
||||||
|
prefix = optionalString (hasPrefix "/" (head paths)) "/";
|
||||||
|
path = dirListToPath (splitPath paths);
|
||||||
|
in
|
||||||
|
prefix + path;
|
||||||
|
|
||||||
|
sanitizeName = name:
|
||||||
|
replaceStrings
|
||||||
|
[ "." ] [ "" ]
|
||||||
|
(strings.sanitizeDerivationName (removePrefix "/" name));
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
257
users/modules/bindmounts.nix
Normal file
257
users/modules/bindmounts.nix
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
{ pkgs, config, lib, ... }:
|
||||||
|
|
||||||
|
# Modified from https://github.com/nix-community/impermanence/blob/master/home-manager.nix
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
with lib.our;
|
||||||
|
let
|
||||||
|
cfg = config.home.bindmounts;
|
||||||
|
|
||||||
|
persistentStoragePaths = attrNames cfg;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
|
||||||
|
home.bindmounts = mkOption {
|
||||||
|
default = { };
|
||||||
|
type = with types; attrsOf (
|
||||||
|
submodule ({ name, ... }: {
|
||||||
|
options =
|
||||||
|
{
|
||||||
|
directories = mkOption {
|
||||||
|
type = with types; listOf (submodule {
|
||||||
|
options = {
|
||||||
|
source = mkOption {
|
||||||
|
type = with types; str;
|
||||||
|
};
|
||||||
|
|
||||||
|
target = mkOption {
|
||||||
|
type = with types; str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
A list of directories and target locations that you wish to bind-mount from the initial source.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
allowOther = mkOption {
|
||||||
|
type = with types; nullOr bool;
|
||||||
|
default = null;
|
||||||
|
example = true;
|
||||||
|
apply = x:
|
||||||
|
if x == null then
|
||||||
|
warn ''
|
||||||
|
home.bindmounts."${name}".allowOther not set; assuming 'false'.
|
||||||
|
See https://github.com/nix-community/impermanence#home-manager for more info.
|
||||||
|
''
|
||||||
|
false
|
||||||
|
else
|
||||||
|
x;
|
||||||
|
description = ''
|
||||||
|
Whether to allow other users, such as
|
||||||
|
<literal>root</literal>, access to files through the
|
||||||
|
bind mounted directories listed in
|
||||||
|
<literal>directories</literal>. Requires the NixOS
|
||||||
|
configuration parameter
|
||||||
|
<literal>programs.fuse.userAllowOther</literal> to
|
||||||
|
be <literal>true</literal>.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
systemd.user.services =
|
||||||
|
let
|
||||||
|
mkBindMountService = persistentStoragePath: dir:
|
||||||
|
let
|
||||||
|
inherit (dir) source target;
|
||||||
|
targetDir = escapeShellArg (concatPaths [ persistentStoragePath source ]);
|
||||||
|
mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory target ]);
|
||||||
|
name = "bindMount-${sanitizeName targetDir}";
|
||||||
|
bindfsOptions = concatStringsSep "," (
|
||||||
|
optional (!cfg.${persistentStoragePath}.allowOther) "no-allow-other"
|
||||||
|
++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}"
|
||||||
|
);
|
||||||
|
bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions);
|
||||||
|
bindfs = "bindfs -f" + bindfsOptionFlag;
|
||||||
|
startScript = pkgs.writeShellScript name ''
|
||||||
|
set -eu
|
||||||
|
if ! mount | grep -F ${mountPoint}' ' && ! mount | grep -F ${mountPoint}/; then
|
||||||
|
mkdir -p ${mountPoint}
|
||||||
|
${bindfs} ${targetDir} ${mountPoint}
|
||||||
|
else
|
||||||
|
echo "There is already an active mount at or below ${mountPoint}!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
stopScript = pkgs.writeShellScript "unmount-${name}" ''
|
||||||
|
set -eu
|
||||||
|
triesLeft=6
|
||||||
|
while (( triesLeft > 0 )); do
|
||||||
|
if fusermount -u ${mountPoint}; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
(( triesLeft-- ))
|
||||||
|
if (( triesLeft == 0 )); then
|
||||||
|
echo "Couldn't perform regular unmount of ${mountPoint}. Attempting lazy unmount."
|
||||||
|
fusermount -uz ${mountPoint}
|
||||||
|
else
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
value = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Bind mount ${targetDir} at ${mountPoint}";
|
||||||
|
|
||||||
|
# Don't restart the unit, it could corrupt data and
|
||||||
|
# crash programs currently reading from the mount.
|
||||||
|
X-RestartIfChanged = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
Install.WantedBy = [ "default.target" ];
|
||||||
|
|
||||||
|
Service = {
|
||||||
|
ExecStart = "${startScript}";
|
||||||
|
ExecStop = "${stopScript}";
|
||||||
|
Environment = "PATH=${makeBinPath [ pkgs.coreutils pkgs.utillinux pkgs.gnugrep pkgs.bindfs ]}:/run/wrappers/bin";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkBindMountServicesForPath = persistentStoragePath:
|
||||||
|
listToAttrs (map
|
||||||
|
(mkBindMountService persistentStoragePath)
|
||||||
|
cfg.${persistentStoragePath}.directories
|
||||||
|
);
|
||||||
|
in
|
||||||
|
builtins.foldl'
|
||||||
|
recursiveUpdate
|
||||||
|
{ }
|
||||||
|
(map mkBindMountServicesForPath persistentStoragePaths);
|
||||||
|
|
||||||
|
home.activation =
|
||||||
|
let
|
||||||
|
dag = config.lib.dag;
|
||||||
|
|
||||||
|
# The name of the activation script entry responsible for
|
||||||
|
# reloading systemd user services. The name was initially
|
||||||
|
# `reloadSystemD` but has been changed to `reloadSystemd`.
|
||||||
|
reloadSystemd =
|
||||||
|
if config.home.activation ? reloadSystemD then
|
||||||
|
"reloadSystemD"
|
||||||
|
else
|
||||||
|
"reloadSystemd";
|
||||||
|
|
||||||
|
mkBindMount = persistentStoragePath: dir:
|
||||||
|
let
|
||||||
|
inherit (dir) source target;
|
||||||
|
targetDir = escapeShellArg (concatPaths [ persistentStoragePath source ]);
|
||||||
|
mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory target ]);
|
||||||
|
mount = "${pkgs.utillinux}/bin/mount";
|
||||||
|
bindfsOptions = concatStringsSep "," (
|
||||||
|
optional (!cfg.${persistentStoragePath}.allowOther) "no-allow-other"
|
||||||
|
++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}"
|
||||||
|
);
|
||||||
|
bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions);
|
||||||
|
bindfs = "${pkgs.bindfs}/bin/bindfs" + bindfsOptionFlag;
|
||||||
|
systemctl = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)} ${config.systemd.user.systemctlPath}";
|
||||||
|
in
|
||||||
|
''
|
||||||
|
mkdir -p ${targetDir}
|
||||||
|
mkdir -p ${mountPoint}
|
||||||
|
if ${mount} | grep -F ${mountPoint}' ' >/dev/null; then
|
||||||
|
if ! ${mount} | grep -F ${mountPoint}' ' | grep -F bindfs; then
|
||||||
|
if ! ${mount} | grep -F ${mountPoint}' ' | grep -F ${targetDir}' ' >/dev/null; then
|
||||||
|
# The target directory changed, so we need to remount
|
||||||
|
echo "remounting ${mountPoint}"
|
||||||
|
${systemctl} --user stop bindMount-${sanitizeName targetDir}
|
||||||
|
${bindfs} ${targetDir} ${mountPoint}
|
||||||
|
mountedPaths[${mountPoint}]=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif ${mount} | grep -F ${mountPoint}/ >/dev/null; then
|
||||||
|
echo "Something is mounted below ${mountPoint}, not creating bind mount to ${targetDir}" >&2
|
||||||
|
else
|
||||||
|
${bindfs} ${targetDir} ${mountPoint}
|
||||||
|
mountedPaths[${mountPoint}]=1
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
mkBindMountsForPath = persistentStoragePath:
|
||||||
|
concatMapStrings
|
||||||
|
(mkBindMount persistentStoragePath)
|
||||||
|
cfg.${persistentStoragePath}.directories;
|
||||||
|
|
||||||
|
mkUnmount = persistentStoragePath: dir:
|
||||||
|
let
|
||||||
|
inherit (dir) target;
|
||||||
|
mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory target ]);
|
||||||
|
in
|
||||||
|
''
|
||||||
|
if [[ -n ''${mountedPaths[${mountPoint}]+x} ]]; then
|
||||||
|
triesLeft=3
|
||||||
|
while (( triesLeft > 0 )); do
|
||||||
|
if fusermount -u ${mountPoint}; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
(( triesLeft-- ))
|
||||||
|
if (( triesLeft == 0 )); then
|
||||||
|
echo "Couldn't perform regular unmount of ${mountPoint}. Attempting lazy unmount."
|
||||||
|
fusermount -uz ${mountPoint} || true
|
||||||
|
else
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
mkUnmountsForPath = persistentStoragePath:
|
||||||
|
concatMapStrings
|
||||||
|
(mkUnmount persistentStoragePath)
|
||||||
|
cfg.${persistentStoragePath}.directories;
|
||||||
|
|
||||||
|
in
|
||||||
|
mkIf (any (path: cfg.${path}.directories != [ ]) persistentStoragePaths) {
|
||||||
|
createAndMountPersistentStoragePaths =
|
||||||
|
dag.entryBefore
|
||||||
|
[ "writeBoundary" ]
|
||||||
|
''
|
||||||
|
declare -A mountedPaths
|
||||||
|
${(concatMapStrings mkBindMountsForPath persistentStoragePaths)}
|
||||||
|
'';
|
||||||
|
|
||||||
|
unmountPersistentStoragePaths =
|
||||||
|
dag.entryBefore
|
||||||
|
[ "createAndMountPersistentStoragePaths" ]
|
||||||
|
''
|
||||||
|
unmountBindMounts() {
|
||||||
|
${concatMapStrings mkUnmountsForPath persistentStoragePaths}
|
||||||
|
}
|
||||||
|
# Run the unmount function on error to clean up stray
|
||||||
|
# bind mounts
|
||||||
|
trap "unmountBindMounts" ERR
|
||||||
|
'';
|
||||||
|
|
||||||
|
runUnmountPersistentStoragePaths =
|
||||||
|
dag.entryBefore
|
||||||
|
[ reloadSystemd ]
|
||||||
|
''
|
||||||
|
unmountBindMounts
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue