{-# LANGUAGE DeriveDataTypeable #-} -- | This module contains code to use files on a remote FTP server as -- Sources and Sinks. -- -- Using these functions looks like this: -- -- > let uri = fromJust $ parseURI "ftp://ftp.kernel.org/pub/README_ABOUT_BZ2_FILES" -- > runResourceT $ createSource uri $$ consume module Network.FTP.Conduit ( createSink , createSource , FTPException(..) ) where import Data.Conduit import qualified Data.ByteString as BS import Data.ByteString.UTF8 hiding (foldl) import Network.Socket hiding (send, sendTo, recv, recvFrom, Closed) import Network.Socket.ByteString import Network.URI import Network.Utils import Control.Exception import Data.Word import System.ByteOrder import Data.Bits import Prelude hiding (getLine, catch) import Data.Typeable import Control.Monad.IO.Class -- | Thrown if a FTP-level protocol exception happens data FTPException = UnexpectedCode Int BS.ByteString | GeneralError String | IncorrectScheme String | SocketClosed deriving (Typeable, Show) instance Exception FTPException hton_16 :: Word16 -> Word16 hton_16 x = case byteOrder of BigEndian -> x LittleEndian -> x `shiftL` 8 + x `shiftR` 8 _ -> undefined getByte :: Socket -> IO Word8 getByte s = do b <- recv s 1 if BS.null b then throw SocketClosed else return $ BS.head b getLine :: Socket -> IO BS.ByteString getLine s = do b <- getByte s helper b where helper b = do b' <- getByte s if BS.pack [b, b'] == fromString "\r\n" then return BS.empty else helper b' >>= return . (BS.cons b) extractCode :: BS.ByteString -> Int extractCode = read . toString . (BS.takeWhile (/= 32)) readExpected :: Socket -> Int -> IO BS.ByteString readExpected s i = do line <- getLine s --putStrLn $ "Read: " ++ (toString line) if extractCode line /= i then throw $ UnexpectedCode i line else return line writeLine :: Socket -> BS.ByteString -> IO () writeLine s bs = do --putStrLn $ "Writing: " ++ (toString bs) sendAll s $ bs `BS.append` (fromString "\r\n") -- hardcode the newline for platform independence close :: (Socket, Socket) -> IO () close (c, d) = do --putStrLn "Closing data connection" sClose d catch (do _ <- readExpected c 226 writeLine c $ fromString "QUIT" _ <- readExpected c 221 --putStrLn "Closing control connection" sClose c ) (\ e -> sClose c >> throw (e :: IOException)) -- | Create a conduit source out of a 'URI'. Uses the @RETR@ command. createSource :: MonadResource m => URI -> Source m BS.ByteString createSource uri = sourceIO setup close pull where setup = do (c, d, path') <- common uri catch (do writeLine c $ fromString $ "RETR " ++ path' _ <- readExpected c 150 return (c, d) ) (\ e -> sClose d >> sClose c >> throw (e :: IOException)) pull (_, d) = liftIO $ do bytes <- recv d 4096 if BS.null bytes then do return IOClosed else return $ IOOpen bytes -- | Create a conduit sink out of a 'URI'. Uses the @STOR@ command. createSink :: MonadResource m => URI -> Sink BS.ByteString m () createSink uri = sinkIO setup close push (liftIO . close) where setup = do (c, d, path') <- common uri catch (do writeLine c $ fromString $ "STOR " ++ path' _ <- readExpected c 150 return (c, d) ) (\ e -> sClose d >> sClose c >> throw (e :: IOException)) push (_, d) input = liftIO $ do sendAll d input return IOProcessing common :: URI -> IO (Socket, Socket, String) common (URI { uriScheme = scheme' , uriAuthority = authority' , uriPath = path' }) = do if scheme' /= "ftp:" then throw $ IncorrectScheme scheme' else return () --putStrLn "Opening control connection" c <- connectTCP host (PortNum (hton_16 port)) catch (do _ <- readExpected c 220 writeLine c $ fromString $ "USER " ++ user _ <- readExpected c 331 writeLine c $ fromString $ "PASS " ++ pass _ <- readExpected c 230 writeLine c $ fromString "TYPE I" _ <- readExpected c 200 writeLine c $ fromString "PASV" pasv_response <- readExpected c 227 let (pasvhost, pasvport) = parsePasvString pasv_response --putStrLn "Opening data connection" d <- connectTCP (toString pasvhost) (PortNum (hton_16 pasvport)) return (c, d, path') ) (\ e -> sClose c >> throw (e :: IOException)) where (host, port, user, pass) = case authority' of Nothing -> undefined Just (URIAuth userInfo regName port') -> ( regName , if null port' then 21 else read (tail port') , if null userInfo then "anonymous" else takeWhile (\ l -> l /= ':' && l /= '@') userInfo , if null userInfo || not (':' `elem` userInfo) then "" else init $ tail $ (dropWhile (/= ':')) userInfo ) parsePasvString ps = (pasvhost, pasvport) where pasvhost = BS.init $ foldl (\ a ip -> a `BS.append` (fromString $ show ip) `BS.append` (fromString ".")) BS.empty [ip1, ip2, ip3, ip4] pasvport = (fromIntegral port1) `shiftL` 8 + (fromIntegral port2) (ip1, ip2, ip3, ip4, port1, port2) = read $ toString $ (`BS.append` (fromString ")")) $ (BS.takeWhile (/= 41)) $ (BS.dropWhile (/= 40)) ps :: (Int, Int, Int, Int, Int, Int)