1 -- SPDX-FileCopyrightText: 2022 Frank Doepper
3 -- SPDX-License-Identifier: GPL-3.0-only
6 {-# LANGUAGE OverloadedStrings #-}
8 -- generic AMQP publisher
9 import Control.Concurrent
10 import qualified Control.Exception as X
11 import Control.Monad (forM_)
12 import qualified Data.ByteString.Char8 as BS
13 import qualified Data.ByteString.Lazy.Char8 as BL
14 import qualified Data.Map as M
16 import qualified Data.Text as T
18 import Data.Time.Clock.POSIX
19 import Data.Version (showVersion)
20 import Data.Word (Word64)
23 import Network.AMQP.Types
24 import Network.AMQP.Utils.Connection
25 import Network.AMQP.Utils.Helpers
26 import Network.AMQP.Utils.Options
27 import Paths_amqp_utils (version)
28 import qualified RawFilePath.Directory as RD
29 import System.Environment
31 import System.FilePath.Posix.ByteString
35 import qualified System.Posix.Files.ByteString as F
42 args <- getArgs >>= parseargs 'a'
43 hSetBuffering stdout LineBuffering
44 hSetBuffering stderr LineBuffering
45 printparam "client version" ["amqp-utils", showVersion version]
46 printparam "routing key" $ rKey args
47 printparam "exchange" $ currentExchange args
48 (conn, chan) <- connect args
49 addChannelExceptionHandler chan (X.throwTo tid)
50 printparam "confirm mode" $ confirm args
53 confirmSelect chan False
54 addConfirmationListener chan confirmCallback
56 let inputFile' = firstInputFile (inputFiles args)
60 else F.getFileStatus inputFile' >>= return . F.isDirectory
62 publishOneMsg' chan args {removeSentFile = removeSentFile args && isDir}
65 printparam "hotfolder mode" True
66 printparam "initial scan" (initialScan args)
67 if isNothing (moveSentFileTo args)
68 then printparam "remove sent file" (removeSentFile args)
69 else printparam "move sent file to" (moveSentFileTo args)
81 wds <- mapM (watchHotfolder args publishOneMsg) (inputFiles args)
82 sleepingBeauty >>= (\x -> do
83 forM_ wds (\(wd,folder) -> do
85 printparam "END watching" folder
89 X.throw (X.ErrorCall "ERROR: watching a directory is only supported in Linux")
96 else readFileRawLazy inputFile'
98 then mapM_ (publishOneMsg (currentExchange args) (rKey args) Nothing) (BL.lines messageFile)
99 else publishOneMsg (currentExchange args) (rKey args) (Just (inputFile')) messageFile
102 then waitForConfirms chan >>= printparam "confirmed"
104 X.catch (closeConnection conn) exceptionHandler
109 -- | watch a hotfolder
112 -> (String -> String -> Maybe RawFilePath -> BL.ByteString -> IO ())
113 -> (RawFilePath, String, String)
114 -> IO (WatchDescriptor, RawFilePath)
115 watchHotfolder args publishOneMsg (folder, exchange, rkey) = do
116 printparam "hotfolder" folder
117 inotify <- initINotify
123 (handleEvent (publishOneMsg exchange rkey) (suffix args) folder)
125 if (initialScan args)
126 then RD.listDirectory folder >>=
127 mapM_ (\fn -> handleFile (publishOneMsg exchange rkey) (suffix args) (folder </> fn))
132 -- | A handler for clean exit
133 exceptionHandler :: AMQPException -> IO ()
134 exceptionHandler (ChannelClosedException Normal txt) =
135 printparam "exit" txt >> exitWith ExitSuccess
136 exceptionHandler (ConnectionClosedException Normal txt) =
137 printparam "exit" txt >> exitWith ExitSuccess
138 exceptionHandler x = printparam "exception" x >> exitWith (ExitFailure 1)
140 -- | The handler for publisher confirms
141 confirmCallback :: (Word64, Bool, AckType) -> IO ()
142 confirmCallback (deliveryTag, isAll, ackType) =
153 -- | Hotfolder event handler
155 (Maybe RawFilePath -> BL.ByteString -> IO ()) -> [BS.ByteString] -> RawFilePath -> Event -> IO ()
156 -- just handle closewrite and movedin events
157 handleEvent func suffixes folder (Closed False (Just fileName) True) =
158 handleFile func suffixes (folder </> fileName)
159 handleEvent func suffixes folder (MovedIn False fileName _) =
160 handleFile func suffixes (folder </> fileName)
161 handleEvent _ _ _ _ = return ()
163 -- | Hotfolder file handler
165 (Maybe RawFilePath -> BL.ByteString -> IO ()) -> [BS.ByteString] -> RawFilePath -> IO ()
166 handleFile func suffixes@(_:_) fileName =
167 if (any (flip BS.isSuffixOf fileName) suffixes) && not ("." `BS.isPrefixOf` fileName)
168 then handleFile func [] fileName
170 handleFile func [] fileName =
172 (readFileRawLazy fileName >>= func (Just fileName))
173 (\e -> printparam "exception while processing" fileName >> printparam "exception in handleFile" (e :: X.IOException))
176 -- | Publish one message with our settings
185 publishOneMsg' chan a exchange rkey fn content = do
186 printparam "sending" fn
187 (mtype, mencoding) <-
190 let firstchunk = if BL.null content then BS.empty else head $ BL.toChunks content
191 m <- magicOpen [MagicMimeType]
193 t <- BS.useAsCStringLen firstchunk (magicCString m)
194 magicSetFlags m [MagicMimeEncoding]
195 e <- BS.useAsCStringLen firstchunk (magicCString m)
196 return (Just (T.pack t), Just (T.pack e))
197 else return ((contenttype a), (contentencoding a))
198 now <- getCurrentTime >>= return . floor . utcTimeToPOSIXSeconds
205 , msgDeliveryMode = persistent a
206 , msgTimestamp = Just now
208 , msgType = msgtype a
209 , msgUserID = userid a
210 , msgApplicationID = appid a
211 , msgClusterID = clusterid a
212 , msgContentType = mtype
213 , msgContentEncoding = mencoding
214 , msgReplyTo = replyto a
215 , msgPriority = prio a
216 , msgCorrelationID = corrid a
217 , msgExpiration = msgexp a
218 , msgHeaders = substheader (fnheader a) (fmap takeFileName fn) $ msgheader a
221 removeSentFileIfRequested (removeSentFile a) (moveSentFileTo a) fn
224 [String] -> Maybe BS.ByteString -> Maybe FieldTable -> Maybe FieldTable
225 substheader (s:r) (Just fname) old =
226 substheader r (Just fname) (addheader' old s fname)
227 substheader _ _ old = old
228 removeSentFileIfRequested False _ _ = return ()
229 removeSentFileIfRequested True _ Nothing = return ()
230 removeSentFileIfRequested True Nothing (Just fname) =
231 printparam "removing" fname >> RD.removeFile fname
232 removeSentFileIfRequested True (Just path) (Just fname) =
233 printparam "moving" [fname,"to",path] >>
234 F.rename fname (replaceDirectory fname ((takeDirectory fname) </> path))
235 addheader' :: Maybe FieldTable -> String -> BS.ByteString -> Maybe FieldTable
236 addheader' Nothing k v =
237 Just $ FieldTable $ M.singleton (T.pack k) (FVString v)
238 addheader' (Just (FieldTable oldheader)) k v =
239 Just $ FieldTable $ M.insert (T.pack k) (FVString v) oldheader