packages feed

amqp-utils-0.6.2.2: agitprop.hs

{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}

-- generic AMQP publisher
import           Control.Concurrent
import qualified Control.Exception             as X
import qualified Data.ByteString.Lazy.Char8    as BL
#if linux_HOST_OS
#if MIN_VERSION_hinotify(0,3,10)
import qualified Data.ByteString.Char8         as BS
#endif
import           Data.List                     (isSuffixOf)
#endif
import           Data.Maybe
import qualified Data.Text                     as T
import           Data.Time
import           Data.Time.Clock.POSIX
import           Data.Version                  (showVersion)
import           Data.Word                     (Word64)
import           Magic
import           Network.AMQP
import           Network.AMQP.Types
import           Network.AMQP.Utils.Connection
import           Network.AMQP.Utils.Helpers
import           Network.AMQP.Utils.Options
import           Paths_amqp_utils              (version)
import           System.Directory
import           System.Environment
import           System.Exit
import           System.FilePath.Posix
#if linux_HOST_OS
import           System.INotify
#endif
import qualified System.Posix.Files            as F

main :: IO ()
main = do
  hr "starting"
  tid <- myThreadId
  args <- getArgs >>= parseargs 'a'
  printparam "client version" ["amqp-utils", showVersion version]
  printparam "routing key" $ rKey args
  printparam "exchange" $ currentExchange args
  isDir <-
    if inputFile args == "-"
      then return False
      else F.getFileStatus (inputFile args) >>= return . F.isDirectory
  if isDir
    then printparam "hotfolder" (inputFile args) >>
         printparam "initial scan" (initialScan args) >>
         if isNothing (moveSentFileTo args)
           then printparam "remove sent file" (removeSentFile args)
           else printparam "move sent file to" (moveSentFileTo args)
    else printparam
           "input file"
           [ inputFile args
           , if (lineMode args)
               then "(line-by-line)"
               else ""
           ]
  (conn, chan) <- connect args
  addChannelExceptionHandler chan (X.throwTo tid)
  printparam "confirm mode" $ confirm args
  if (confirm args)
    then do
      confirmSelect chan False
      addConfirmationListener chan confirmCallback
    else return ()
  let publishOneMsg =
        publishOneMsg' chan args {removeSentFile = removeSentFile args && isDir}
  X.catch
    (if isDir
       then do
#if linux_HOST_OS
         setCurrentDirectory (inputFile args)
         if (initialScan args)
           then getDirectoryContents "." >>=
                mapM_ (\fn -> handleFile publishOneMsg (suffix args) fn)
           else return ()
         inotify <- initINotify
         wd <-
           addWatch
             inotify
             [CloseWrite, MoveIn]
             "."
             (handleEvent publishOneMsg (suffix args))
         hr $ "BEGIN watching " ++ (inputFile args)
         sleepingBeauty >>= printparam "exception"
         removeWatch wd
         hr $ "END watching " ++ (inputFile args)
#else
         X.throw (X.ErrorCall "ERROR: watching a directory is only supported in Linux")
#endif
       else do
         hr $ "BEGIN sending"
         messageFile <-
           if inputFile args == "-"
             then BL.getContents
             else BL.readFile (inputFile args)
         if (lineMode args)
           then mapM_ (publishOneMsg Nothing) (BL.lines messageFile)
           else publishOneMsg (Just (inputFile args)) messageFile
         hr "END sending")
    exceptionHandler
  -- all done. wait and close.
  if (confirm args)
    then waitForConfirms chan >>= printparam "confirmed"
    else return ()
  X.catch (closeConnection conn) exceptionHandler

-- | A handler for clean exit
exceptionHandler :: AMQPException -> IO ()
exceptionHandler (ChannelClosedException Normal txt) =
  printparam "exit" txt >> exitWith ExitSuccess
exceptionHandler (ConnectionClosedException Normal txt) =
  printparam "exit" txt >> exitWith ExitSuccess
exceptionHandler x = printparam "exception" x >> exitWith (ExitFailure 1)

-- | The handler for publisher confirms
confirmCallback :: (Word64, Bool, AckType) -> IO ()
confirmCallback (deliveryTag, isAll, ackType) =
  printparam
    "confirmed"
    [ show deliveryTag
    , if isAll
        then "all"
        else "this"
    , show ackType
    ]

#if linux_HOST_OS
-- | Hotfolder event handler
handleEvent ::
     (Maybe String -> BL.ByteString -> IO ()) -> [String] -> Event -> IO ()
-- just handle closewrite and movedin events
#if MIN_VERSION_hinotify(0,3,10)
handleEvent func suffixes (Closed False (Just fileName) True) =
  handleFile func suffixes (BS.unpack fileName)
handleEvent func suffixes (MovedIn False fileName _) =
  handleFile func suffixes (BS.unpack fileName)
#else
handleEvent func suffixes (Closed False (Just fileName) True) =
  handleFile func suffixes fileName
handleEvent func suffixes (MovedIn False fileName _) =
  handleFile func suffixes fileName
#endif
handleEvent _ _ _ = return ()

-- | Hotfolder file handler
handleFile ::
     (Maybe String -> BL.ByteString -> IO ()) -> [String] -> FilePath -> IO ()
handleFile _ _ ('.':_) = return () -- ignore hidden files
handleFile func suffixes@(_:_) fileName =
  if any (flip isSuffixOf fileName) suffixes
    then handleFile func [] fileName
    else return ()
handleFile func [] fileName =
  X.catch
    (BL.readFile fileName >>= func (Just fileName))
    (\e -> printparam "exception in handleFile" (e :: X.IOException))
#endif

-- | Publish one message with our settings
publishOneMsg' :: Channel -> Args -> Maybe FilePath -> BL.ByteString -> IO ()
publishOneMsg' chan a fn content = do
  printparam "sending" fn
  (mtype, mencoding) <-
    if (magic a) && isJust fn
      then do
        m <- magicOpen [MagicMimeType]
        magicLoadDefault m
        t <- magicFile m (fromJust fn)
        magicSetFlags m [MagicMimeEncoding]
        e <- magicFile m (fromJust fn)
        return (Just (T.pack t), Just (T.pack e))
      else return ((contenttype a), (contentencoding a))
  now <- getCurrentTime >>= return . floor . utcTimeToPOSIXSeconds
  publishMsg
    chan
    (T.pack $ currentExchange a)
    (T.pack $ rKey a)
    newMsg
      { msgBody = content
      , msgDeliveryMode = persistent a
      , msgTimestamp = Just now
      , msgID = msgid a
      , msgType = msgtype a
      , msgUserID = userid a
      , msgApplicationID = appid a
      , msgClusterID = clusterid a
      , msgContentType = mtype
      , msgContentEncoding = mencoding
      , msgReplyTo = replyto a
      , msgPriority = prio a
      , msgCorrelationID = corrid a
      , msgExpiration = msgexp a
      , msgHeaders = substheader (fnheader a) fn $ msgheader a
      } >>=
    printparam "sent"
  removeSentFileIfRequested (removeSentFile a) (moveSentFileTo a) fn
  where
    substheader ::
         [String] -> Maybe String -> Maybe FieldTable -> Maybe FieldTable
    substheader (s:r) (Just fname) old =
      substheader r (Just fname) (addheader old (s ++ "=" ++ fname))
    substheader _ _ old = old
    removeSentFileIfRequested False _ _ = return ()
    removeSentFileIfRequested True _ Nothing = return ()
    removeSentFileIfRequested True Nothing (Just fname) =
      printparam "removing" fname >> removeFile fname
    removeSentFileIfRequested True (Just path) (Just fname) =
      printparam "moving" (fname ++ " to " ++ path) >>
      renameFile fname (replaceDirectory fname path)