{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} {-| Description: Utility code for file IO with 'RawFilePath's. -} module Data.Git.Internal.FileUtil where import Control.Applicative import Control.DeepSeq import Control.Exception import Control.Monad import Control.Monad.IO.Class import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import System.IO import System.IO.Error (catchIOError, isAlreadyExistsError, isDoesNotExistError, isPermissionError, tryIOError) import System.Posix.ByteString import System.Posix.FilePath -- | A somewhat more involved version of 'System.IO.withFile'. withHandle :: NFData a => Maybe FileMode -> OpenMode -> RawFilePath -> OpenFileFlags -> (Handle -> IO a) -> IO a withHandle c m p offs app = bracket (fdToHandle =<< openFd p m c offs) hClose (\h -> app h >>= (\r -> r `deepseq` return r)) -- | Uses 'mkstemp' (with the given path as the filename template) instead of the 'withHandle' -- machinery to open the temp file. The awkward type in the callback is to let the user compute -- the name of the output file---when @'Just' tgt@, the temp file is 'rename'd to @tgt@, otherwise -- we use the template. withHandleAtomic :: NFData a => RawFilePath -> (Handle -> IO (Maybe RawFilePath, a)) -> IO a withHandleAtomic p app = bracketOnError (mkstemp p) cleanup go where cleanup (tmp, h) = hClose h >> removeLink tmp go (tmp, h) = do (np, force -> !r) <- app h hClose h rename tmp $ maybe p id np return r -- | An open temp file. data TempFile = TempFile { tempHandle :: Handle -- | The filename 'mkstemp' came up with. , tempFileName :: RawFilePath -- | Template given to 'mkstemp' for this file. , tempTemplate :: RawFilePath -- | When given 'Nothing', deletes the temp file, when given @'Just' tgt@, @rename@s the temp -- file to @tgt@. , closeTempFile :: Maybe RawFilePath -> IO () } -- | Make a 'TempFile' using the given 'RawFilePath' as a template for 'mkstemp'. tempFile :: RawFilePath -> IO TempFile tempFile p = do createRawDirectoryIfMissing True (dropFileName p) bracketOnError (mkstemp p) cleanup go where cleanup (tmp, h) = hClose h >> removeLink tmp go t@(tmp, h) = return $ TempFile h tmp p (finish t) finish t Nothing = cleanup t finish (tmp, h) (Just np) = rename tmp np >> hClose h readFileFlags :: OpenFileFlags -- | Flags to open a file for reading readFileFlags = defaultFileFlags -- | Read a file into a lazy 'BL.ByteString'. readRawFileL :: RawFilePath -> IO BL.ByteString readRawFileL p = withHandle Nothing ReadOnly p readFileFlags BL.hGetContents -- | Read a file into a strict 'B.ByteString'. readRawFileS :: RawFilePath -> IO B.ByteString readRawFileS p = withHandle Nothing ReadOnly p readFileFlags B.hGetContents -- | Write a lazy 'BL.ByteString' to a file via 'withHandleAtomic'. writeRawFileL :: RawFilePath -> BL.ByteString -> IO () writeRawFileL p bs = withHandleAtomic p (\h -> BL.hPut h bs *> return (Nothing, ())) -- | Write a strict 'B.ByteString' to a file via 'withHandleAtomic'. writeRawFileS :: RawFilePath -> B.ByteString -> IO () writeRawFileS p bs = withHandleAtomic p (\h -> B.hPut h bs *> return (Nothing, ())) -- | Do something when a file exists. whenFileExists :: MonadIO m => RawFilePath -> a -- ^ Return this when the file doesn't exist. -> m a -- ^ Do this when it does. -> m a whenFileExists p a io = do exists <- liftIO $ fileExist p if exists then io else return a -- | Like 'whenFileExists', with 'empty' as a default. mwhenFileExists :: (MonadIO m, Alternative f) => RawFilePath -> m a -> m (f a) mwhenFileExists p io = whenFileExists p empty (pure <$> io) -- | Lists the files in a directory. getRawDirectoryContents :: RawFilePath -> IO [RawFilePath] getRawDirectoryContents dir = bracket (openDirStream dir) closeDirStream go where go ds = do f <- readDirStream ds if f == "" then return [] else (f:) <$> go ds -- | This is stolen directly from 'System.Directory.createDirectoryIfMissing', swapped to use -- functions from 'System.Posix.FilePath'. NB: due to the absence of a @normalise@ function in the -- latter module, this function doesn't do any normalization. createRawDirectoryIfMissing :: Bool -> RawFilePath -> IO () createRawDirectoryIfMissing create_parents path0 | create_parents = createDirs (parents path0) | otherwise = createDirs (take 1 (parents path0)) where parents = reverse . scanl1 () . splitDirectories {- . normalise -} createDirs [] = return () createDirs (dir:[]) = createDir dir ioError createDirs (dir:dirs) = createDir dir $ \_ -> do createDirs dirs createDir dir ioError createDir dir notExistHandler = do r <- tryIOError (createDirectory dir dirmode) case r of Right () -> return () Left e | isDoesNotExistError e -> notExistHandler e | isAlreadyExistsError e || isPermissionError e -> do canIgnore <- isDir `catchIOError` \ _ -> return (isAlreadyExistsError e) unless canIgnore (ioError e) | otherwise -> ioError e where isDir = (isDirectory <$> getFileStatus dir) dirmode = foldr unionFileModes directoryMode dirmodes dirmodes = [ ownerModes, groupReadMode, groupExecuteMode , otherReadMode, otherExecuteMode ] -- | A version of 'System.Directory.withCurrentDirectory' using a 'RawFilePath'. withRawCurrentDirectory :: RawFilePath -> IO a -> IO a withRawCurrentDirectory dir action = bracket getWorkingDirectory changeWorkingDirectory $ \ _ -> do changeWorkingDirectory dir action