pgdl-10.9: src/DownloadInterface.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase#-}
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
{-# LANGUAGE CPP #-}
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.List
import Data.Monoid
import Control.Monad
import Control.Monad.Trans.Resource
import Control.Monad.IO.Class
import Control.Concurrent
import Control.Applicative
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 Control.Concurrent.Chan as C
import qualified Brick.BChan as BC
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 String String)
-- ^ download progress
data DownloadEvent = UpdateFinishedSize Integer
| DownloadFinish
downloadInterface :: DownloadSettings -> IO ()
downloadInterface dSettings = do
eventChan <- BC.newBChan 100000000 -- select a big enough number
unless (justOpen dSettings) . void . forkIO $
download dSettings (BC.writeBChan 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
}
appEvent :: DownloadState -> T.BrickEvent String DownloadEvent -> T.EventM String (T.Next DownloadState)
appEvent ds@(DownloadState _) be = case be of
T.VtyEvent e -> case e of
V.EvKey V.KEsc [] -> M.halt ds
V.EvKey (V.KChar 'q') _ -> M.halt ds
#if MIN_VERSION_brick(0, 19, 0)
V.EvKey (V.KChar 'o') [] -> M.continue $ UserInput ds (E.editor "command" (Just 1) "")
#else
V.EvKey (V.KChar 'o') [] -> M.continue $ UserInput ds (E.editor "command" (str . unlines) (Just 1) "")
#endif
V.EvKey V.KEnter [] -> do
liftIO $ localStoragePath dSettings `openBy` ""
M.continue ds
_ -> M.continue ds
T.AppEvent ae -> case ae of
UpdateFinishedSize b -> M.continue . DownloadState $ b
DownloadFinish -> M.continue FinishedState
appEvent FinishedState be = case be of
T.VtyEvent e -> case e of
V.EvKey (V.KChar 'q') _ -> M.halt FinishedState
#if MIN_VERSION_brick(0, 19, 0)
V.EvKey (V.KChar 'o') [] -> M.continue $ UserInput FinishedState (E.editor "command" (Just 1) "")
#else
V.EvKey (V.KChar 'o') [] -> M.continue $ UserInput FinishedState (E.editor "command" (str . unlines) (Just 1) "")
#endif
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) be = case be of
T.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 <- E.handleEditorEvent ev ed
M.continue $ UserInput st newEd
T.AppEvent de -> case de of
UpdateFinishedSize b -> M.continue (UserInput (DownloadState b) ed)
DownloadFinish -> M.continue (UserInput FinishedState ed)
appEvent _ _ = error "unknown event received in event loop. (DL interface)"
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 String]
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 $
#if MIN_VERSION_brick(0, 19, 0)
E.renderEditor (str . unlines) True ed
#else
E.renderEditor True ed
#endif
M.customMain (V.mkVty mempty) (Just 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 ->
(DownloadEvent -> 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 ()