archive-1.2.12: System/Archive/Prune.hs
-- |Limit the number of incremental backups.
module System.Archive.Prune
( prune
, nextVictim
) where
import Control.Monad (when)
import Data.Function (on)
import Data.List (sortBy, stripPrefix)
import Data.Maybe (mapMaybe)
import Data.Time (UTCTime, NominalDiffTime, parseTime, formatTime, diffUTCTime, zonedTimeToUTC)
import System.Directory (getDirectoryContents)
import System.Exit (ExitCode(ExitSuccess))
import System.FilePath ((</>))
import System.IO (hPutStr, stderr)
import System.Locale (defaultTimeLocale)
import System.Process (readProcessWithExitCode, showCommandForUser)
-- |Remove backups until we have a certain number left.
prune :: String -> FilePath -> String -> Int -> IO ()
prune dateFormat baseDirName prefix keep =
do paths <- getDirectoryContents baseDirName
let times = sortBy (flip compare) . mapMaybe (timeFromPath dateFormat prefix) $ paths
when -- Always keep two: the oldest and the newest
(length times > max 2 keep)
(nextVictim dateFormat baseDirName prefix >>= \ victim ->
maybe (return ()) rmrf victim >>
prune dateFormat baseDirName prefix keep)
-- |Decide which archives is the least important
nextVictim :: String -> FilePath -> String -> IO (Maybe FilePath)
nextVictim dateFormat baseDirName prefix =
do paths <- getDirectoryContents baseDirName
-- Compute the backup times and sort newest first.
let times = sortBy (flip compare) . mapMaybe (timeFromPath dateFormat prefix) $ paths
case times of
[] -> return Nothing
(newest : older) -> do
let ages = map (diffUTCTime newest) older
-- For each backup, compute the interval from the
-- backup before to the backup after.
intervals = map (uncurry diffUTCTime) (zip (newest : older) (tail older))
-- The importance of the backup is the quotient of
-- the interval just computed and the backup's age.
importance :: [(UTCTime, NominalDiffTime)]
importance = zip older (map (uncurry (/)) (zip intervals ages))
victim :: UTCTime
victim = fst . head $ sortBy (compare `on` snd) importance
let path :: FilePath
path = baseDirName </> (prefix ++ formatTime defaultTimeLocale "%F_%T" victim)
return $ Just path
-- |Parse the date string in a backup directory name.
timeFromPath :: String -> FilePath -> FilePath -> Maybe UTCTime
timeFromPath dateFormat prefix path =
stripPrefix prefix path >>=
parseTime defaultTimeLocale dateFormat >>=
return . zonedTimeToUTC
-- |Remove a directory and its contents.
rmrf :: FilePath -> IO ()
rmrf path = do
let cmd = "rm"
args = ["-rf", path]
hPutStr stderr ("Removing backup " ++ path ++ "...")
(code, _, _) <- readProcessWithExitCode cmd args ""
case code of
ExitSuccess -> hPutStr stderr "done.\n"
code -> error $ showCommandForUser cmd args ++ " -> " ++ show code