packages feed

minirotate-0.1.2.1: src/Main.hs

module Main where

import System.Time
import System.FilePath
import System.Directory
import System.Environment
import System.IO ( hPutStrLn, stderr )
import System.Exit ( exitFailure )

import Data.Accessor
import Data.Accessor.Tuple
import Data.Accessor.Basic ( get )
import Data.Maybe
import Data.List ( sort, partition, intercalate )

import Control.Monad ( when )
import Control.Applicative ( (<$>) )

import Text.Printf

import Paths_minirotate ( version )
import Data.Version ( showVersion )

import Debug.Trace

-- local imports
import Options
import Pattern

data FileOrDir = File | Dir deriving (Show,Ord,Eq)

-- | auxilary function. to be replaced by proper logging.
logErr str = hPutStrLn stderr str

main :: IO ()
main = do
  args <- getArgs
  case parseOptions args of
    Left errmsg -> logErr errmsg >> exitFailure
    Right (locs,(envO,runO)) -> do
         when (get showHelp $ envO) (logErr usage)
         when (get showVers $ envO) (logErr $ printf "minirotate version %s" (showVersion version))
         when (get showDefs $ envO) (logErr $ printf "Defaults:\n\t%s\n\t%s\n"
                                             (show (def :: EnvOptions))
                                             (show (def :: RunOptions)))
         when (get continue $ envO) (runRotate runO locs)



copyOpDry :: Bool -> CopyMode -> (FilePath -> FilePath -> IO ())
copyOpDry True Copy = \from to -> logErr (printf "COPY: %s -> %s" from to)
copyOpDry True Move = \from to -> logErr (printf "MOVE: %s -> %s" from to)
copyOpDry False Copy = copyFile
copyOpDry False Move = renameFile

removeFileDry True fn = logErr (printf "REMOVE: %s" fn)
removeFileDry False fn = removeFile fn

runRotate :: RunOptions -> [FilePath] -> IO ()
runRotate opts locs | length locs < 2 = logErr "Need at least one source location and destination\n" >> exitFailure
                    | otherwise = do

  -- check pattern
  runPattern <- case compilePattern (get filePattern $ opts) of
                  Left err -> logErr ("Pattern error: " ++ err) >> exitFailure
                  Right pat -> return pat

  let -- if source location is empty string "" we skip it.
      locFrom = (filter (not . null)) $ init locs
      locTo = last locs

      -- | oneLocation firstLevel? (element type, element name)
      oneLocation _ (File,fp) = do
        modTime <- getModificationTime fp
        (copyOpDry (get dryRun $ opts) (get copyMode $ opts)) fp (locTo </> (runPattern modTime fp))
      oneLocation True (Dir,fp) = do
        logErr (printf "Directory %s is a subdirectory at source site. Recursive operation is not supported. Skipping."
                        (show fp))
      oneLocation False (Dir,fp) = do
        cont <- getDirectoryContentsRooted fp
        mapM fileOrDir cont >>= mapM_ (either logErr (oneLocation True))

  -- check for correct destination
  dest <- fileOrDir locTo
  case dest of
    Left err -> logErr ("Problem with destination:" ++ err) >> exitFailure
    Right (File,fp) -> logErr ("Destination should be directory, not file: " ++ (show fp))  >> exitFailure
    _ -> return () -- all ok.

  -- for each location decide if it's file or directory.
  -- then copy file to destination or report error.
  (sort <$> mapM fileOrDir locFrom) >>= mapM_ (either logErr (oneLocation False))

  -- rotate files in final directory
  rotateFiles opts locTo

-- | rotate files in location directory
rotateFiles :: RunOptions -> FilePath -> IO ()
rotateFiles opts loc = do
  -- time stuff
  now <- getClockTime

  -- get files in directory, filter out directories
  let checkFileGetModTime (Left err) = logErr ("rotateFiles: bad file: " ++ err) >> return Nothing
      checkFileGetModTime (Right (Dir,fp)) = logErr ("rotateFiles: skipping directory: " ++ show fp) >> return Nothing
      checkFileGetModTime (Right (File,fp)) = do
                         mt <- getModificationTime fp
                         return (Just (fp,mt))

  -- important note: files are sorted by (modification time,name)
  cont <- sort <$> (mapM fileOrDir =<< getDirectoryContentsRooted loc)
  contM <- catMaybes <$> mapM checkFileGetModTime cont

  -- now apply the rules (in order):
  -- 1. Delete too old files (by mod time)
  -- 2. Delete exceeding files (by filename)
  -- 3. Undelete to keep minimum count of files (by filename)

  let -- here is the implementation of rules. note the nice symmetry.
      -- 1.
      maxAge = addToClockTime (noTimeDiff { tdSec = negate (get maximumAge $ opts) }) now
      tooOld (todel,tokeep) = let (newdel,keep) = partition (\(fp,mt) -> mt < maxAge) tokeep
                              in {- tS 1 -}  ((newdel ++ todel),keep)
      -- 2.
      tooMany (todel,tokeep) = let (newdel,keep) = splitAt (length contM - (get maximumFiles $ opts)) (sort tokeep)
                               in {- tS 2 -} ((newdel ++ todel),keep)
      -- 3.
      keepMin (todel,tokeep) = let (del,newkeep) = splitAt (length contM - (get minimumFiles $ opts)) (sort todel)
                               in {- tS 3 -} (del,newkeep ++ tokeep)

      getDeletedFiles = map fst . fst
      getKeptFiles = map fst . snd
      partitionedFiles = (keepMin . tooMany . tooOld) $ ([],contM)

{-
  print ("maxAge",maxAge)
  print ("now",now)

  putStrLn "DELETE:"
  print (getDeletedFiles partitionedFiles)
  putStrLn "KEEP:"
  print (getKeptFiles partitionedFiles)
-}

  mapM_ (removeFileDry (get dryRun $ opts)) (getDeletedFiles partitionedFiles)

  return ()


-- | * A few auxilary functions

getDirectoryContentsRooted loc = map (loc </>) . filterSpecial <$> getDirectoryContents loc
-- | remove special directory elements ('.','..')
filterSpecial = (filter (\fp -> not (fp `elem` [".",".."])))
fileOrDir fp = do
  f <- doesFileExist fp
  d <- doesDirectoryExist fp
  return $ case () of
    _ | f -> Right (File,fp)
      | d -> Right (Dir,fp)
      | otherwise -> Left (printf "Neither file nor directory: %s" (show fp))


-- debug stuff
tS tag xs = let mapf' = showX . map fst
                mapf  = showY
            in
             traceShow (printf "%d. DEL: %s KEEP: %s"
                              (tag :: Int)
                              (mapf . fst $ xs)
                              (mapf . snd $ xs)
                       :: String
                      )
                      xs

showY xs = "[" ++ intercalate ", " (map (\(n,t) -> "(" ++ n ++ "," ++ show t ++ ")" ) xs) ++ "]"
showX xs = "[" ++ intercalate ", " xs ++ "]"