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.Lazy.Char8 as BL
13 import qualified RawFilePath.Directory as RD
14 import qualified Data.ByteString.Char8 as BS
15 import qualified Data.Map as M
17 import qualified Data.Text as T
19 import Data.Time.Clock.POSIX
20 import Data.Version (showVersion)
21 import Data.Word (Word64)
24 import Network.AMQP.Types
25 import Network.AMQP.Utils.Connection
26 import Network.AMQP.Utils.Helpers
27 import Network.AMQP.Utils.Options
28 import Paths_amqp_utils (version)
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 -- all done. wait and close.
101 then waitForConfirms chan >>= printparam "confirmed"
103 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) =
149 -- | Hotfolder event handler
151 (Maybe RawFilePath -> BL.ByteString -> IO ()) -> [BS.ByteString] -> RawFilePath -> Event -> IO ()
152 -- just handle closewrite and movedin events
153 handleEvent func suffixes folder (Closed False (Just fileName) True) =
154 handleFile func suffixes (folder </> fileName)
155 handleEvent func suffixes folder (MovedIn False fileName _) =
156 handleFile func suffixes (folder </> fileName)
157 handleEvent _ _ _ _ = return ()
159 -- | Hotfolder file handler
161 (Maybe RawFilePath -> BL.ByteString -> IO ()) -> [BS.ByteString] -> RawFilePath -> IO ()
162 handleFile func suffixes@(_:_) fileName =
163 if (any (flip BS.isSuffixOf fileName) suffixes) && not ("." `BS.isPrefixOf` fileName)
164 then handleFile func [] fileName
166 handleFile func [] fileName =
168 (readFileRawLazy fileName >>= func (Just fileName))
169 (\e -> printparam "exception while processing" fileName >> printparam "exception in handleFile" (e :: X.IOException))
172 -- | Publish one message with our settings
181 publishOneMsg' chan a exchange rkey fn content = do
182 printparam "sending" fn
183 (mtype, mencoding) <-
186 let firstchunk = if BL.null content then BS.empty else head $ BL.toChunks content
187 m <- magicOpen [MagicMimeType]
189 t <- BS.useAsCStringLen firstchunk (magicCString m)
190 magicSetFlags m [MagicMimeEncoding]
191 e <- BS.useAsCStringLen firstchunk (magicCString m)
192 return (Just (T.pack t), Just (T.pack e))
193 else return ((contenttype a), (contentencoding a))
194 now <- getCurrentTime >>= return . floor . utcTimeToPOSIXSeconds
201 , msgDeliveryMode = persistent a
202 , msgTimestamp = Just now
204 , msgType = msgtype a
205 , msgUserID = userid a
206 , msgApplicationID = appid a
207 , msgClusterID = clusterid a
208 , msgContentType = mtype
209 , msgContentEncoding = mencoding
210 , msgReplyTo = replyto a
211 , msgPriority = prio a
212 , msgCorrelationID = corrid a
213 , msgExpiration = msgexp a
214 , msgHeaders = substheader (fnheader a) (fmap takeFileName fn) $ msgheader a
217 removeSentFileIfRequested (removeSentFile a) (moveSentFileTo a) fn
220 [String] -> Maybe BS.ByteString -> Maybe FieldTable -> Maybe FieldTable
221 substheader (s:r) (Just fname) old =
222 substheader r (Just fname) (addheader' old s fname)
223 substheader _ _ old = old
224 removeSentFileIfRequested False _ _ = return ()
225 removeSentFileIfRequested True _ Nothing = return ()
226 removeSentFileIfRequested True Nothing (Just fname) =
227 printparam "removing" fname >> RD.removeFile fname
228 removeSentFileIfRequested True (Just path) (Just fname) =
229 printparam "moving" [fname,"to",path] >>
230 F.rename fname (replaceDirectory fname ((takeDirectory fname) </> path))
231 addheader' :: Maybe FieldTable -> String -> BS.ByteString -> Maybe FieldTable
232 addheader' Nothing k v =
233 Just $ FieldTable $ M.singleton (T.pack k) (FVString v)
234 addheader' (Just (FieldTable oldheader)) k v =
235 Just $ FieldTable $ M.insert (T.pack k) (FVString v) oldheader