mirror of
https://github.com/nix-community/impermanence
synced 2024-11-10 13:54:18 +00:00
5b3345400c
Use bindfs to create bind mounts for directories instead of symlinking them. This should be less problematic for many applications, since bind mounts are much more transparent. This sets up the bind mounts in the activation script, before any writes are done by home-manager, then tears them down again afterwards. The bind mounts are then handled by individual systemd services, since they're long-running fuse processes and need to be managed as such. This also means we leverage home-manager's mechanism for deciding which user services should be active after a switch to a new generation, and don't have to bother with cleaning up old leftover fuse processes. NOTE: All unmounts done in the activation script are put into a function which is run either on error, or right before home-manager starts / reloads systemd units. This will conflict with other attempts to add traps on ERR, but this isn't currently done upstream.
217 lines
6.9 KiB
Nix
217 lines
6.9 KiB
Nix
{ pkgs, config, lib, ... }:
|
|
|
|
with lib;
|
|
let
|
|
cfg = config.home.persistence;
|
|
|
|
persistentStoragePaths = attrNames cfg;
|
|
|
|
inherit (pkgs.callPackage ./lib.nix { }) splitPath dirListToPath concatPaths sanitizeName;
|
|
in
|
|
{
|
|
options = {
|
|
|
|
home.persistence = mkOption {
|
|
default = { };
|
|
type = with types; attrsOf (
|
|
submodule {
|
|
options =
|
|
{
|
|
directories = mkOption {
|
|
type = with types; listOf str;
|
|
default = [ ];
|
|
};
|
|
|
|
files = mkOption {
|
|
type = with types; listOf str;
|
|
default = [ ];
|
|
};
|
|
|
|
removePrefixDirectory = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
};
|
|
|
|
config = {
|
|
home.file =
|
|
let
|
|
link = file:
|
|
pkgs.runCommand
|
|
"${sanitizeName file}"
|
|
{ }
|
|
"ln -s '${file}' $out";
|
|
|
|
mkLinkNameValuePair = persistentStoragePath: file: {
|
|
name =
|
|
if cfg.${persistentStoragePath}.removePrefixDirectory then
|
|
dirListToPath (tail (splitPath [ file ]))
|
|
else
|
|
file;
|
|
value = { source = link (concatPaths [ persistentStoragePath file ]); };
|
|
};
|
|
|
|
mkLinksToPersistentStorage = persistentStoragePath:
|
|
listToAttrs (map
|
|
(mkLinkNameValuePair persistentStoragePath)
|
|
(cfg.${persistentStoragePath}.files)
|
|
);
|
|
in
|
|
foldl' recursiveUpdate { } (map mkLinksToPersistentStorage persistentStoragePaths);
|
|
|
|
systemd.user.services =
|
|
let
|
|
mkBindMountService = persistentStoragePath: dir:
|
|
let
|
|
mountDir =
|
|
if cfg.${persistentStoragePath}.removePrefixDirectory then
|
|
dirListToPath (tail (splitPath [ dir ]))
|
|
else
|
|
dir;
|
|
targetDir = concatPaths [ persistentStoragePath dir ];
|
|
mountPoint = concatPaths [ config.home.homeDirectory mountDir ];
|
|
name = "bindMount-${sanitizeName targetDir}";
|
|
startScript = pkgs.writeShellScript name ''
|
|
set -eu
|
|
if ! ${pkgs.utillinux}/bin/mount | ${pkgs.gnugrep}/bin/grep -E '${mountPoint}( |/)'; then
|
|
${pkgs.bindfs}/bin/bindfs -f --no-allow-other "${targetDir}" "${mountPoint}"
|
|
else
|
|
echo "There is already an active mount at or below ${mountPoint}!" >&2
|
|
exit 1
|
|
fi
|
|
'';
|
|
stopScript = pkgs.writeShellScript "unmount-${name}" ''
|
|
fusermount -uz "${mountPoint}"
|
|
'';
|
|
in
|
|
{
|
|
inherit name;
|
|
value = {
|
|
Unit = {
|
|
Description = "Bind mount ${targetDir} at ${mountPoint}";
|
|
PartOf = [ "graphical-session-pre.target" ];
|
|
|
|
# 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}";
|
|
};
|
|
};
|
|
};
|
|
|
|
mkBindMountServicesForPath = persistentStoragePath:
|
|
listToAttrs (map
|
|
(mkBindMountService persistentStoragePath)
|
|
cfg.${persistentStoragePath}.directories
|
|
);
|
|
in
|
|
builtins.foldl'
|
|
recursiveUpdate
|
|
{ }
|
|
(map mkBindMountServicesForPath persistentStoragePaths);
|
|
|
|
home.activation =
|
|
let
|
|
dag = config.lib.dag;
|
|
|
|
mkBindMount = persistentStoragePath: dir:
|
|
let
|
|
mountDir =
|
|
if cfg.${persistentStoragePath}.removePrefixDirectory then
|
|
dirListToPath (tail (splitPath [ dir ]))
|
|
else
|
|
dir;
|
|
targetDir = concatPaths [ persistentStoragePath dir ];
|
|
mountPoint = concatPaths [ config.home.homeDirectory mountDir ];
|
|
systemctl = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)} ${config.systemd.user.systemctlPath}";
|
|
in
|
|
''
|
|
if [[ ! -e "${targetDir}" ]]; then
|
|
mkdir -p "${targetDir}"
|
|
fi
|
|
if [[ ! -e "${mountPoint}" ]]; then
|
|
mkdir -p "${mountPoint}"
|
|
fi
|
|
if ${pkgs.utillinux}/bin/mount | grep "${mountPoint}"; then
|
|
if ! ${pkgs.utillinux}/bin/mount | grep "${mountPoint}" | grep "${targetDir}"; then
|
|
# The target directory changed, so we need to remount
|
|
echo "remounting ${mountPoint}"
|
|
${systemctl} --user stop bindMount-${sanitizeName targetDir}
|
|
${pkgs.bindfs}/bin/bindfs --no-allow-other "${targetDir}" "${mountPoint}"
|
|
mountedPaths["${mountPoint}"]=1
|
|
fi
|
|
else
|
|
${pkgs.bindfs}/bin/bindfs --no-allow-other "${targetDir}" "${mountPoint}"
|
|
mountedPaths["${mountPoint}"]=1
|
|
fi
|
|
'';
|
|
|
|
mkBindMountsForPath = persistentStoragePath:
|
|
concatMapStrings
|
|
(mkBindMount persistentStoragePath)
|
|
cfg.${persistentStoragePath}.directories;
|
|
|
|
mkUnmount = persistentStoragePath: dir:
|
|
let
|
|
mountDir =
|
|
if cfg.${persistentStoragePath}.removePrefixDirectory then
|
|
dirListToPath (tail (splitPath [ dir ]))
|
|
else
|
|
dir;
|
|
mountPoint = concatPaths [ config.home.homeDirectory mountDir ];
|
|
in
|
|
''
|
|
if [[ -n ''${mountedPaths["${mountPoint}"]+x} ]]; then
|
|
fusermount -u "${mountPoint}"
|
|
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
|
|
'';
|
|
};
|
|
};
|
|
|
|
}
|