packages feed

carte-0.1.0.0: src/Main.hs

-- Author:  Christophe De Troyer
-- Email:   christophe.detroyer@gmail.com
-- License: GPLv3
-- Date:    November 10, 2016

module Main where
import           Base36
import           Control.Monad
import           Control.Monad.Reader
import           Control.Monad.Trans.State
import           Data.Time.Clock.POSIX
import           Data.Tuple.Select
import           Network
import           Options.Applicative
import           System.FilePath.Posix
import           System.IO
import           System.Random
import           Data.Semigroup ((<>))

 --      ___           ___           ___                         ___
 --     /  /\         /  /\         /  /\          ___          /  /\
 --    /  /::\       /  /::\       /  /::\        /__/\        /  /::\
 --   /  /:/\:\     /  /:/\:\     /  /:/\:\       \  \:\      /  /:/\:\
 --  /  /:/  \:\   /  /::\ \:\   /  /::\ \:\       \__\:\    /  /::\ \:\
 -- /__/:/ \  \:\ /__/:/\:\_\:\ /__/:/\:\_\:\      /  /::\  /__/:/\:\ \:\
 -- \  \:\  \__\/ \__\/  \:\/:/ \__\/~|::\/:/     /  /:/\:\ \  \:\ \:\_\/
 --  \  \:\            \__\::/     |  |:|::/     /  /:/__\/  \  \:\ \:\
 --   \  \:\           /  /:/      |  |:|\/     /__/:/        \  \:\_\/
 --    \  \:\         /__/:/       |__|:|~      \__\/          \  \:\
 --     \__\/         \__\/         \__\|                       \__\/

--------------------------------------------------------------------------------
--- ARGUMENT PARSING -----------------------------------------------------------
--------------------------------------------------------------------------------

type URL = String

-- | `Configuration` is the data type that holds all the runtime
-- information. It is populated by arguments entered by the user.
data Configuration =
  Configuration
    { port  :: Integer
    , url   :: URL
    , files :: String
    } deriving (Show)

portParser :: Parser Integer
portParser =
    option auto        -- an option which reads its argument with Read
      ( short 'p'      -- a short name i.e. "-p"
     <> long "port"    -- a long name i.e. "--port"
     <> metavar "PORT" -- a symbolic name in the help text
     <> value 5001     -- a default value
     <> help "The port on which to listen for connections" )

urlParser :: Parser String
urlParser =
    strOption
      ( short 'u'
     <> long "url"
     <> metavar "URL"
     <> value "localhost"
     <> help "The root url for building links to files" )

filesParser :: Parser String
filesParser =
    strOption
      ( short 'd'
     <> long "dir"
     <> metavar "DIRECTORY"
     <> value "."
     <> help "The DIRECTORY in which to store pastes" )

configuration :: Parser Configuration
configuration =
    Configuration
      <$> portParser
      <*> urlParser
      <*> filesParser

--------------------------------------------------------------------------------
--- MAIN -----------------------------------------------------------------------
--------------------------------------------------------------------------------

main :: IO ()
main = do cfg <- execParser prog
          socketloop cfg
  where
    prog = info
      (helper <*> configuration)
      ( fullDesc
     <> progDesc "Start a Carte paste server"
     <> header "Carte - a socket based paste server" )

--------------------------------------------------------------------------------
--- SOCKET LOOP ----------------------------------------------------------------
--------------------------------------------------------------------------------

-- | Starts the `loop` function which keeps on listening for client
-- connections. The current time in seconds is used as the starting
-- seed for the random generator.
socketloop :: Configuration -> IO ()
socketloop cfg = withSocketsDo $ do
                   seed <- fmap mkStdGen currentTimeSeconds
                   s    <- listenOn (PortNumber . fromInteger . port $ cfg)
                   runReaderT (evalStateT (loop s) seed) cfg

-- | Fires a new thread for each client connection, handles the rest
-- of the upload.
loop :: Socket -> StateT StdGen (ReaderT Configuration IO) ()
loop s = liftIO (accept s) >>= handleUploader . sel1  >> loop s

--------------------------------------------------------------------------------
--- UPLOADER HANDLING ----------------------------------------------------------
--------------------------------------------------------------------------------

-- | Takes in a `Handle` (socket), reads the input from the socket,
-- writes it to disk, notifies the user and closes the socket.
handleUploader :: Handle -> StateT StdGen (ReaderT Configuration IO) ()
handleUploader source = do
  key     <- newKey
  baseUrl <- asks url
  fileDir <- asks files
  void . liftIO $ do
    receiveFile source (fileDir </> key)
    notify source (baseUrl </> key)
    hClose source

-- | Writes data from first `Handle` to second `Handle` until EOF
-- is encountered. The `Handle`s are not closed.
pipe :: Handle -> Handle -> IO ()
pipe src sink = do
  l   <- hGetLine src
  hPutStrLn sink l
  end <- hIsEOF src
  unless end $ pipe src sink

-- | Takes in a `Handle` and writes all the data to the given file at
-- FilePath. Uses `WriteMode` so overwrites any contents.
receiveFile :: Handle -> FilePath -> IO ()
receiveFile src filepath = withBinaryFile filepath WriteMode (pipe src)

-- | Prints the web URL on the socket (to the user).
notify :: Handle -> URL -> IO ()
notify = hPutStrLn

-- | Returns the current time in seconds. Used as a seed for the random generator.
currentTimeSeconds :: IO Int
currentTimeSeconds = round <$> getPOSIXTime

-- | Generates a new random ID for the snippet.
newKey :: (Monad m) => StateT StdGen m String
newKey = do
  (key, gen') <- gets random
  put gen'
  return $ base36 key