{-|
Module      : System.SymbolicLink
Description : Tools for working with symbolic links.
Copyright   : (c) Fuzz Leonard, 2019
License     : BSD-3
Maintainer  : fuzz@kt-22.com
Stability   : experimental
Portability : POSIX

SymbolicLink provides tools for working with symbolic links on POSIX systems.

The executable @symlink@ changes to the user's home directory, reads in a
sequence of source/target mappings from a YAML file in @.symlinks@ and attempts
to create them. If the target exists and is a symbolic link it will be removed
and replaced, otherwise symlink will refuse to clobber it.

The function @filePathExist@ works like @fileExist@ from @System.Posix.Files@
or @doesPathExist@ from @System.Directory@ but does not follow symlinks, thus
making it suitable for working with unreferenced symlinks. Unreferenced
symlinks are not necessarily "broken"; one should not have to handle exceptions
to work with them.

Conceptually @filePathExist@ is concerned with the perspective of the current
user environment, thus we consider the path to not exist if the user does not
have the necessary permissions or if any other error occurs while attempting
to get the @FilePath@ status. 
-}

module System.SymbolicLink
  ( FileType
  , filePathExist
  , filePathExistEither
  , fileType
  , fileTypeEither
  , fileTypeMaybe
  , getFileTypeFromStatus
  )
where

import Control.Exception
import System.Posix.Files ( FileStatus
                          , getSymbolicLinkStatus
                          , isBlockDevice
                          , isCharacterDevice
                          , isDirectory
                          , isNamedPipe
                          , isRegularFile
                          , isSocket
                          , isSymbolicLink
                          )
-- | 'filePathExist' works like 'System.Posix.Files.fileExist'
--   except it doesn't follow symlinks.
filePathExist :: FilePath -> IO Bool
filePathExist f = do
  x <- try (getSymbolicLinkStatus f) :: IO (Either IOError FileStatus)
  case x of
    Left  _ -> return False
    Right _ -> return True

-- | Like 'filePathExist' but get the 'IOError' instead of 'False'
filePathExistEither :: FilePath -> IO (Either IOError Bool)
filePathExistEither f = do
  x <- try (getSymbolicLinkStatus f) :: IO (Either IOError FileStatus)
  case x of
    Left  e -> return $ Left e
    Right _ -> return $ Right True

-- | Return the actual 'FileType' of a 'FilePath' without following
-- symbolic links. This is unsafe; you should ensure you can get a
-- 'FileStatus' before evaluation.
fileType :: FilePath -> IO FileType
fileType f = do
  s <- getSymbolicLinkStatus f
  return $ getFileTypeFromStatus s

-- | Like 'fileType' but with errors wrapped for your protection.
fileTypeEither :: FilePath -> IO (Either IOError FileType)
fileTypeEither f = do
  x <- filePathExistEither f
  case x of
    Left  e -> return $ Left e
    Right _ -> do
      t <- fileType f
      return $ Right t

-- | Like 'fileType' without errors because life is short.
fileTypeMaybe :: FilePath -> IO (Maybe FileType)
fileTypeMaybe f = do
  x <- filePathExist f
  if x
    then do
      s <- getSymbolicLinkStatus f
      return $ Just (getFileTypeFromStatus s)
    else return Nothing

-- | Remember that 'FileStatus' can go stale and thus so can 'FileType'.
getFileTypeFromStatus :: FileStatus -> FileType
getFileTypeFromStatus s | isBlockDevice s  = BlockDevice
                        | isDirectory s    = Directory
                        | isNamedPipe s    = NamedPipe
                        | isRegularFile s  = RegularFile
                        | isSocket s       = Socket
                        | isSymbolicLink s = SymbolicLink
                        | otherwise        = Unknown

data FileType =
    BlockDevice
  | CharacterDevice
  | Directory
  | NamedPipe
  | RegularFile
  | Socket
  | SymbolicLink
  | Unknown
  deriving (Eq, Show)