-- | -- Module : System.PlanB -- Copyright : © 2016–2017 Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- Failure-tolerant file and directory editing. All functions here can -- recover from exceptions thrown while they work. They bring file-system -- into the state it was in before specific function was called. Temporary -- files and backups are handled automatically. {-# LANGUAGE DataKinds #-} module System.PlanB ( -- * Operations on files withNewFile , withExistingFile -- * Operations on directories , withNewDir , withExistingDir -- * Operations on containers , withNewContainer , withExistingContainer -- * Configuration options , tempDir , nameTemplate , preserveCorpse , moveByRenaming , overrideIfExists , useIfExists ) where import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class (MonadIO (..)) import Data.Bool (bool) import Data.Maybe (fromMaybe) import Path import System.IO.Error import System.PlanB.Type import qualified Path.IO as P ---------------------------------------------------------------------------- -- Operations on files -- | Create a new file. Name of the file is taken as the second argument. -- The third argument allows to perform actions (in simplest case just -- creation of file), result of those actions should be new file with the -- given file name. -- -- This action throws 'alreadyExistsErrorType' by default instead of -- silently overwriting already existing file, use 'overrideIfExists' and -- 'useIfExists' to change this behavior. withNewFile :: (MonadIO m, MonadMask m) => PbConfig 'New -- ^ Configuration -> Path b File -- ^ Name of file to create -> (Path Abs File -> m a) -- ^ Given name of temporary file, do it -> m a withNewFile pbc fpath action = withTempDir pbc $ \tdir -> do let apath = constructFilePath tdir fpath checkExistenceOfFile pbc apath fpath liftM2 const (action apath) (moveFile pbc apath fpath) -- | Edit an existing file. Name of the file is taken as the second -- argument. The third argument allows to perform actions on temporary copy -- of the specified file. -- -- This action throws 'doesNotExistErrorType' exception if target file does -- not exist. withExistingFile :: (MonadIO m, MonadMask m) => PbConfig 'Existing -- ^ Configuration -> Path b File -- ^ Name of file to edit -> (Path Abs File -> m a) -- ^ Given name of temporary file, do it -> m a withExistingFile pbc fpath action = withTempDir pbc $ \tdir -> do let apath = constructFilePath tdir fpath copyFile fpath apath liftM2 const (action apath) (moveFile pbc apath fpath) ---------------------------------------------------------------------------- -- Operations on directories -- | Create a new directory. Name of the directory is specified as the -- second argument. The third argument allows to perform actions in -- “sandboxed” version of new directory. -- -- This action throws 'alreadyExistsErrorType' by default instead of -- silently overwriting already existing directory, use 'overrideIfExists' -- and 'useIfExists'to change this behavior. withNewDir :: (MonadIO m, MonadMask m) => PbConfig 'New -- ^ Configuration -> Path b Dir -- ^ Name of directory to create -> (Path Abs Dir -> m a) -- ^ Given name of temporary directory, do it -> m a withNewDir pbc dpath action = withTempDir pbc $ \tdir -> do checkExistenceOfDir pbc tdir dpath liftM2 const (action tdir) (moveDir pbc tdir dpath) -- | Edit an existing directory. Name of the directory is specified as the -- second argument. The third argument allows to perform actions in -- “sandboxed” copy of target directory. -- -- This action throws 'doesNotExistErrorType' exception if target directory -- does not exist. withExistingDir :: (MonadIO m, MonadMask m) => PbConfig 'Existing -- ^ Configuration -> Path b Dir -- ^ Name of directory to edit -> (Path Abs Dir -> m a) -- ^ Given name of temporary directory, do it -> m a withExistingDir pbc dpath action = withTempDir pbc $ \tdir -> do copyDir dpath tdir liftM2 const (action tdir) (moveDir pbc tdir dpath) ---------------------------------------------------------------------------- -- Operations on containers -- | Create a new container file. This is suitable for processing of all -- sorts of archive-like objects. The first and second arguments specify how -- to unpack directory from file and pack it back. The fourth argument names -- the new file. The fifth argument allows to perform actions knowing name -- of temporary directory. -- -- This action throws 'alreadyExistsErrorType' by default instead of -- silently overwriting already existing file, use 'overrideIfExists' and -- 'useIfExists'to change this behavior. withNewContainer :: (MonadIO m, MonadMask m) => (Path Abs File -> Path Abs Dir -> m ()) -- ^ How to unpack file into specified directory -> (Path Abs Dir -> Path b File -> m ()) -- ^ How to pack specified directory into file -> PbConfig 'New -- ^ Configuration -> Path b File -- ^ Name of container to create -> (Path Abs Dir -> m a) -- ^ Given name of temporary directory, do it -> m a withNewContainer unpack pack pbc fpath action = withTempDir pbc $ \tdir -> do withTempDir pbc $ \udir -> do let apath = constructFilePath udir fpath checkExistenceOfFile pbc apath fpath using <- P.doesFileExist apath when using (unpack apath tdir) liftM2 const (action tdir) (pack tdir fpath) -- | Edit an existing container file. This is suitable for processing of all -- sorts of archive-like objects. The first and second arguments specify how -- to unpack directory from file and pack it back (overwriting old version). -- Fourth argument names container file to edit. The last argument allows to -- perform actions knowing name of temporary directory. -- -- This action throws 'doesNotExistErrorType' exception if target file does -- not exist. withExistingContainer :: (MonadIO m, MonadMask m) => (Path b File -> Path Abs Dir -> m ()) -- ^ How to unpack file into specified directory -> (Path Abs Dir -> Path b File -> m ()) -- ^ How to pack specified directory into file -> PbConfig 'Existing -- ^ Configuration -> Path b File -- ^ Name of container to edit -> (Path Abs Dir -> m a) -- ^ Given name of temporary directory, do it -> m a withExistingContainer unpack pack pbc fpath action = withTempDir pbc $ \tdir -> do unpack fpath tdir liftM2 const (action tdir) (pack tdir fpath) ---------------------------------------------------------------------------- -- Helpers -- | Use a temporary directory. This action is controlled by the supplied -- configuration, see 'HasTemp'. The temporary directory is removed -- automatically when given action finishes, although this can be changed -- via the mentioned configuration value too. If given action finishes -- successfully, temporary directory is always deleted. withTempDir :: (HasTemp c, MonadIO m, MonadMask m) => c -- ^ Configuration -> (Path Abs Dir -> m a) -- ^ Action to perform with the temporary file -> m a withTempDir pbc action = bracketOnError make freeOptionally $ \dir -> liftM2 const (action dir) (freeAlways dir) where make = do tsys <- P.getTempDir let tdir = fromMaybe tsys (getTempDir pbc) P.createDirIfMissing True tdir let ntmp = fromMaybe "plan-b" (getNameTemplate pbc) P.createTempDir tdir ntmp freeAlways = ignoringIOErrors . P.removeDirRecur freeOptionally = unless (getPreserveCorpse pbc) . freeAlways -- | Construct name of file combining given directory path and file name -- from path to file. constructFilePath :: Path Abs Dir -- ^ Directory name -> Path b File -- ^ Get file name from this path -> Path Abs File -- ^ Resulting path constructFilePath dir file = dir filename file -- | Check existence of file and perform actions according to the given -- configuration. By default we throw 'alreadyExistsErrorType' unless the -- user has specified different 'AlreadyExistsBehavior'. If it's -- 'AebOverride', then we don't need to do anything, file will be -- overwritten automatically, if we have 'AebUse', then we should copy it -- into given directory. checkExistenceOfFile :: (CanHandleExisting c, MonadIO m) => c -- ^ Configuration -> Path Abs File -- ^ Where to copy file (when we have 'AebUse') -> Path b File -- ^ File to check -> m () checkExistenceOfFile pbc apath fpath = liftIO $ do let ffile = toFilePath fpath location = "System.PlanB.checkExistenceOfFile" exists <- P.doesFileExist fpath when exists $ case howHandleExisting pbc of Nothing -> throwM $ mkIOError alreadyExistsErrorType location Nothing (Just ffile) Just AebOverride -> return () Just AebUse -> copyFile fpath apath -- | Check existence of directory and perform actions according to the given -- configuration. See 'checkExistenceOfFile', overall behavior is the same. checkExistenceOfDir :: (CanHandleExisting c, MonadIO m) => c -- ^ Configuration -> Path Abs Dir -- ^ Where to copy directory (when we have 'AebUse') -> Path b Dir -- ^ Directory to check -> m () checkExistenceOfDir pbc apath dpath = liftIO $ do let ddir = toFilePath dpath location = "System.PlanB.checkExistenceOfDir" exists <- P.doesDirExist dpath when exists $ case howHandleExisting pbc of Nothing -> throwM $ mkIOError alreadyExistsErrorType location Nothing (Just ddir) Just AebOverride -> return () Just AebUse -> copyDir dpath apath -- | Move a specified file to another location. File can be moved either by -- copying or by renaming, exact method is determined by the supplied -- configuration. moveFile :: (HasTemp c, MonadIO m) => c -- ^ Configuration -> Path b0 File -- ^ Original location -> Path b1 File -- ^ Where to move -> m () moveFile pbc = bool P.copyFile P.renameFile (getMoveByRenaming pbc) -- | Move a specified directory to another location. If destination location -- is already occupied, delete that object first. Directory can be moved -- either by copying or by renaming, exact method is determined by the -- supplied configuration. moveDir :: (HasTemp c, MonadIO m, MonadCatch m) => c -- ^ Configuration -> Path b0 Dir -- ^ Original location -> Path b1 Dir -- ^ Where to move -> m () moveDir pbc src dest = do exists <- P.doesDirExist dest when exists (P.removeDirRecur dest) bool P.copyDirRecur P.renameDir (getMoveByRenaming pbc) src dest -- | Copy a file to a new location. Throw 'doesNotExistErrorType' if it does -- not exist. copyFile :: MonadIO m => Path b0 File -- ^ Original location -> Path b1 File -- ^ Where to put copy of the file -> m () copyFile src dest = liftIO $ do let fsrc = toFilePath src location = "System.PlanB.copyFile" exists <- P.doesFileExist src if exists then P.copyFile src dest else throwM $ mkIOError doesNotExistErrorType location Nothing (Just fsrc) -- | Copy contents of one directory into another (recursively). Source -- directory must exist, otherwise 'doesNotExistErrorType' is thrown. -- Destination directory will be created if it doesn't exist. copyDir :: (MonadIO m, MonadCatch m) => Path b0 Dir -- ^ Original location -> Path b1 Dir -- ^ Where to put copy of the directory -> m () copyDir src dest = do let fsrc = toFilePath src location = "System.PlanB.copyDir" exists <- P.doesDirExist src if exists then P.copyDirRecur src dest else throwM $ mkIOError doesNotExistErrorType location Nothing (Just fsrc) -- | Perform an action ignoring IO exceptions it may throw. ignoringIOErrors :: MonadCatch m => m () -> m () ignoringIOErrors ioe = ioe `catch` handler where handler :: MonadThrow m => IOError -> m () handler = const (return ())