packages feed

dropsolve-0.1: dropsolve.hs

{-# LANGUAGE ScopedTypeVariables #-}

import System (getArgs)
import System.Directory
import System.IO
import System.IO.Error (IOErrorType)
import System.FilePath
import System.Environment
import System.Process
import System.Exit
import Data.Time.Clock
import Data.Time.Calendar
import Data.List (isInfixOf, isSuffixOf, concat)
import Data.Char (intToDigit, digitToInt, isDigit, toUpper)
import Text.Regex.Posix ((=~))
import Control.Monad (when, mapM_, filterM)
import Control.Exception.Base (try)

main = do
   hSetBuffering stdout NoBuffering
   hSetBuffering stdin  NoBuffering
   args <- getArgs
   case args of
	[]            -> printUsage >> printHelp
        ("-h":[])     -> printUsage >> printHelp
	("--help":[]) -> printUsage >> printHelp
	(dropDir:[])  -> resolve dropDir
	otherwise     -> error $ "Invalid Arguments\n" ++ usage

printUsage = putStrLn usage
usage = "\nUsage: dropsolve DROPBOXDIR"

printHelp = do
   trashDir <- trashDirectory
   putStrLn $ ""
   putStrLn $ "Runtime options:"
   putStrLn $ "   Take File (NUM) => By pressing a digit, the conflicting file with the"
   putStrLn $ "                      digit NUM is used as the new version. A copy of the"
   putStrLn $ "                      current file and the other conflicting files is put"
   putStrLn $ "                      into the trash directory (" ++ trashDir ++ ")."
   putStrLn $ ""
   putStrLn $ "   Move to (T)rash => By pressing 'T' or 't', all conflicting files are"
   putStrLn $ "                      moved into the trash directory (" ++ trashDir ++ ")."
   putStrLn $ ""
   putStrLn $ "   Show (D)iff     => By pressing 'D' or 'd', the difference between the first"
   putStrLn $ "                      and the second conflicting file is shown. The diff tool"
   putStrLn $ "                      can be specified by the user by setting the environment"
   putStrLn $ "                      variable 'DROPSOLVE_DIFF'. The default diff tool is 'gvimdiff -f'."
   putStrLn $ ""
   putStrLn $ "   (S)kip          => By pressing 'S' or 's', the current conflict is skipped"
   putStrLn $ "                      and the next one is shown."
   putStrLn $ ""
   putStrLn $ "   (Q)uit          => By pressing 'Q' or 'q', the application is quit."
   putStrLn $ ""
   putStrLn $ "   (H)elp          => By pressing 'H' or 'h', this help is printed."
   putStrLn $ ""

resolve file = do
   dirExists <- doesDirectoryExist file
   if dirExists
      then do
	 entries <- getDirContents file
	 mapM_ (\e -> resolve $ file </> e) entries
      else do
	 fileExists <- doesFileExist file
	 when (fileExists && hasConflict file) $
	    handleConflict file

hasConflict file = "conflicted copy" `isInfixOf` file

handleConflict file = do
   exist <- doesFileExist file
   when exist $ do
      let confInfo = conflictInfo file
      confFiles <- findConflicting confInfo
      resolveConflict confInfo confFiles 

   where
      findConflicting confInfo = do
	 let d  = dir confInfo
	     fn = fileName confInfo
	 entries <- getDirContents d
	 let confs = filter (isConfFile fn) entries
	 mapM (\e -> return $ d </> e) confs
	 where
	    isConfFile file = \e -> file `isInfixOf` e && hasConflict e

      resolveConflict confInfo confFiles
	 | length confFiles == 0 = return ()
	 | otherwise = do
	    let origFile = dir confInfo </> fileName confInfo
	    putStrLn $ "\nConflicting file: " ++ origFile
            putConfFiles confFiles 1
	    askUser confInfo confFiles

      putConfFiles (c:cs) num = do
	 let confInfo = conflictInfo c
	     h        = host confInfo
	     d        = date confInfo
	     digit    = intToDigit num
	 putStrLn $ "   (" ++ [digit] ++ ") " ++ h ++ " from " ++ d
	 putConfFiles cs (num + 1)
      putConfFiles [] _ = return ()

      askUser confInfo confFiles = do
	 putStr "\nTake File (NUM) | Move to (T)rash | Show (D)iff | (S)kip | (Q)uit | (H)elp : " 
	 char <- getChar
	 let numConfs = length confFiles
	     askAgain = askUser confInfo confFiles
	 case toUpper char of
	      c | c == 'D' && numConfs >= 2             -> showDiff (head confFiles) (confFiles !! 1) >> askAgain
	        | c == 'T'                              -> mapM_ (\c -> moveToTrash c) confFiles  
	        | c == 'S'                              -> return ()
		| c == 'Q'                              -> exitSuccess
		| c == 'H'                              -> printHelp >> askAgain
		| c == '?'                              -> printHelp >> askAgain
		| isDigit c && digitToInt c <= numConfs -> takeFile (digitToInt c) confInfo confFiles
		| otherwise                             -> askAgain
	 where
	    moveToTrash file = do
	       trashDir <- trashDirectory
	       createDirectoryIfMissing True trashDir
               let (dir, fileName) = splitFileName file
	       copyFile file (trashDir </> fileName) 
	       removeFile file

	    takeFile num confInfo confFiles = do
               (year, month, day) <- getCurrentDate
	       let idx        = num - 1
                   file       = confFiles !! idx
		   origFile   = dir confInfo </> fileName confInfo
		   origBackup = origFile ++ "_backup_" ++ show year ++ "-" ++ show month ++ "-" ++ show day
	       copyFile origFile origBackup
	       moveToTrash origBackup
	       copyFile file origFile
	       mapM_ (\c -> moveToTrash c) confFiles

	    showDiff file1 file2 = do
	       putStrLn ""
	       diff <- getEnvOrDefault "DROPSOLVE_DIFF" defaultDiff
	       handle <- runCommand $ diff ++ " " ++ quote file1 ++ " " ++ quote file2
	       waitForProcess handle
	       return ()

	    quote string = "\"" ++ string ++ "\""

-- conflicting file info
data ConflictInfo = ConflictInfo {
   filePath :: String,
   dir      :: String,
   fileName :: String,
   host     :: String,
   date     :: String }

conflictInfo :: FilePath -> ConflictInfo
conflictInfo filePath =
   let (_:dir:fileName:host:date:[]) = concat (filePath =~ regex :: [[String]])
       in ConflictInfo filePath dir fileName host date
   where
      regex = "(.*)" </> "(.*) \\((.*) conflicted copy (.*)\\).*"


getDirContents dir = do
   entries <- getDirectoryContents dir
   filterM notDots entries
   where
      notDots entry = return . not $ "." `isSuffixOf` entry || ".." `isSuffixOf` entry


appDirectory   = getAppUserDataDirectory "dropsolve"
trashDirectory = appDirectory >>= \d -> return $ d </> "trash" 

defaultDiff = "gvimdiff -f"

getCurrentDate :: IO (Integer,Int,Int) -- :: (year,month,day)
getCurrentDate = getCurrentTime >>= return . toGregorian . utctDay

getEnvOrDefault envVar defaultValue = do
   result <- try $ getEnv envVar
   case result of
	Right value          -> return value
	Left  (_ :: IOError) -> return defaultValue