packages feed

hydra-kernel-0.17.5: src/main/haskell/Hydra/Overlay/Haskell/Lib/Files.hs

-- | Haskell implementations of hydra.lib.files primitives

module Hydra.Overlay.Haskell.Lib.Files where

import qualified Control.Exception as E
import qualified Data.ByteString as BS
import qualified Hydra.Error.File as FileError
import qualified Hydra.File as File
import qualified Hydra.Time as Time
import qualified System.Directory as Dir
import qualified System.IO.Error as IOE
import qualified System.Posix.Files as Posix
import System.Posix.Types (EpochTime(..))
import Foreign.C.Types (CTime(..))
import Prelude hiding (appendFile, readFile, writeFile)


appendFile :: File.FilePath -> BS.ByteString -> IO (Either FileError.FileError ())
appendFile path contents =
  withFileError path $ BS.appendFile (File.unFilePath path) contents

copy :: Bool -> File.FilePath -> File.FilePath -> IO (Either FileError.FileError ())
copy recursive source destination =
  withFileError source $
    if recursive
      then copyDirectoryRecursive (File.unFilePath source) (File.unFilePath destination)
      else Dir.copyFile (File.unFilePath source) (File.unFilePath destination)

createDirectory :: Bool -> File.FilePath -> IO (Either FileError.FileError ())
createDirectory recursive path =
  withFileError path $
    if recursive
      then Dir.createDirectoryIfMissing True (File.unFilePath path)
      else Dir.createDirectory (File.unFilePath path)

createSymlink :: File.FilePath -> File.FilePath -> IO (Either FileError.FileError ())
createSymlink target link =
  withFileError link $ Posix.createSymbolicLink (File.unFilePath target) (File.unFilePath link)

exists :: File.FilePath -> IO (Either FileError.FileError Bool)
exists path =
  withFileError path $ Dir.doesPathExist (File.unFilePath path)

listDirectory :: File.FilePath -> IO (Either FileError.FileError [File.FilePath])
listDirectory path =
  withFileError path $
    fmap (fmap File.FilePath) (Dir.listDirectory (File.unFilePath path))

readFile :: File.FilePath -> IO (Either FileError.FileError BS.ByteString)
readFile path =
  withFileError path $ BS.readFile (File.unFilePath path)

readSymlink :: File.FilePath -> IO (Either FileError.FileError File.FilePath)
readSymlink path =
  withFileError path $ do
    isLink <- Posix.isSymbolicLink <$> Posix.getSymbolicLinkStatus (File.unFilePath path)
    if isLink
      then File.FilePath <$> Posix.readSymbolicLink (File.unFilePath path)
      else E.throwIO (IOE.mkIOError IOE.illegalOperationErrorType "readSymlink" Nothing
             (Just (File.unFilePath path)))

removeDirectory :: Bool -> File.FilePath -> IO (Either FileError.FileError ())
removeDirectory recursive path =
  withFileError path $
    if recursive
      then Dir.removeDirectoryRecursive (File.unFilePath path)
      else Dir.removeDirectory (File.unFilePath path)

removeFile :: File.FilePath -> IO (Either FileError.FileError ())
removeFile path =
  withFileError path $ Dir.removeFile (File.unFilePath path)

rename :: File.FilePath -> File.FilePath -> IO (Either FileError.FileError ())
rename source destination =
  withFileError source $ Dir.renamePath (File.unFilePath source) (File.unFilePath destination)

status :: Bool -> File.FilePath -> IO (Either FileError.FileError File.FileStatus)
status followLinks path =
  withFileError path $ do
    let p = File.unFilePath path
        getStatus = if followLinks then Posix.getFileStatus else Posix.getSymbolicLinkStatus
    st <- getStatus p
    pure File.FileStatus {
      File.fileStatusFileType = posixFileType st,
      File.fileStatusSize = fromIntegral (Posix.fileSize st),
      File.fileStatusModificationTime = epochToTimespec (Posix.modificationTime st),
      File.fileStatusAccessTime = Just (epochToTimespec (Posix.accessTime st)),
      File.fileStatusStatusChangeTime = Just (epochToTimespec (Posix.statusChangeTime st))}

writeFile :: File.FilePath -> BS.ByteString -> IO (Either FileError.FileError ())
writeFile path contents =
  withFileError path $ BS.writeFile (File.unFilePath path) contents

-- Helpers (not primitives)

withFileError :: File.FilePath -> IO a -> IO (Either FileError.FileError a)
withFileError path action =
  E.catch (Right <$> action) $ \e ->
    pure $ Left $ classify path (e :: IOError)

classify :: File.FilePath -> IOError -> FileError.FileError
classify path e
  | IOE.isAlreadyExistsError e = FileError.FileErrorAlreadyExists path
  | IOE.isDoesNotExistError e = FileError.FileErrorNotFound path
  | IOE.isPermissionError e = FileError.FileErrorPermissionDenied path
  | IOE.isIllegalOperation e = FileError.FileErrorInvalidPath (IOE.ioeGetErrorString e)
  | otherwise = FileError.FileErrorOther (IOE.ioeGetErrorString e)

-- | Classify a POSIX file status's file type as a hydra.file.FileType.
posixFileType :: Posix.FileStatus -> File.FileType
posixFileType st
  | Posix.isSymbolicLink st  = File.FileTypeLink
  | Posix.isDirectory st     = File.FileTypeDirectory
  | Posix.isRegularFile st   = File.FileTypeRegular
  | Posix.isBlockDevice st   = File.FileTypeBlock
  | Posix.isCharacterDevice st = File.FileTypeCharacter
  | Posix.isNamedPipe st     = File.FileTypeFifo
  | Posix.isSocket st        = File.FileTypeSocket
  | otherwise                = File.FileTypeRegular

-- | Convert a POSIX EpochTime (whole seconds since the Unix Epoch; sub-second resolution is not
-- exposed by the portable "unix" API) to a hydra.time.Timespec.
epochToTimespec :: EpochTime -> Time.Timespec
epochToTimespec (CTime secs) =
  Time.Timespec {
    Time.timespecSeconds = fromIntegral secs,
    Time.timespecNanoseconds = 0}

-- | Recursively copy a directory tree from source to destination.
copyDirectoryRecursive :: FilePath -> FilePath -> IO ()
copyDirectoryRecursive source destination = do
  isDir <- Dir.doesDirectoryExist source
  if isDir
    then do
      Dir.createDirectoryIfMissing True destination
      entries <- Dir.listDirectory source
      mapM_ (\name -> copyDirectoryRecursive (source ++ "/" ++ name) (destination ++ "/" ++ name)) entries
    else Dir.copyFile source destination