pgdl-9.2: src/DownloadInterface.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase#-}
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
module DownloadInterface (downloadInterface, DownloadSettings(..))
where
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.ByteString.Char8 as BC
import Data.Conduit
import Data.Conduit.Binary
import Data.Default
import Data.List
import Control.Monad
import Control.Monad.Trans.Resource
import Control.Monad.IO.Class
import Control.Concurrent
import Control.Applicative
import qualified Control.Concurrent.Chan as C
import Network.HTTP.Conduit
import Network.HTTP.Types.Header
import System.Posix.Files
import System.Process
import qualified System.IO as IO
import Distribution.System
import qualified Graphics.Vty as V
import qualified Brick.Main as M
import qualified Brick.Types as T
import qualified Brick.Widgets.Edit as E
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.ProgressBar as P
import qualified Brick.Widgets.Center as C
import qualified Brick.AttrMap as A
import Brick.Types (Widget)
import Brick.Widgets.Core
import Brick.Util (on)
import Networking
data DownloadSettings =
DownloadSettings { networkResource :: NetworkResource
, relativeUrl :: Text
, localStoragePath :: Text
-- ^ file name included
, justOpen :: Bool
-- ^ when True, don't use any network resource
, continueDownload :: Bool
-- ^ when True, continue the download progress (use "Range" in http header)
-- else redownload the whole file
}
-- bytes already downloaded
data DownloadState = DownloadState Integer | FinishedState
| UserInput DownloadState E.Editor
-- ^ download progress
data DEvent = VtyEvent V.Event
| UpdateFinishedSize Integer
| DownloadFinish
downloadInterface :: DownloadSettings -> IO ()
downloadInterface dSettings = do
eventChan <- C.newChan
unless (justOpen dSettings) . void . forkIO $
download dSettings (C.writeChan eventChan)
progressBarTotSize <- if justOpen dSettings
then getLocalFileSize (T.unpack $ localStoragePath dSettings) >>= \case
Nothing -> error "file do not exist."
Just s -> return s
else getContentLength dSettings
let
initialState = if justOpen dSettings
then FinishedState
else DownloadState 0
theApp =
M.App { M.appDraw = drawUI
, M.appChooseCursor = M.neverShowCursor
, M.appHandleEvent = appEvent
, M.appStartEvent = return
, M.appAttrMap = const theMap
, M.appLiftVtyEvent = VtyEvent
}
appEvent :: DownloadState -> DEvent -> T.EventM (T.Next DownloadState)
appEvent ds@(DownloadState _) de = case de of
VtyEvent e -> case e of
V.EvKey V.KEsc [] -> M.halt ds
V.EvKey (V.KChar 'q') _ -> M.halt ds
V.EvKey (V.KChar 'o') [] -> M.continue $ UserInput ds (E.editor "command" (str.unlines) (Just 1) "")
V.EvKey V.KEnter [] -> do
liftIO $ localStoragePath dSettings `openBy` ""
M.continue ds
_ -> M.continue ds
UpdateFinishedSize b -> M.continue . DownloadState $ b
DownloadFinish -> M.continue FinishedState
appEvent FinishedState de = case de of
VtyEvent e -> case e of
V.EvKey (V.KChar 'q') _ -> M.halt FinishedState
V.EvKey (V.KChar 'o') [] -> M.continue $ UserInput FinishedState (E.editor "command" (str.unlines) (Just 1) "")
V.EvKey V.KEnter [] -> do
liftIO $ localStoragePath dSettings `openBy` ""
M.halt FinishedState
-- ^ file opened, we can quit download interface now
_ -> M.continue FinishedState
_ -> error "received non vty event after FinishedState was reached."
appEvent (UserInput st ed) de = case de of
VtyEvent e -> case e of
V.EvKey V.KEsc [] -> M.continue st
V.EvKey V.KEnter [] -> do
liftIO $ localStoragePath dSettings `openBy` concat (E.getEditContents ed)
case st of
DownloadState _ -> M.continue st
FinishedState -> M.halt FinishedState
_ -> error "unexpected state in UserInput state."
ev -> do
newEd <- T.handleEvent ev ed
M.continue $ UserInput st newEd
UpdateFinishedSize b -> M.continue (UserInput (DownloadState b) ed)
DownloadFinish -> M.continue (UserInput FinishedState ed)
theMap = A.attrMap V.defAttr [ (P.progressCompleteAttr, V.black `on` V.cyan)
, (P.progressIncompleteAttr, V.black `on` V.white)
, ("input box", V.black `on` V.blue)
]
drawUI :: DownloadState -> [Widget]
drawUI (DownloadState bytes) = [vBox [bar, note]]
where
bar = C.vCenter . C.hCenter $ P.progressBar Nothing (fromIntegral bytes / fromIntegral progressBarTotSize)
note = C.vCenter . C.hCenter . str $ unlines [ "press Enter to open the file (even if its download has not finished)"
, "press 'o' to specify which program should be used to open the file."
]
drawUI FinishedState = [ui]
where
ui = C.vCenter . C.hCenter . str $ unlines [ "The file is ready for open."
, "press Enter to open the file, or press 'q' to return to file listing"
, "press 'o' to specify which program should be used to open the file."
]
drawUI (UserInput (UserInput _ _) _) = error "unexpected state in UserInput state."
drawUI (UserInput s ed) = (padTop (T.Pad 1) . vLimit 5 $ ask) : drawUI s
where
ask = C.vCenter . C.hCenter .
hLimit 50 . vLimit 5 .
B.borderWithLabel (str "please input a program name") .
forceAttr "input box" .
hLimit 40 $
E.renderEditor ed
M.customMain (V.mkVty Data.Default.def) eventChan theApp initialState
return ()
openBy :: Text -> String -> IO ()
openBy file "" = case buildOS of
OSX -> openBy file "open"
Linux -> openBy file "xdg-open"
_ -> error "don't know how to open file in this OS"
openBy file cmd = void $
case buildOS of
OSX -> runCommand $ cmd ++ " " ++ addq filepath ++ " "
Linux -> runCommand $ "nohup " ++ cmd ++ " " ++ addq filepath ++ " &>/dev/null &"
_ -> error "don't know how to open file in this OS"
where
filepath = T.unpack file
addq :: String -> String
addq s = "\"" ++ s ++ "\""
getContentLength :: DownloadSettings -> IO Integer
getContentLength dSettings = do
let (req, manager) = networkResource dSettings $ relativeUrl dSettings
runResourceT $ do
response <- http req manager
let headers = responseHeaders response
case find (\(hn, bs) -> hn == hContentLength) headers of
Nothing -> error "no content-length presents in response header"
Just (hn, bs) -> case BC.readInteger bs of
Nothing -> error "no integer in content-length header"
Just (sz, _) -> return sz
getLocalFileSize :: String -> IO (Maybe Integer)
getLocalFileSize path =
fileExist path >>= \case
False -> return Nothing
True -> Just . fromIntegral . fileSize <$> getFileStatus path
download :: DownloadSettings ->
(DEvent -> IO ()) ->
IO ()
download dSettings tell = do
-- this implementation needs to be change
-- since some filename contains characters that cannot be represented by String
let
filepath = T.unpack $ localStoragePath dSettings
checkFile :: IO ()
checkFile = forever $ do
threadDelay 100000 -- 0.1 second
getLocalFileSize filepath >>= \case
Nothing -> return ()
Just s -> tell . UpdateFinishedSize $ s
checkerThreadID <- forkIO checkFile
let (req, manager) = networkResource dSettings $ relativeUrl dSettings
localFileSize <- getLocalFileSize (T.unpack $ localStoragePath dSettings) >>= \case
Nothing -> return 0
Just s -> return s
remoteFileSize <- getContentLength dSettings
let extraHeaders = [("Range", "bytes=" `BC.append` BC.pack (show localFileSize) `BC.append` "-")]
let req' = if continueDownload dSettings
then req { requestHeaders = extraHeaders ++ requestHeaders req }
else req
-- use http and ResumableSource in conduit for constant memory usage
runResourceT $ do
if continueDownload dSettings then
when (localFileSize < remoteFileSize) $ do
response <- http req' manager
let body = responseBody response
body $$+- sinkIOHandle (IO.openBinaryFile filepath IO.AppendMode)
else do
response <- http req' manager
let body = responseBody response
body $$+- sinkIOHandle (IO.openBinaryFile filepath IO.WriteMode)
liftIO $ do
killThread checkerThreadID
tell DownloadFinish
return ()