nova-nix-0.9.0.0: src/Nix/Store/Symlink.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- | Symlink-aware filesystem primitives shared by the store walks.
--
-- Two things every walk over store content needs and nothing in
-- @directory@ gives directly: classifying a node WITHOUT following a
-- link, and creating a link whose Windows flavour (file or directory)
-- is read off the target. They live in a leaf module so the collector's
-- root walk and the out-link creation ("Nix.Store.GC") share them with
-- the materializers in "Nix.Store" without importing either.
module Nix.Store.Symlink
( -- * Classification
WalkNode (..),
classifyWalkNode,
-- * Creation
createSymlinkOfKind,
)
where
import Control.Exception (IOException, catch, try)
import Data.Text (Text)
import qualified Data.Text as T
import Nix.Store.Exclusive (symlinkTakenMessage)
import qualified System.Directory as Dir
import qualified System.Directory.OsPath as OsDir
import System.IO.Error (isAlreadyExistsError)
import System.OsPath (OsPath, decodeFS, takeDirectory, (</>))
-- | A node kind for store walks, classified WITHOUT following symlinks:
-- the link test runs first because 'Dir.doesDirectoryExist' and
-- 'Dir.doesFileExist' follow links and would report a link as its target.
-- A dangling link still classifies as 'WalkSymlink'; a probe failure
-- classifies as 'WalkAbsent' rather than throwing mid-walk.
data WalkNode = WalkSymlink | WalkDirectory | WalkRegular | WalkAbsent
deriving (Eq, Show)
-- | Classify one path for a store walk. The walks dispatch on this (or
-- run the same link-first probe order) so no store walk follows a
-- symlink: following one reads or mutates content outside the tree
-- being walked, and does not terminate on a link cycle.
classifyWalkNode :: FilePath -> IO WalkNode
classifyWalkNode path = do
isLink <- Dir.pathIsSymbolicLink path `catch` \(_ :: IOException) -> pure False
if isLink
then pure WalkSymlink
else do
isDir <- Dir.doesDirectoryExist path
if isDir
then pure WalkDirectory
else do
isFile <- Dir.doesFileExist path
pure (if isFile then WalkRegular else WalkAbsent)
-- | Create one symlink, choosing the Windows flavour from the target's
-- kind: the target is resolved relative to the link's directory (an
-- absolute target resolves to itself) and probed once. A creation
-- failure is loud rather than approximated: writing the target text as a
-- regular file once registered a tree whose NAR hash differed from the
-- signed narinfo's, silent store corruption that a later push refused to
-- publish. A name already taken is refused in upstream's words, as a
-- folded sibling or a raced root meets it.
-- The parent directory is not created here; a caller that wants that
-- does it first. Both paths are 'OsPath's, so on POSIX the link and
-- its target are the bytes they were spelled from.
createSymlinkOfKind :: OsPath -> OsPath -> IO (Either Text ())
createSymlinkOfKind linkPath target = do
targetIsDir <- OsDir.doesDirectoryExist (takeDirectory linkPath </> target)
result <-
try $
if targetIsDir
then OsDir.createDirectoryLink target linkPath
else OsDir.createFileLink target linkPath
case result of
Right () -> pure (Right ())
Left (e :: IOException) -> do
shownLink <- decodeFS linkPath
shownTarget <- decodeFS target
pure . Left $
if isAlreadyExistsError e
then symlinkTakenMessage shownLink shownTarget
else
"cannot create symlink "
<> T.pack shownLink
<> " -> "
<> T.pack shownTarget
<> ": "
<> T.pack (show e)
<> " (on Windows this needs Developer Mode or elevation)"