diff --git a/home-manager.nix b/home-manager.nix index 1d8b8c7..f9cc618 100644 --- a/home-manager.nix +++ b/home-manager.nix @@ -2,49 +2,98 @@ with lib; let - cfg = config.home.persistence; - - persistentStorageNames = attrNames cfg; - - getDirPath = v: if isString v then v else v.directory; - getDirMethod = v: v.method or "bindfs"; - isBindfs = v: (getDirMethod v) == "bindfs"; - isSymlink = v: (getDirMethod v) == "symlink"; + cfg = config.impermanence; + persistentStorages = builtins.mapAttrs + (_: persistentStorage: + let + storage = persistentStorage // { + mkDirCfg = getHomeDirCfg { + inherit storage pkgs; + inherit (config.home) homeDirectory; + }; + }; + in + storage + ) + config.home.persistence; + + persistentStoragesList = builtins.attrValues persistentStorages; + + getPath = v: if isString v then v else v.directory or v.file; + isBindfs = v: v.method == "bindfs"; + isSymlink = v: v.method == "symlink"; + + orderedDirs = lib.pipe persistentStorages [ + (lib.attrsets.mapAttrsToList (persistentStorageName: storage: builtins.map + (dir: { + inherit persistentStorageName storage dir; + inherit (dir) method; + path = dir.directory; + dirCfg = storage.mkDirCfg dir.directory; + }) + storage.directories + )) + builtins.concatLists + (builtins.sort (a: b: a.path < b.path)) + ]; + + dirsByHomeMountpoint = lib.pipe orderedDirs [ + (builtins.map (e: { + name = e.dirCfg.mountPoint; + value = e; + })) + builtins.listToAttrs + ]; + + orderedFiles = lib.pipe persistentStorages [ + (lib.attrsets.mapAttrsToList (persistentStorageName: storage: builtins.map + (file: { + inherit persistentStorageName storage file; + inherit (file) method; + path = file.file; + dirCfg = storage.mkDirCfg file.file; + }) + storage.files + )) + builtins.concatLists + (builtins.sort (a: b: a.path < b.path)) + ]; inherit (pkgs.callPackage ./lib.nix { }) - splitPath - dirListToPath - concatPaths + getHomeDirCfg sanitizeName ; - mount = "${pkgs.util-linux}/bin/mount"; - unmountScript = mountPoint: tries: sleep: '' - triesLeft=${toString tries} - if ${mount} | grep -F ${mountPoint}' ' >/dev/null; then - 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} - else - sleep ${toString sleep} - fi - fi - done - fi - ''; + scripts = pkgs.callPackage ./scripts { systemctl = config.systemd.user.systemctlPath; }; in { options = { + impermanence.defaultDirectoryMethod = lib.mkOption { + type = with types; enum [ "bindfs" "symlink" "external" ]; + default = "bindfs"; + description = '' + The linking method that should be used by default for directories: + - `bindfs` is the default (for non-root user) and works for most use cases, + - `symlink` is there for the programs misbehaving with `bindfs`, + - `external` allows you to handle the setup in your own code, this is default for `root`, + ''; + }; + + impermanence.defaultFileMethod = lib.mkOption { + type = with types; enum [ "symlink" "external" ]; + default = "symlink"; + description = '' + The linking method that should be used for files: + - `symlink` is the default (for non-root user), + - `external` allows you to handle the setup in your own code, this is default for `root`, + ''; + }; + home.persistence = mkOption { default = { }; type = with types; attrsOf ( - submodule ({ name, ... }: { + submodule ({ name, ... }@persistenceArgs: { options = { persistentStoragePath = mkOption { @@ -56,26 +105,51 @@ in ''; }; + defaultDirectoryMethod = lib.mkOption { + type = with types; enum [ "bindfs" "symlink" "external" ]; + default = cfg.defaultDirectoryMethod; + description = '' + The linking method that should be used for directories, + see `impermanence.defaultDirectoryMethod` for details. + ''; + }; + + defaultFileMethod = lib.mkOption { + type = with types; enum [ "symlink" "external" ]; + default = cfg.defaultFileMethod; + description = '' + The linking method that should be used for files, + see `impermanence.defaultFileMethod` for details. + ''; + }; + directories = mkOption { - type = with types; listOf (either str (submodule { - options = { - directory = mkOption { - type = str; - default = null; - description = "The directory path to be linked."; + type = + let + directoryType = types.submodule { + options = { + directory = mkOption { + type = with types; str; + default = null; + description = "The directory path to be linked."; + }; + method = mkOption { + type = with types; enum [ "bindfs" "symlink" "external" ]; + default = persistenceArgs.config.defaultDirectoryMethod; + description = '' + The linking method that should be used for this directory, + see `impermanence.defaultDirectoryMethod` for details. + ''; + }; + }; }; - method = mkOption { - type = types.enum [ "bindfs" "symlink" ]; - default = "bindfs"; - description = '' - The linking method that should be used for this - directory. bindfs is the default and works for most use - cases, however some programs may behave better with - symlinks. - ''; - }; - }; - })); + in + with types; listOf ( + coercedTo + (either str directoryType) + (value: if builtins.isString value then { directory = value; } else value) + directoryType + ); default = [ ]; example = [ "Downloads" @@ -101,10 +175,39 @@ in }; files = mkOption { - type = with types; listOf str; + type = + let + fileType = types.submodule { + options = { + file = mkOption { + type = with types; str; + default = null; + description = "The file path to be linked."; + }; + method = mkOption { + type = with types; enum [ "symlink" "external" ]; + default = persistenceArgs.config.defaultFileMethod; + description = '' + The linking method that should be used for this file, + see `impermanence.defaultFileMethod` for details. + ''; + }; + }; + }; + in + with types; listOf ( + coercedTo + (either str fileType) + (value: if builtins.isString value then { file = value; } else value) + fileType + ); default = [ ]; example = [ ".screenrc" + { + directory = ".bashrc"; + method = "external"; + } ]; description = '' A list of files in your home directory you want to @@ -206,62 +309,36 @@ in { } "ln -s '${file}' $out"; - mkLinkNameValuePair = persistentStorageName: fileOrDir: { - name = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ fileOrDir ])) - else - fileOrDir; - value = { source = link (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath fileOrDir ]); }; - }; + mkLinkNameValuePair = storage: fileOrDir: + let + dirCfg = storage.mkDirCfg fileOrDir; + in + { + name = dirCfg.mountDir; + value = { source = link dirCfg.targetDir; }; + }; - mkLinksToPersistentStorage = persistentStorageName: + mkLinksToPersistentStorage = storage: listToAttrs (map - (mkLinkNameValuePair persistentStorageName) - (map getDirPath (cfg.${persistentStorageName}.files ++ - (filter isSymlink cfg.${persistentStorageName}.directories))) + (mkLinkNameValuePair storage) + (map getPath (builtins.filter isSymlink (storage.files ++ storage.directories))) ); in - foldl' recursiveUpdate { } (map mkLinksToPersistentStorage persistentStorageNames); + foldl' recursiveUpdate { } (map mkLinksToPersistentStorage persistentStoragesList); systemd.user.services = let - mkBindMountService = persistentStorageName: dir: + mkBindMountService = storage: dir: let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - targetDir = escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath dir ]); - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); - name = "bindMount-${sanitizeName targetDir}"; - bindfsOptions = concatStringsSep "," ( - optional (!cfg.${persistentStorageName}.allowOther) "no-allow-other" - ++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}" - ); - bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions); - bindfs = "bindfs" + bindfsOptionFlag; - startScript = pkgs.writeShellScript name '' - set -eu - if ! mount | grep -F ${mountPoint}' ' && ! mount | grep -F ${mountPoint}/; then - mkdir -p ${mountPoint} - exec ${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 - ${unmountScript mountPoint 6 5} - ''; + dirCfg = storage.mkDirCfg dir; + name = dirCfg.unitName; in { + # try to keep those changes in sync with scripts scripts/hm-bind-mount-activation.bash:bindfs-run() inherit name; value = { Unit = { - Description = "Bind mount ${targetDir} at ${mountPoint}"; + Description = "Bind mount ${dirCfg.escaped.targetDir} at ${dirCfg.escaped.mountPoint}"; # Don't restart the unit, it could corrupt data and # crash programs currently reading from the mount. @@ -278,34 +355,55 @@ in "sockets.target" "timers.target" ]; + + After = lib.pipe dir [ + # generate all system path prefixes + (lib.strings.splitString "/") + (pcs: builtins.map (i: lib.lists.sublist 0 i pcs) (lib.lists.range 0 (builtins.length pcs - 1))) + (builtins.map (lib.strings.concatStringsSep "/")) + + # try to find an existing mountpoint for each generated path, skip those not found + (builtins.map (path: let inherit (storage.mkDirCfg path) mountPoint; in dirsByHomeMountpoint.${mountPoint} or null)) + (builtins.filter (e: e != null)) + + (builtins.map (orderedDir: "${orderedDir.dirCfg.unitName}.service")) + ]; + + ConditionPathIsMountPoint = [ "!${dirCfg.mountPoint}" ]; }; Install.WantedBy = [ "paths.target" ]; Service = { Type = "forking"; - ExecStart = "${startScript}"; - ExecStop = "${stopScript}"; - Environment = "PATH=${makeBinPath [ pkgs.coreutils pkgs.util-linux pkgs.gnugrep pkgs.bindfs ]}:/run/wrappers/bin"; + ExecStart = escapeShellArgs ([ + (lib.getExe scripts.hm.bind-mount-service) + dirCfg.targetDir + dirCfg.mountPoint + ] ++ dirCfg.runBindfsArgs); + + ExecStop = escapeShellArgs [ + (lib.getExe scripts.hm.unmount) + dirCfg.mountPoint + "6" + "5" + ]; + Slice = "background.slice"; }; }; }; - mkBindMountServicesForPath = persistentStorageName: + mkBindMountServicesForPath = storage: listToAttrs (map - (mkBindMountService persistentStorageName) - (map getDirPath (filter isBindfs cfg.${persistentStorageName}.directories)) + (mkBindMountService storage) + (map getPath (filter isBindfs storage.directories)) ); in - builtins.foldl' - recursiveUpdate - { } - (map mkBindMountServicesForPath persistentStorageNames); + builtins.foldl' recursiveUpdate { } (map mkBindMountServicesForPath persistentStoragesList); home.activation = let dag = config.lib.dag; - mount = "${pkgs.util-linux}/bin/mount"; # The name of the activation script entry responsible for # reloading systemd user services. The name was initially @@ -316,151 +414,111 @@ in else "reloadSystemd"; - mkBindMount = persistentStorageName: dir: + mkBindMount = orderedDir: let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - targetDir = escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath dir ]); - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); - bindfsOptions = concatStringsSep "," ( - optional (!cfg.${persistentStorageName}.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}"; + dirCfg = orderedDir.dirCfg; + scriptArgs = [ + (lib.getExe scripts.hm.bind-mount-activation) + dirCfg.mountPoint + dirCfg.targetDir + dirCfg.unitName + ] ++ dirCfg.runBindfsArgs; 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 + # ${dirCfg.escaped.mountPoint} <- ${dirCfg.escaped.targetDir} + while read -r line; do + echo "$line" + if [[ "$line" == ${escapeShellArg scripts.outputPrefix}ERROR:* ]] ; then + bindMountErrors+=("''${line#${escapeShellArg scripts.outputPrefix}ERROR:}") + elif [[ "$line" == ${escapeShellArg scripts.outputPrefix}* ]] ; then + mountedPaths["''${line#${escapeShellArg scripts.outputPrefix}}"]="1" + fi + done < <(${escapeShellArgs scriptArgs} || echo ${escapeShellArg scripts.outputPrefix}ERROR:$? ) ''; - mkBindMountsForPath = persistentStorageName: - concatMapStrings - (mkBindMount persistentStorageName) - (map getDirPath (filter isBindfs cfg.${persistentStorageName}.directories)); - - mkUnmount = persistentStorageName: dir: + mkUnmount = orderedDir: let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); + dirCfg = orderedDir.dirCfg; in '' - if [[ -n ''${mountedPaths[${mountPoint}]+x} ]]; then - ${unmountScript mountPoint 3 1} + # ${dirCfg.escaped.mountPoint} <- ${dirCfg.escaped.targetDir} + if [[ -n "''${mountedPaths[${dirCfg.escaped.mountPoint}]+x}" ]]; then + ${lib.getExe scripts.hm.unmount} ${dirCfg.escaped.mountPoint} 3 1 fi ''; - mkUnmountsForPath = persistentStorageName: - concatMapStrings - (mkUnmount persistentStorageName) - (map getDirPath (filter isBindfs cfg.${persistentStorageName}.directories)); - - mkLinkCleanup = persistentStorageName: dir: + mkLinkCleanup = orderedDir: let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); + dirCfg = orderedDir.dirCfg; in '' + # ${dirCfg.escaped.mountPoint} <- ${dirCfg.escaped.targetDir} # Unmount if it's mounted. Ensures smooth transition: bindfs -> symlink - ${unmountScript mountPoint 3 1} + ${lib.getExe scripts.hm.unmount} ${dirCfg.escaped.mountPoint} 3 1 # If it is a directory and it's empty - if [ -d ${mountPoint} ] && [ -z "$(ls -A ${mountPoint})" ]; then - echo "Removing empty directory ${mountPoint}" - rm -d ${mountPoint} + if [ -d ${dirCfg.escaped.mountPoint} ] && [ -z "$(ls -A ${dirCfg.escaped.mountPoint})" ]; then + echo "Removing empty directory ${dirCfg.mountPoint}" + rm -d ${dirCfg.escaped.mountPoint} fi ''; - mkLinkCleanupForPath = persistentStorageName: - concatMapStrings - (mkLinkCleanup persistentStorageName) - (map getDirPath (filter isSymlink cfg.${persistentStorageName}.directories)); + mkDirScripts = { filterFn, mapFn, reverse ? false }: lib.pipe orderedDirs [ + (builtins.filter (orderedDir: filterFn orderedDir.dir)) + (builtins.map (orderedDir: mapFn orderedDir)) + (if reverse then lib.lists.reverseList else (x: x)) + (x: if x == [ ] then [ ": # nothing returned frmo mkDirScripts" ] else x) + (builtins.concatStringsSep "\n") + ]; + in + { + # Clean up existing empty directories in the way of links + impermanenceCleanEmptyLinkTargets = + dag.entryBefore + [ "checkLinkTargets" ] + (mkDirScripts { filterFn = isSymlink; mapFn = mkLinkCleanup; reverse = true; }); + + impermanenceCreateAndMountPersistentStoragePaths = + dag.entryBefore + [ "writeBoundary" ] + '' + declare -A mountedPaths + bindMountErrors=() + ${mkDirScripts {filterFn = isBindfs; mapFn = mkBindMount; reverse = false; }} + test "''${#bindMountErrors[@]}" == 0 + ''; + impermanenceUnmountPersistentStoragePaths = + dag.entryBefore + [ "impermanenceCreateAndMountPersistentStoragePaths" ] + '' + unmountBindMounts() { + ${mkDirScripts {filterFn = isBindfs; mapFn = mkUnmount; reverse = true; }} + } - in - mkMerge [ - (mkIf (any (path: (filter isSymlink cfg.${path}.directories) != [ ]) persistentStorageNames) { - # Clean up existing empty directories in the way of links - cleanEmptyLinkTargets = - dag.entryBefore - [ "checkLinkTargets" ] - '' - ${concatMapStrings mkLinkCleanupForPath persistentStorageNames} - ''; - }) - (mkIf (any (path: (filter isBindfs cfg.${path}.directories) != [ ]) persistentStorageNames) { - createAndMountPersistentStoragePaths = - dag.entryBefore - [ "writeBoundary" ] - '' - declare -A mountedPaths - ${(concatMapStrings mkBindMountsForPath persistentStorageNames)} - ''; - - unmountPersistentStoragePaths = - dag.entryBefore - [ "createAndMountPersistentStoragePaths" ] - '' - PATH=$PATH:/run/wrappers/bin - unmountBindMounts() { - ${concatMapStrings mkUnmountsForPath persistentStorageNames} - } - - # Run the unmount function on error to clean up stray - # bind mounts - trap "unmountBindMounts" ERR - ''; - - runUnmountPersistentStoragePaths = - dag.entryBefore - [ reloadSystemd ] - '' - unmountBindMounts - ''; - }) - (mkIf (any (path: (cfg.${path}.files != [ ]) || ((filter isSymlink cfg.${path}.directories) != [ ])) persistentStorageNames) { - createTargetFileDirectories = - dag.entryBefore - [ "writeBoundary" ] - (concatMapStrings - (persistentStorageName: - concatMapStrings - (targetFilePath: '' - mkdir -p ${escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath (dirOf targetFilePath) ])} - '') - (map getDirPath (cfg.${persistentStorageName}.files ++ (filter isSymlink cfg.${persistentStorageName}.directories)))) - persistentStorageNames); - }) - ]; + # Run the unmount function on error to clean up stray + # bind mounts + trap "unmountBindMounts" ERR + ''; + + impermanenceRunUnmountPersistentStoragePaths = + dag.entryBefore + [ reloadSystemd ] + '' + unmountBindMounts + ''; + + impermanenceCreateTargetFileDirectories = + dag.entryBefore + [ "writeBoundary" ] + (lib.pipe (builtins.filter isSymlink (orderedFiles ++ orderedDirs)) [ + (builtins.map (ordered: builtins.dirOf ordered.dirCfg.targetDir)) + (builtins.sort (a: b: a < b)) + lib.lists.unique + (builtins.map (path: ''${pkgs.coreutils}/bin/mkdir -p ${escapeShellArg path}'')) + (builtins.concatStringsSep "\n") + ]); + }; }; } diff --git a/lib.nix b/lib.nix index 9b95917..850e155 100644 --- a/lib.nix +++ b/lib.nix @@ -78,14 +78,50 @@ let list; in result.duplicates; + + getHomeDirCfg = { pkgs, homeDirectory, storage }: dir: + let + dirCfg.storage = storage; + + dirCfg.mountDir = + if dirCfg.storage.removePrefixDirectory then + dirListToPath (builtins.tail (splitPath [ dir ])) + else + dir; + + + dirCfg.mountPoint = concatPaths [ homeDirectory dirCfg.mountDir ]; + dirCfg.targetDir = concatPaths [ dirCfg.storage.persistentStoragePath dir ]; + + dirCfg.escaped.mountPoint = lib.escapeShellArg dirCfg.mountPoint; + dirCfg.escaped.targetDir = lib.escapeShellArg dirCfg.targetDir; + + dirCfg.sanitized.targetDir = sanitizeName dirCfg.targetDir; + dirCfg.sanitized.mountPoint = sanitizeName dirCfg.mountPoint; + dirCfg.sanitized.mountDir = sanitizeName dirCfg.mountDir; + + dirCfg.unitName = "bindMount--${dirCfg.sanitized.mountDir}"; + + dirCfg.runBindfsArgs = + let + bindfsOptions = + lib.lists.optional (!dirCfg.storage.allowOther) "no-allow-other" + ++ lib.lists.optional (lib.versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${dirCfg.targetDir}" + ; + in + lib.lists.optionals (bindfsOptions != [ ]) [ "-o" (concatStringsSep "," bindfsOptions) ] + ; + in + dirCfg; in { inherit - splitPath - dirListToPath concatPaths + dirListToPath + duplicates + getHomeDirCfg parentsOf sanitizeName - duplicates + splitPath ; } diff --git a/mount-file.bash b/mount-file.bash deleted file mode 100755 index 6202d07..0000000 --- a/mount-file.bash +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -set -o nounset # Fail on use of unset variable. -set -o errexit # Exit on command failure. -set -o pipefail # Exit on failure of any command in a pipeline. -set -o errtrace # Trap errors in functions and subshells. -shopt -s inherit_errexit # Inherit the errexit option status in subshells. - -# Print a useful trace when an error occurs -trap 'echo Error when executing ${BASH_COMMAND} at line ${LINENO}! >&2' ERR - -# Get inputs from command line arguments -if [[ "$#" != 3 ]]; then - echo "Error: 'mount-file.bash' requires *three* args." >&2 - exit 1 -fi - -mountPoint="$1" -targetFile="$2" -debug="$3" - -trace() { - if (( "$debug" )); then - echo "$@" - fi -} -if (( "$debug" )); then - set -o xtrace -fi - -if [[ -L "$mountPoint" && $(readlink -f "$mountPoint") == "$targetFile" ]]; then - trace "$mountPoint already links to $targetFile, ignoring" -elif mount | grep -F "$mountPoint"' ' >/dev/null && ! mount | grep -F "$mountPoint"/ >/dev/null; then - trace "mount already exists at $mountPoint, ignoring" -elif [[ -e "$mountPoint" ]]; then - echo "A file already exists at $mountPoint!" >&2 - exit 1 -elif [[ -e "$targetFile" ]]; then - touch "$mountPoint" - mount -o bind "$targetFile" "$mountPoint" -else - ln -s "$targetFile" "$mountPoint" -fi diff --git a/nixos.nix b/nixos.nix index f49cd1a..ead6f7c 100644 --- a/nixos.nix +++ b/nixos.nix @@ -2,39 +2,41 @@ let inherit (lib) + all + any attrNames attrValues - zipAttrsWith - flatten - mkAfter - mkOption - mkDefault - mkIf - mkMerge - mapAttrsToList - types - foldl' - unique + catAttrs concatMap concatMapStrings - listToAttrs + concatMapStringsSep + concatStringsSep + elem escapeShellArg escapeShellArgs - recursiveUpdate - all filter filterAttrs - concatStringsSep - concatMapStringsSep - catAttrs - optional - optionalString + flatten + foldl' + hasPrefix + id + intersectLists + listToAttrs literalExpression - elem mapAttrs - intersectLists - any - id + mapAttrsToList + mkAfter + mkDefault + mkIf + mkMerge + mkOption + optional + optionalString + pipe + recursiveUpdate + types + unique + zipAttrsWith ; inherit (utils) @@ -48,14 +50,13 @@ let parentsOf ; - cfg = config.environment.persistence; + scripts = pkgs.callPackage ./scripts { }; + + cfg = config.impermanence; + cfgs = config.environment.persistence; users = config.users.users; - allPersistentStoragePaths = zipAttrsWith (_name: flatten) (filter (v: v.enable) (attrValues cfg)); + allPersistentStoragePaths = zipAttrsWith (_name: flatten) (filter (v: v.enable) (attrValues cfgs)); inherit (allPersistentStoragePaths) files directories; - mountFile = pkgs.runCommand "impermanence-mount-file" { buildInputs = [ pkgs.bash ]; } '' - cp ${./mount-file.bash} $out - patchShebangs $out - ''; # Create fileSystems bind mount entry. mkBindMountNameValuePair = { dirPath, persistentStoragePath, hideMount, ... }: { @@ -72,10 +73,156 @@ let # Create all fileSystems bind mount entries for a specific # persistent storage path. bindMounts = listToAttrs (map mkBindMountNameValuePair directories); + + systemMountPoints = builtins.map (fs: fs.mountPoint) (builtins.attrValues config.fileSystems); + getMountDependencies = persistentStoragePath: mountPath: + let + toMountUnit = path: "${escapeSystemdPath path}.mount"; + persistentPath = concatPaths [ persistentStoragePath mountPath ]; + getParentMount = path: + let dir = dirOf path; in pipe systemMountPoints [ + (builtins.filter (mountPoint: hasPrefix mountPoint dir)) + (builtins.sort builtins.lessThan) + lib.lists.last + ]; + persistentParent = getParentMount persistentPath; + mountParent = getParentMount mountPath; + parent = dirOf mountPath; + in + rec { + persistentPaths = [ persistentParent ]; + mountPaths = [ mountParent ]; + mountServices = [ "${mkCreateDirectoryUnitName parent persistentStoragePath}.service" ]; + persistentUnits = builtins.map toMountUnit persistentPaths; + mountUnits = builtins.map toMountUnit mountPaths ++ mountServices; + }; + + # The parent directories of files. + fileDirs = unique (catAttrs "parentDirectory" files); + + # All the directories actually listed by the user and the + # parent directories of listed files. + explicitDirectories = directories ++ fileDirs; + + # Home directories have to be handled specially, since + # they're at the permissions boundary where they + # themselves should be owned by the user and have stricter + # permissions than regular directories, whereas its parent + # should be owned by root and have regular permissions. + # + # This simply collects all the home directories and sets + # the appropriate permissions and ownership. + homeDirectories = + foldl' + (state: dir: + let + defaultPerms = { + mode = "0755"; + user = "root"; + group = "root"; + }; + homeDir = { + directory = dir.home; + dirPath = dir.home; + home = null; + mode = "0700"; + user = dir.user; + group = users.${dir.user}.group; + inherit defaultPerms; + inherit (dir) persistentStoragePath enableDebugging; + }; + in + if dir.home != null then + if !(elem homeDir state) then + state ++ [ homeDir ] + else + state + else + state + ) + [ ] + explicitDirectories; + + # Generate entries for all parent directories of the + # argument directories, listed in the order they need to + # be created. The parent directories are assigned default + # permissions. + mkParentDirectories = dirs: + let + # Create a new directory item from `dir`, the child + # directory item to inherit properties from and + # `path`, the parent directory path. + mkParent = dir: path: { + directory = path; + dirPath = + if dir.home != null then + concatPaths [ dir.home path ] + else + path; + inherit (dir) persistentStoragePath home enableDebugging; + inherit (dir.defaultPerms) user group mode; + }; + # Create new directory items for all parent + # directories of a directory. + mkParents = dir: + map (mkParent dir) (parentsOf dir.directory); + in + unique (flatten (map mkParents dirs)); + + # Parent directories of home folders. This is usually only + # /home, unless the user's home is in a non-standard + # location. + homeDirectoriesParents = mkParentDirectories homeDirectories; + + # Parent directories of all explicitly listed directories. + explicitParentDirectories = mkParentDirectories explicitDirectories; + + # All directories in the order they should be created. + allDirectories = builtins.concatLists [ + homeDirectoriesParents + homeDirectories + explicitParentDirectories + explicitDirectories + ]; + + mkCommandDirWithPerms = + { dirPath + , persistentStoragePath + , user + , group + , mode + , ... + }: + escapeShellArgs [ + (lib.getExe scripts.os.create-directories) + persistentStoragePath + dirPath + user + group + mode + ]; + + mkCommandPersistFile = { filePath, persistentStoragePath, ... }: + let + mountPoint = filePath; + targetFile = concatPaths [ persistentStoragePath filePath ]; + in + escapeShellArgs [ + (lib.getExe scripts.os.mount-file) + mountPoint + targetFile + ]; + + mkCreateDirectoryUnitName = path: persistentStoragePath: "impermanence-mkdir-${escapeSystemdPath persistentStoragePath}--${escapeSystemdPath path}"; in { options = { + impermanence.activationScriptsEnable = mkOption { + type = with types; bool; + default = true; + }; + environment.persistence = mkOption { default = { }; type = @@ -104,7 +251,7 @@ in options = { persistentStoragePath = mkOption { type = path; - default = cfg.${name}.persistentStoragePath; + default = cfgs.${name}.persistentStoragePath; defaultText = "environment.persistence.‹name›.persistentStoragePath"; description = '' The path to persistent storage where the real @@ -122,7 +269,7 @@ in }; enableDebugging = mkOption { type = bool; - default = cfg.${name}.enableDebugging; + default = cfgs.${name}.enableDebugging; defaultText = "environment.persistence.‹name›.enableDebugging"; internal = true; description = '' @@ -193,7 +340,7 @@ in }; hideMount = mkOption { type = bool; - default = cfg.${name}.hideMounts; + default = cfgs.${name}.hideMounts; defaultText = "environment.persistence.‹name›.hideMounts"; example = true; description = '' @@ -488,26 +635,29 @@ in virtualisation.fileSystems = mkOption { }; }; - config = mkIf (allPersistentStoragePaths != { }) { + config = mkIf (allPersistentStoragePaths != { }) (lib.mkMerge [{ systemd.services = let mkPersistFileService = { filePath, persistentStoragePath, enableDebugging, ... }: let - targetFile = escapeShellArg (concatPaths [ persistentStoragePath filePath ]); + targetFile = concatPaths [ persistentStoragePath filePath ]; mountPoint = escapeShellArg filePath; + deps = getMountDependencies persistentStoragePath filePath; in { - "persist-${escapeSystemdPath targetFile}" = { - description = "Bind mount or link ${targetFile} to ${mountPoint}"; - wantedBy = [ "local-fs.target" ]; - before = [ "local-fs.target" ]; + "persist-${escapeSystemdPath (escapeShellArg targetFile)}" = { + description = "nix/impermanence: bind mount or link ${escapeShellArg targetFile} to ${mountPoint}"; + wantedBy = deps.mountUnits; + wants = deps.persistentUnits; + after = deps.mountUnits ++ deps.persistentUnits; path = [ pkgs.util-linux ]; unitConfig.DefaultDependencies = false; + environment.DEBUG = builtins.toString enableDebugging; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; - ExecStart = "${mountFile} ${mountPoint} ${targetFile} ${escapeShellArg enableDebugging}"; - ExecStop = pkgs.writeShellScript "unbindOrUnlink-${escapeSystemdPath targetFile}" '' + ExecStart = mkCommandPersistFile { inherit filePath persistentStoragePath; }; + ExecStop = pkgs.writeShellScript "unbindOrUnlink-${escapeSystemdPath (escapeShellArg targetFile)}" '' set -eu if [[ -L ${mountPoint} ]]; then rm ${mountPoint} @@ -519,172 +669,58 @@ in }; }; }; + mkDirectoryService = { dirPath, persistentStoragePath, enableDebugging, ... }@dirCfg: { + "${mkCreateDirectoryUnitName dirPath persistentStoragePath}" = + let + deps = getMountDependencies persistentStoragePath dirPath; + in + { + description = "nix/impermanence: create ${escapeShellArg dirPath} directory inside ${escapeShellArg dirPath}"; + wantedBy = deps.mountUnits; + wants = deps.persistentUnits; + after = deps.mountUnits ++ deps.persistentUnits; + unitConfig.DefaultDependencies = false; + environment.DEBUG = builtins.toString enableDebugging; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = mkCommandDirWithPerms dirCfg; + }; + }; + }; + + allServiceDirectories = pipe allDirectories [ + (builtins.filter (dirCfg: !(lib.strings.hasSuffix "/." dirCfg.dirPath))) + ]; in - foldl' recursiveUpdate { } (map mkPersistFileService files); + foldl' recursiveUpdate { } (map mkDirectoryService allServiceDirectories ++ map mkPersistFileService files); fileSystems = mkIf (directories != [ ]) bindMounts; # So the mounts still make it into a VM built from `system.build.vm` virtualisation.fileSystems = mkIf (directories != [ ]) bindMounts; - system.activationScripts = - let - # Script to create directories in persistent and ephemeral - # storage. The directory structure's mode and ownership mirror - # those of persistentStoragePath/dir. - createDirectories = pkgs.runCommand "impermanence-create-directories" { buildInputs = [ pkgs.bash ]; } '' - cp ${./create-directories.bash} $out - patchShebangs $out - ''; - - mkDirWithPerms = - { dirPath - , persistentStoragePath - , user - , group - , mode - , enableDebugging - , ... - }: - let - args = [ - persistentStoragePath - dirPath - user - group - mode - enableDebugging - ]; - in - '' - ${createDirectories} ${escapeShellArgs args} - ''; - + system.activationScripts = lib.mkIf cfg.activationScriptsEnable { + "impermanenceCreatePersistentStorageDirs" = { + deps = [ "users" "groups" ]; # Build an activation script which creates all persistent # storage directories we want to bind mount. - dirCreationScript = - let - # The parent directories of files. - fileDirs = unique (catAttrs "parentDirectory" files); - - # All the directories actually listed by the user and the - # parent directories of listed files. - explicitDirs = directories ++ fileDirs; - - # Home directories have to be handled specially, since - # they're at the permissions boundary where they - # themselves should be owned by the user and have stricter - # permissions than regular directories, whereas its parent - # should be owned by root and have regular permissions. - # - # This simply collects all the home directories and sets - # the appropriate permissions and ownership. - homeDirs = - foldl' - (state: dir: - let - defaultPerms = { - mode = "0755"; - user = "root"; - group = "root"; - }; - homeDir = { - directory = dir.home; - dirPath = dir.home; - home = null; - mode = "0700"; - user = dir.user; - group = users.${dir.user}.group; - inherit defaultPerms; - inherit (dir) persistentStoragePath enableDebugging; - }; - in - if dir.home != null then - if !(elem homeDir state) then - state ++ [ homeDir ] - else - state - else - state - ) - [ ] - explicitDirs; - - # Generate entries for all parent directories of the - # argument directories, listed in the order they need to - # be created. The parent directories are assigned default - # permissions. - mkParentDirs = dirs: - let - # Create a new directory item from `dir`, the child - # directory item to inherit properties from and - # `path`, the parent directory path. - mkParent = dir: path: { - directory = path; - dirPath = - if dir.home != null then - concatPaths [ dir.home path ] - else - path; - inherit (dir) persistentStoragePath home enableDebugging; - inherit (dir.defaultPerms) user group mode; - }; - # Create new directory items for all parent - # directories of a directory. - mkParents = dir: - map (mkParent dir) (parentsOf dir.directory); - in - unique (flatten (map mkParents dirs)); - - # Parent directories of home folders. This is usually only - # /home, unless the user's home is in a non-standard - # location. - homeDirParents = mkParentDirs homeDirs; - - # Parent directories of all explicitly listed directories. - parentDirs = mkParentDirs explicitDirs; - - # All directories in the order they should be created. - allDirs = homeDirParents ++ homeDirs ++ parentDirs ++ explicitDirs; - in - pkgs.writeShellScript "impermanence-run-create-directories" '' - _status=0 - trap "_status=1" ERR - ${concatMapStrings mkDirWithPerms allDirs} - exit $_status - ''; - - mkPersistFile = { filePath, persistentStoragePath, enableDebugging, ... }: - let - mountPoint = filePath; - targetFile = concatPaths [ persistentStoragePath filePath ]; - args = escapeShellArgs [ - mountPoint - targetFile - enableDebugging - ]; - in - '' - ${mountFile} ${args} - ''; - - persistFileScript = - pkgs.writeShellScript "impermanence-persist-files" '' - _status=0 - trap "_status=1" ERR - ${concatMapStrings mkPersistFile files} - exit $_status - ''; - in - { - "createPersistentStorageDirs" = { - deps = [ "users" "groups" ]; - text = "${dirCreationScript}"; - }; - "persist-files" = { - deps = [ "createPersistentStorageDirs" ]; - text = "${persistFileScript}"; - }; + text = builtins.toString (pkgs.writeShellScript "impermanence-run-create-directories" '' + _status=0 + trap "_status=1" ERR + ${concatMapStrings (dir: "DEBUG=${builtins.toString dir.enableDebugging} ${mkCommandDirWithPerms dir}\n") allDirectories} + exit $_status + ''); }; + "impermanencePersistFiles" = { + deps = [ "impermanenceCreatePersistentStorageDirs" ]; + text = builtins.toString (pkgs.writeShellScript "impermanence-persist-files" '' + _status=0 + trap "_status=1" ERR + ${concatMapStrings (file: "DEBUG=${builtins.toString file.enableDebugging} ${mkCommandPersistFile file}\n") files} + exit $_status + ''); + }; + }; # Create the mountpoints of directories marked as needed for boot # which are also persisted. For this to work, it has to run at @@ -757,7 +793,7 @@ in config.fileSystems.${fs}.neededForBoot == cond else cond; - persistentStoragePaths = attrNames cfg; + persistentStoragePaths = attrNames cfgs; usersPerPath = allPersistentStoragePaths.users; homeDirOffenders = filterAttrs @@ -869,6 +905,6 @@ in '' ]) ]); - }; + }]); } diff --git a/scripts/default.nix b/scripts/default.nix new file mode 100644 index 0000000..bf73468 --- /dev/null +++ b/scripts/default.nix @@ -0,0 +1,73 @@ +{ lib +, pkgs +, systemctl ? lib.getExe' pkgs.systemd "systemctl" +, ... +}: +let + outputPrefix = "OUTPUT:"; + + os.mount-file = pkgs.writeShellApplication { + name = "impermanence-mount-file"; + runtimeInputs = (with pkgs; [ + util-linux + gnugrep + ]) ++ [ + path-info + ]; + text = builtins.readFile ./os-mount-file.bash; + }; + os.create-directories = pkgs.writeShellApplication { + name = "impermanence-create-directories"; + runtimeInputs = with pkgs; [ coreutils ]; + text = builtins.readFile ./os-create-directories.bash; + }; + + hm.unmount = pkgs.writeShellApplication { + name = "impermanence-hm-unmount"; + runtimeInputs = (with pkgs; [ + fuse + ]) ++ [ + path-info + ]; + text = builtins.readFile ./hm-unmount.bash; + }; + + path-info = pkgs.writeShellApplication { + name = "impermanence-path-info"; + runtimeInputs = with pkgs; [ util-linux gnugrep ]; + text = builtins.readFile ./path-info.bash; + }; + + hm.bind-mount-activation = pkgs.writeShellApplication { + name = "impermanence-hm-bind-mount-activation"; + runtimeInputs = (with pkgs; [ + bindfs + coreutils + util-linux + ]) ++ (with hm; [ + unmount + ]) ++ [ + path-info + ]; + text = '' + PATH="${builtins.dirOf systemctl}:$PATH" + ${builtins.readFile ./hm-bind-mount-activation.bash} + ''; + }; + hm.bind-mount-service = pkgs.writeShellApplication { + name = "impermanence-hm-bind-mount-service"; + runtimeInputs = (with pkgs; [ + bindfs + coreutils + ]) ++ (with hm; [ + unmount + ]) ++ [ + path-info + ]; + text = builtins.readFile ./hm-bind-mount-service.bash; + }; +in +{ + inherit os hm outputPrefix; + inherit path-info; +} diff --git a/scripts/hm-bind-mount-activation.bash b/scripts/hm-bind-mount-activation.bash new file mode 100755 index 0000000..976276f --- /dev/null +++ b/scripts/hm-bind-mount-activation.bash @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -o nounset # Fail on use of unset variable. +set -o errexit # Exit on command failure. +set -o pipefail # Exit on failure of any command in a pipeline. +set -o errtrace # Trap errors in functions and subshells. +set -o noglob # Disable filename expansion (globbing), since it could otherwise happen during path splitting. +shopt -s inherit_errexit # Inherit the errexit option status in subshells. +trap 'echo "Error when executing $BASH_COMMAND at line $LINENO!" >&2' ERR +test -z "${DEBUG:=""}" || set -x + +mountPoint="$1" +targetDir="$2" +unitName="$3" +bindfsArgs=("${@:4}") +: "${outputPrefix:="OUTPUT:"}" + +activationUnitName="activation-${unitName}" +export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-"/run/user/${UID}"}" + +unmountCmd="$(command -v impermanence-hm-unmount)" +mountpointCmd="$(command -v mountpoint)" + +eval "$(impermanence-path-info "$mountPoint" FSTYPE SOURCE)" + +if [[ "$IS_MOUNTPOINT" == 1 && "$IS_DEAD" == 1 ]]; then + echo "seems like the process died, remounting $mountPoint..." + "$unmountCmd" "$mountPoint" 3 1 + eval "$(impermanence-path-info "$mountPoint" FSTYPE SOURCE)" +fi + +mkdir -p "$targetDir" +mkdir -p "$mountPoint" + +dumpvars() { + for var in "$@"; do + printf "%s=%q " "${var}" "${!var}" + done +} + +bindfs-run() { + # executing directly inside `home.activation` results in `bindfs` being + # killed upon `home-manager-.service` restarts + # we can work around it, by putting `bindfs` inside `background.slice` + # through `systemd-run` + local targetDir="$1" mountPoint="$2" args=() run_args=() + if [[ "${UID}" != 0 ]]; then + args+=(--user) + run_args+=(--slice=background) + fi + if systemctl "${args[@]}" is-active "${activationUnitName}.service" &>/dev/null; then + echo "'${activationUnitName}.service' is already running, not starting another one." + return + fi + # ExecCondition serves same purpose as Unit.ConditionPathIsMountPoint + systemd-run "${args[@]}" "${run_args[@]}" --unit="${activationUnitName}" \ + --service-type=forking \ + --property=ExecCondition="!${mountpointCmd@Q} ${mountPoint@Q}" \ + --property=ExecStop="${unmountCmd@Q} ${targetDir@Q} ${mountPoint@Q}" \ + bindfs "${bindfsArgs[@]}" "${targetDir}" "${mountPoint}" +} + +if [[ "$IS_MOUNTPOINT" == 1 && "$FSTYPE" == "fuse" && "$SOURCE" != "$targetDir" ]]; then + echo "remounting $mountPoint from $SOURCE to $targetDir" + systemctl --user stop "${unitName}.service" "${activationUnitName}.service" + bindfs-run "${targetDir}" "${mountPoint}" + echo "$outputPrefix$mountPoint" +elif [[ "$IS_MOUNTPOINT" == 0 ]]; then + echo "mounting $targetDir at $mountPoint" + bindfs-run "${targetDir}" "${mountPoint}" + echo "$outputPrefix$mountPoint" +else + echo "${mountPoint@Q} is already a mountpoint: $(dumpvars FSTYPE SOURCE)" +fi diff --git a/scripts/hm-bind-mount-service.bash b/scripts/hm-bind-mount-service.bash new file mode 100755 index 0000000..0858ede --- /dev/null +++ b/scripts/hm-bind-mount-service.bash @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -o nounset # Fail on use of unset variable. +set -o errexit # Exit on command failure. +set -o pipefail # Exit on failure of any command in a pipeline. +set -o errtrace # Trap errors in functions and subshells. +set -o noglob # Disable filename expansion (globbing), since it could otherwise happen during path splitting. +shopt -s inherit_errexit # Inherit the errexit option status in subshells. +trap 'echo "Error when executing $BASH_COMMAND at line $LINENO!" >&2' ERR +test -z "${DEBUG:=""}" || set -x + +targetDir="$1" +mountPoint="$2" +bindfsArgs=("${@:3}") + +eval "$(impermanence-path-info "$mountPoint" SOURCE)" + +if [[ "$IS_MOUNTPOINT" == 1 && "$IS_DEAD" == 1 ]]; then + impermanence-hm-unmount "$mountPoint" 3 1 + eval "$(impermanence-path-info "$mountPoint" SOURCE)" +fi + +if [[ "$IS_MOUNTPOINT" == 0 ]]; then + mkdir -p "$mountPoint" + + exec bindfs "${bindfsArgs[@]}" "$targetDir" "$mountPoint" +elif [[ "$SOURCE" == "$targetDir" ]]; then + echo "Mountpoint '$mountPoint' already points at '$targetDir'!" >&2 +else + echo "There is already an active mount at or below '$mountPoint'!" >&2 + exit 1 +fi diff --git a/scripts/hm-unmount.bash b/scripts/hm-unmount.bash new file mode 100755 index 0000000..b4675d7 --- /dev/null +++ b/scripts/hm-unmount.bash @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -o nounset # Fail on use of unset variable. +set -o errexit # Exit on command failure. +set -o pipefail # Exit on failure of any command in a pipeline. +set -o errtrace # Trap errors in functions and subshells. +set -o noglob # Disable filename expansion (globbing), since it could otherwise happen during path splitting. +shopt -s inherit_errexit # Inherit the errexit option status in subshells. +trap 'echo "Error when executing $BASH_COMMAND at line $LINENO!" >&2' ERR +test -z "${DEBUG:=""}" || set -x + +mountPoint="$1" +triesLeft="$2" +sleep="$3" + +eval "$(impermanence-path-info "$mountPoint" SOURCE)" + +if [[ "$IS_MOUNTPOINT" == 1 ]]; then + while ((triesLeft > 0)); do + if fusermount -u "$mountPoint"; then + break + fi + + ((triesLeft--)) + if ((triesLeft == 0)); then + echo "Couldn't perform regular unmount of $mountPoint. Attempting lazy unmount." + fusermount -uz "$mountPoint" + else + sleep "$sleep" + fi + done +fi diff --git a/create-directories.bash b/scripts/os-create-directories.bash similarity index 54% rename from create-directories.bash rename to scripts/os-create-directories.bash index 5f9a793..75dbdca 100755 --- a/create-directories.bash +++ b/scripts/os-create-directories.bash @@ -1,16 +1,16 @@ #!/usr/bin/env bash - -set -o nounset # Fail on use of unset variable. -set -o errexit # Exit on command failure. -set -o pipefail # Exit on failure of any command in a pipeline. -set -o errtrace # Trap errors in functions and subshells. -set -o noglob # Disable filename expansion (globbing), - # since it could otherwise happen during - # path splitting. -shopt -s inherit_errexit # Inherit the errexit option status in subshells. - -# Print a useful trace when an error occurs -trap 'echo Error when executing ${BASH_COMMAND} at line ${LINENO}! >&2' ERR +# Script to create directories in persistent and ephemeral +# storage. The directory structure's mode and ownership mirror +# those of persistentStoragePath/dir. + +set -o nounset # Fail on use of unset variable. +set -o errexit # Exit on command failure. +set -o pipefail # Exit on failure of any command in a pipeline. +set -o errtrace # Trap errors in functions and subshells. +set -o noglob # Disable filename expansion (globbing), since it could otherwise happen during path splitting. +shopt -s inherit_errexit # Inherit the errexit option status in subshells. +trap 'echo "Error when executing $BASH_COMMAND at line $LINENO!" >&2' ERR +test -z "${DEBUG:=""}" || set -x # Given a source directory, /source, and a target directory, # /target/foo/bar/bazz, we want to "clone" the target structure @@ -28,28 +28,23 @@ trap 'echo Error when executing ${BASH_COMMAND} at line ${LINENO}! >&2' ERR # 3. Copy the mode of the source path to the target path # Get inputs from command line arguments -if [[ "$#" != 6 ]]; then - printf "Error: 'create-directories.bash' requires *six* args.\n" >&2 - exit 1 +if [[ "$#" != 5 ]]; then + printf "Error: 'create-directories.bash' requires *five* args.\n" >&2 + exit 1 fi sourceBase="$1" target="$2" user="$3" group="$4" mode="$5" -debug="$6" - -if (( "$debug" )); then - set -o xtrace -fi # check that the source exists and warn the user if it doesn't, then # create them with the specified permissions realSource="$(realpath -m "$sourceBase$target")" if [[ ! -d "$realSource" ]]; then - printf "Warning: Source directory '%s' does not exist; it will be created for you with the following permissions: owner: '%s:%s', mode: '%s'.\n" "$realSource" "$user" "$group" "$mode" - mkdir --mode="$mode" "$realSource" - chown "$user:$group" "$realSource" + printf "Warning: Source directory '%s' does not exist; it will be created for you with the following permissions: owner: '%s:%s', mode: '%s'.\n" "$realSource" "$user" "$group" "$mode" + mkdir --mode="$mode" "$realSource" + chown "$user:$group" "$realSource" fi [[ -d "$target" ]] || mkdir "$target" diff --git a/scripts/os-mount-file.bash b/scripts/os-mount-file.bash new file mode 100755 index 0000000..5403654 --- /dev/null +++ b/scripts/os-mount-file.bash @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -o nounset # Fail on use of unset variable. +set -o errexit # Exit on command failure. +set -o pipefail # Exit on failure of any command in a pipeline. +set -o errtrace # Trap errors in functions and subshells. +set -o noglob # Disable filename expansion (globbing), since it could otherwise happen during path splitting. +shopt -s inherit_errexit # Inherit the errexit option status in subshells. +trap 'echo "Error when executing $BASH_COMMAND at line $LINENO!" >&2' ERR +test -z "${DEBUG:=""}" || set -x + +# Get inputs from command line arguments +if [[ "$#" != 2 ]]; then + echo "Error: 'mount-file.bash' requires *two* args." >&2 + exit 1 +fi + +mountPoint="$1" +targetFile="$2" + +eval "$(impermanence-path-info "$mountPoint" SOURCE)" + +if [[ "$IS_SYMLINK" == 1 && "$SOURCE" == "$targetFile" ]]; then + echo "$mountPoint already links to $targetFile, ignoring" +elif [[ "$IS_MOUNTPOINT" == 1 ]]; then + echo "mount already exists at $mountPoint, ignoring" +elif [[ -e "$mountPoint" ]]; then + echo "A file already exists at $mountPoint!" >&2 + exit 1 +elif [[ -e "$targetFile" ]]; then + echo "Bind mounting ${targetFile} to ${mountPoint}..." + touch "$mountPoint" + mount -o bind "$targetFile" "$mountPoint" +else + echo "Symlinking ${targetFile} to ${mountPoint}..." + ln -s "$targetFile" "$mountPoint" +fi diff --git a/scripts/path-info.bash b/scripts/path-info.bash new file mode 100755 index 0000000..c232c14 --- /dev/null +++ b/scripts/path-info.bash @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -o nounset # Fail on use of unset variable. +set -o errexit # Exit on command failure. +set -o pipefail # Exit on failure of any command in a pipeline. +set -o errtrace # Trap errors in functions and subshells. +set -o noglob # Disable filename expansion (globbing), since it could otherwise happen during path splitting. +shopt -s inherit_errexit # Inherit the errexit option status in subshells. +trap 'echo "Error when executing $BASH_COMMAND at line $LINENO!" >&2' ERR +test -z "${DEBUG:=""}" || set -x + +mountPoint="$1" +outputs="" +variables=() +has_source=0 + +for variable in "${@:2}"; do + # uppercase the variable + variable="${variable^^}" + + output="$variable" + # special handling of some names + case "$variable" in + *_PCT) output="${variable%_PCT}%" ;; + SOURCE) has_source=1 ;; + esac + + variables+=("$variable") + outputs="$outputs,$output" +done +outputs="${outputs#,}" + +IS_MOUNTPOINT=0 +IS_SYMLINK=0 +IS_DEAD=0 + +if [[ -L "$mountPoint" ]]; then + # shellcheck disable=SC2034 + IS_SYMLINK=1 + SOURCE="$(readlink -f "$mountPoint")" +elif _src="$(findmnt --output "$outputs" --shell --pairs --first-only --mountpoint "$mountPoint")"; then + eval "$_src" + + if [[ "$has_source" == 1 && "$SOURCE" == *'['*']' ]]; then + # resolve bind-mounts in [brackets] + _SOURCE_PARENT="$(findmnt --noheadings --output TARGET --first-only "${SOURCE%%[*}")" + SOURCE="${SOURCE#*[}" + SOURCE="${SOURCE%]}" + SOURCE="$_SOURCE_PARENT$SOURCE" + fi +fi + +if mountpoint --quiet "$mountPoint"; then + # shellcheck disable=SC2034 + IS_MOUNTPOINT=1 +fi +if mountpoint "$mountPoint" |& grep -q 'Transport endpoint is not connected'; then + # shellcheck disable=SC2034 + IS_DEAD=1 +fi + +for varName in IS_DEAD IS_MOUNTPOINT IS_SYMLINK "${variables[@]}"; do + value="${!varName:-""}" + echo "$varName=${value@Q}" +done