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
41 args <- getArgs >>= parseargs 'a'
42 printparam "client version" ["amqp-utils", showVersion version]
43 printparam "routing key" $ rKey args
44 printparam "exchange" $ currentExchange args
45 (conn, chan) <- connect args
46 addChannelExceptionHandler chan (X.throwTo tid)
47 printparam "confirm mode" $ confirm args
50 confirmSelect chan False
51 addConfirmationListener chan confirmCallback
53 let inputFile' = firstInputFile (inputFiles args)
57 else F.getFileStatus inputFile' >>= return . F.isDirectory
59 publishOneMsg' chan args {removeSentFile = removeSentFile args && isDir}
62 printparam "hotfolder mode" True
63 printparam "initial scan" (initialScan args)
64 if isNothing (moveSentFileTo args)
65 then printparam "remove sent file" (removeSentFile args)
66 else printparam "move sent file to" (moveSentFileTo args)
78 wds <- mapM (watchHotfolder args publishOneMsg) (inputFiles args)
79 sleepingBeauty >>= (\x -> do
80 forM_ wds (\(wd,folder) -> do
82 printparam "END watching" folder
86 X.throw (X.ErrorCall "ERROR: watching a directory is only supported in Linux")
93 else readFileRawLazy inputFile'
95 then mapM_ (publishOneMsg (currentExchange args) (rKey args) Nothing) (BL.lines messageFile)
96 else publishOneMsg (currentExchange args) (rKey args) (Just (inputFile')) messageFile
99 then waitForConfirms chan >>= printparam "confirmed"
101 X.catch (closeConnection conn) exceptionHandler
106 -- | watch a hotfolder
109 -> (String -> String -> Maybe RawFilePath -> BL.ByteString -> IO ())
110 -> (RawFilePath, String, String)
111 -> IO (WatchDescriptor, RawFilePath)
112 watchHotfolder args publishOneMsg (folder, exchange, rkey) = do
113 printparam "hotfolder" folder
114 inotify <- initINotify
120 (handleEvent (publishOneMsg exchange rkey) (suffix args) folder)
122 if (initialScan args)
123 then RD.listDirectory folder >>=
124 mapM_ (\fn -> handleFile (publishOneMsg exchange rkey) (suffix args) (folder </> fn))
129 -- | A handler for clean exit
130 exceptionHandler :: AMQPException -> IO ()
131 exceptionHandler (ChannelClosedException Normal txt) =
132 printparam "exit" txt >> exitWith ExitSuccess
133 exceptionHandler (ConnectionClosedException Normal txt) =
134 printparam "exit" txt >> exitWith ExitSuccess
135 exceptionHandler x = printparam "exception" x >> exitWith (ExitFailure 1)
137 -- | The handler for publisher confirms
138 confirmCallback :: (Word64, Bool, AckType) -> IO ()
139 confirmCallback (deliveryTag, isAll, ackType) =
150 -- | Hotfolder event handler
152 (Maybe RawFilePath -> BL.ByteString -> IO ()) -> [BS.ByteString] -> RawFilePath -> Event -> IO ()
153 -- just handle closewrite and movedin events
154 handleEvent func suffixes folder (Closed False (Just fileName) True) =
155 handleFile func suffixes (folder </> fileName)
156 handleEvent func suffixes folder (MovedIn False fileName _) =
157 handleFile func suffixes (folder </> fileName)
158 handleEvent _ _ _ _ = return ()
160 -- | Hotfolder file handler
162 (Maybe RawFilePath -> BL.ByteString -> IO ()) -> [BS.ByteString] -> RawFilePath -> IO ()
163 handleFile func suffixes@(_:_) fileName =
164 if (any (flip BS.isSuffixOf fileName) suffixes) && not ("." `BS.isPrefixOf` fileName)
165 then handleFile func [] fileName
167 handleFile func [] fileName =
169 (readFileRawLazy fileName >>= func (Just fileName))
170 (\e -> printparam "exception while processing" fileName >> printparam "exception in handleFile" (e :: X.IOException))
173 -- | Publish one message with our settings
182 publishOneMsg' chan a exchange rkey fn content = do
183 printparam "sending" fn
184 (mtype, mencoding) <-
187 let firstchunk = if BL.null content then BS.empty else head $ BL.toChunks content
188 m <- magicOpen [MagicMimeType]
190 t <- BS.useAsCStringLen firstchunk (magicCString m)
191 magicSetFlags m [MagicMimeEncoding]
192 e <- BS.useAsCStringLen firstchunk (magicCString m)
193 return (Just (T.pack t), Just (T.pack e))
194 else return ((contenttype a), (contentencoding a))
195 now <- getCurrentTime >>= return . floor . utcTimeToPOSIXSeconds
202 , msgDeliveryMode = persistent a
203 , msgTimestamp = Just now
205 , msgType = msgtype a
206 , msgUserID = userid a
207 , msgApplicationID = appid a
208 , msgClusterID = clusterid a
209 , msgContentType = mtype
210 , msgContentEncoding = mencoding
211 , msgReplyTo = replyto a
212 , msgPriority = prio a
213 , msgCorrelationID = corrid a
214 , msgExpiration = msgexp a
215 , msgHeaders = substheader (fnheader a) (fmap takeFileName fn) $ msgheader a
218 removeSentFileIfRequested (removeSentFile a) (moveSentFileTo a) fn
221 [String] -> Maybe BS.ByteString -> Maybe FieldTable -> Maybe FieldTable
222 substheader (s:r) (Just fname) old =
223 substheader r (Just fname) (addheader' old s fname)
224 substheader _ _ old = old
225 removeSentFileIfRequested False _ _ = return ()
226 removeSentFileIfRequested True _ Nothing = return ()
227 removeSentFileIfRequested True Nothing (Just fname) =
228 printparam "removing" fname >> RD.removeFile fname
229 removeSentFileIfRequested True (Just path) (Just fname) =
230 printparam "moving" [fname,"to",path] >>
231 F.rename fname (replaceDirectory fname ((takeDirectory fname) </> path))
232 addheader' :: Maybe FieldTable -> String -> BS.ByteString -> Maybe FieldTable
233 addheader' Nothing k v =
234 Just $ FieldTable $ M.singleton (T.pack k) (FVString v)
235 addheader' (Just (FieldTable oldheader)) k v =
236 Just $ FieldTable $ M.insert (T.pack k) (FVString v) oldheader