{-# LANGUAGE CPP, OverloadedStrings #-}

{-|
A library for listing "files" in an http "directory".

@
import Network.HTTP.Directory
import qualified Data.Text as T
import qualified Data.Text.IO as T

main = do
  let url = \"https:\/\/example.com\/some\/dir\/\"
  files <- httpDirectory\' url
  mapM_ T.putStrLn files
  let file = url '+/+' T.unpack (head files)
  httpFileSize\' file >>= print
  httpLastModified\' file >>= print
@

The main methods use http-client and most of the primed ones http-conduit.
-}

module Network.HTTP.Directory
       ( httpDirectory,
         httpDirectory',
         httpRawDirectory,
         httpRawDirectory',
         httpExists,
         httpExists',
         httpFileSize,
         httpFileSize',
         httpLastModified,
         httpLastModified',
         httpFileSizeTime,
         httpFileSizeTime',
         httpFileSizeAndTime',
         httpFileHeaders,
         httpFileHeaders',
         httpManager,
         httpRedirect,
         httpRedirect',
         httpRedirects,
         isHttpUrl,
         trailingSlash,
         noTrailingSlash,
         Manager,
         (+/+)
       ) where

import Control.Monad (when)

import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
#if MIN_VERSION_base(4,11,0)
import Data.Functor ((<&>))
#endif
import qualified Data.List as L
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime)

import Network.HTTP.Client (hrRedirects, httpLbs, httpNoBody, Manager, method,
                            newManager, parseRequest, Request,
                            Response, responseBody, responseHeaders,
                            responseOpenHistory, responseStatus)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Date (httpDateToUTC, parseHTTPDate)
import qualified Network.HTTP.Simple as S
import Network.HTTP.Types (hAccept, hContentLength, hLocation, hLastModified,
                           methodHead, statusCode, ResponseHeaders)
import Network.URI (parseURI, URI(..))

import Text.HTML.DOM (parseLBS)
import Text.XML.Cursor

-- | List the files (hrefs) in an http directory
--
-- It filters out absolute urls & paths, queries, '..', and '#' links.
--
-- Raises an error if the http request fails.
--
-- Note if the directory (webpage) url is redirected to a different path
-- you may need to use 'httpRedirect' to determine
-- the actual final url prefix for relative links (files).
--
-- (Before 0.1.4 this was the same as httpRawDirectory)
httpDirectory :: Manager -> String -> IO [Text]
httpDirectory :: Manager -> String -> IO [Text]
httpDirectory Manager
mgr String
url = do
  hrefs <- Manager -> String -> IO [Text]
httpRawDirectory Manager
mgr String
url
  return $ defaultFilesFilter (parseURI url) hrefs

defaultFilesFilter :: Maybe URI -> [Text] -> [Text]
defaultFilesFilter :: Maybe URI -> [Text] -> [Text]
defaultFilesFilter Maybe URI
mUri =
  [Text] -> [Text]
forall a. Eq a => [a] -> [a]
L.nub ([Text] -> [Text]) -> ([Text] -> [Text]) -> [Text] -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (Text -> Bool) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Bool] -> Bool
forall (t :: * -> *). Foldable t => t Bool -> Bool
or ([Bool] -> Bool) -> (Text -> [Bool]) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Text -> Bool] -> Text -> [Bool]
forall a b. [a -> b] -> a -> [b]
flist ((Text -> Text -> Bool) -> [Text] -> [Text -> Bool]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Text -> Bool
T.isInfixOf [Text
":", Text
"?", Text
"#"] [Text -> Bool] -> [Text -> Bool] -> [Text -> Bool]
forall a. [a] -> [a] -> [a]
++ [Text -> Bool
nonTrailingSlash] [Text -> Bool] -> [Text -> Bool] -> [Text -> Bool]
forall a. [a] -> [a] -> [a]
++ [(Text -> [Text] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Text
"../", Text
".."])])) ([Text] -> [Text]) -> ([Text] -> [Text]) -> [Text] -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Text
removePath
  where
    -- picked from swish
    flist :: [a->b] -> a -> [b]
    flist :: forall a b. [a -> b] -> a -> [b]
flist [a -> b]
fs a
a = ((a -> b) -> b) -> [a -> b] -> [b]
forall a b. (a -> b) -> [a] -> [b]
map ((a -> b) -> a -> b
forall a b. (a -> b) -> a -> b
$ a
a) [a -> b]
fs

    -- may return "" which nonTrailingSlash then removes
    removePath :: Text -> Text
    removePath :: Text -> Text
removePath Text
t =
      case Maybe Text
murlPath of
        Maybe Text
Nothing -> Text
t
        Just Text
path ->
          Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe Text
t (Maybe Text -> Text) -> Maybe Text -> Text
forall a b. (a -> b) -> a -> b
$ Text -> Text -> Maybe Text
T.stripPrefix Text
path Text
t

    murlPath :: Maybe Text
    murlPath :: Maybe Text
murlPath = (URI -> Text) -> Maybe URI -> Maybe Text
forall a b. (a -> b) -> Maybe a -> Maybe b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (String -> Text
T.pack (String -> Text) -> (URI -> String) -> URI -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
trailingSlash (String -> String) -> (URI -> String) -> URI -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. URI -> String
uriPath) Maybe URI
mUri

    -- True means remove
    nonTrailingSlash :: Text -> Bool
    nonTrailingSlash :: Text -> Bool
nonTrailingSlash Text
"" = Bool
True     -- from removed uriPath
    nonTrailingSlash Text
"/" = Bool
True
    nonTrailingSlash Text
t =
      (Text -> Int
T.length Text
t Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
1) Bool -> Bool -> Bool
&& (Text
"/" Text -> Text -> Bool
`T.isInfixOf` HasCallStack => Text -> Text
Text -> Text
T.init Text
t)

-- | Like httpDirectory but uses the global Manager
--
-- @since 0.1.4
httpDirectory' :: String -> IO [Text]
httpDirectory' :: String -> IO [Text]
httpDirectory' String
url = do
  hrefs <- String -> IO [Text]
httpRawDirectory' String
url
  return $ defaultFilesFilter (parseURI url) hrefs

httpRawDirectoryInternal :: (Request -> IO (Response BL.ByteString)) -> String
                         -> IO [Text]
httpRawDirectoryInternal :: (Request -> IO (Response ByteString)) -> String -> IO [Text]
httpRawDirectoryInternal Request -> IO (Response ByteString)
httpreq String
url = do
  request <- String -> IO Request
forall (m :: * -> *). MonadThrow m => String -> m Request
parseRequest String
url
  response <- httpreq $ S.addRequestHeader hAccept "text/html" request
  checkResponse url response
  let body = Response ByteString -> ByteString
forall body. Response body -> body
responseBody Response ByteString
response
      doc = ByteString -> Document
parseLBS ByteString
body
      cursor = Document -> Cursor
fromDocument Document
doc
  return $ concatMap (attribute "href") $ cursor $// element "a"

-- | List all the hrefs in an http directory html file.
--
-- Raises an error if the http request fails.
--
-- Note if the directory (webpage) url is redirected to a different path
-- you may need to use 'httpRedirect' to determine
-- the actual final url prefix for relative links
-- (files).
--
-- @since 0.1.4
{- HLINT ignore "Use section" -}
httpRawDirectory :: Manager -> String -> IO [Text]
httpRawDirectory :: Manager -> String -> IO [Text]
httpRawDirectory Manager
mgr = (Request -> IO (Response ByteString)) -> String -> IO [Text]
httpRawDirectoryInternal ((Request -> Manager -> IO (Response ByteString))
-> Manager -> Request -> IO (Response ByteString)
forall a b c. (a -> b -> c) -> b -> a -> c
flip Request -> Manager -> IO (Response ByteString)
httpLbs Manager
mgr)

-- | List all the hrefs in an http directory html file.
--
-- Raises an error if the http request fails.
--
-- Like httpRawDirectory but uses Network.HTTP.Simple (http-conduit)
--
-- @since 0.1.9
httpRawDirectory' :: String -> IO [Text]
httpRawDirectory' :: String -> IO [Text]
httpRawDirectory' = (Request -> IO (Response ByteString)) -> String -> IO [Text]
httpRawDirectoryInternal Request -> IO (Response ByteString)
forall (m :: * -> *).
MonadIO m =>
Request -> m (Response ByteString)
S.httpLBS

-- | Test if an file (url) exists
--
-- @since 0.1.3
httpExists :: Manager -> String -> IO Bool
httpExists :: Manager -> String -> IO Bool
httpExists Manager
mgr String
url = do
  response <- Manager -> String -> IO (Response ())
httpHead Manager
mgr String
url
  return $ statusCode (responseStatus response) == 200

-- | Test if an file (url) exists
--
-- @since 0.1.9
httpExists' :: String -> IO Bool
httpExists' :: String -> IO Bool
httpExists' String
url = do
  response <- String -> IO (Response ())
httpHead' String
url
  return $ statusCode (responseStatus response) == 200

-- | Try to get the filesize (Content-Length field) of an http file
--
-- Raises an error if the http request fails.
httpFileSize :: Manager -> String -> IO (Maybe Integer)
httpFileSize :: Manager -> String -> IO (Maybe Integer)
httpFileSize Manager
mgr String
url =
  Manager -> String -> IO ResponseHeaders
httpFileHeaders Manager
mgr String
url IO ResponseHeaders
-> (ResponseHeaders -> Maybe Integer) -> IO (Maybe Integer)
forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b
<&>
  (ByteString -> Integer) -> Maybe ByteString -> Maybe Integer
forall a b. (a -> b) -> Maybe a -> Maybe b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (String -> Integer
forall a. Read a => String -> a
read (String -> Integer)
-> (ByteString -> String) -> ByteString -> Integer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> String
B.unpack) (Maybe ByteString -> Maybe Integer)
-> (ResponseHeaders -> Maybe ByteString)
-> ResponseHeaders
-> Maybe Integer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hContentLength

-- | Try to get the filesize (Content-Length field) of an http file
--
-- Raises an error if the http request fails.
--
-- @since 0.1.9
httpFileSize' :: String -> IO (Maybe Integer)
httpFileSize' :: String -> IO (Maybe Integer)
httpFileSize' String
url =
  String -> IO ResponseHeaders
httpFileHeaders' String
url IO ResponseHeaders
-> (ResponseHeaders -> Maybe Integer) -> IO (Maybe Integer)
forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b
<&>
  (ByteString -> Integer) -> Maybe ByteString -> Maybe Integer
forall a b. (a -> b) -> Maybe a -> Maybe b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (String -> Integer
forall a. Read a => String -> a
read (String -> Integer)
-> (ByteString -> String) -> ByteString -> Integer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> String
B.unpack) (Maybe ByteString -> Maybe Integer)
-> (ResponseHeaders -> Maybe ByteString)
-> ResponseHeaders
-> Maybe Integer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hContentLength

-- | Try to get the modification time (Last-Modified field) of an http file
--
-- Raises an error if the http request fails.
--
-- @since 0.1.1
httpLastModified :: Manager -> String -> IO (Maybe UTCTime)
httpLastModified :: Manager -> String -> IO (Maybe UTCTime)
httpLastModified Manager
mgr String
url = do
  headers <- Manager -> String -> IO ResponseHeaders
httpFileHeaders Manager
mgr String
url
  let mdate = HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hLastModified ResponseHeaders
headers
  return $ httpDateToUTC <$> (parseHTTPDate =<< mdate)

-- | Try to get the modification time (Last-Modified field) of an http file
--
-- Raises an error if the http request fails. Uses global Manager
--
-- @since 0.1.9
httpLastModified' :: String -> IO (Maybe UTCTime)
httpLastModified' :: String -> IO (Maybe UTCTime)
httpLastModified' String
url = do
  headers <- String -> IO ResponseHeaders
httpFileHeaders' String
url
  let mdate = HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hLastModified ResponseHeaders
headers
  return $ httpDateToUTC <$> (parseHTTPDate =<< mdate)

-- | Try to get the filesize and modification time of an http file
--
-- Raises an error if the http request fails.
--
-- @since 0.1.10
httpFileSizeTime :: Manager -> String -> IO (Maybe Integer, Maybe UTCTime)
httpFileSizeTime :: Manager -> String -> IO (Maybe Integer, Maybe UTCTime)
httpFileSizeTime Manager
mgr String
url = do
  headers <- Manager -> String -> IO ResponseHeaders
httpFileHeaders Manager
mgr String
url
  let msize = String -> Integer
forall a. Read a => String -> a
read (String -> Integer)
-> (ByteString -> String) -> ByteString -> Integer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> String
B.unpack (ByteString -> Integer) -> Maybe ByteString -> Maybe Integer
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hContentLength ResponseHeaders
headers
      mdate = HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hLastModified ResponseHeaders
headers
      mtime = HTTPDate -> UTCTime
httpDateToUTC (HTTPDate -> UTCTime) -> Maybe HTTPDate -> Maybe UTCTime
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (ByteString -> Maybe HTTPDate
parseHTTPDate (ByteString -> Maybe HTTPDate)
-> Maybe ByteString -> Maybe HTTPDate
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< Maybe ByteString
mdate)
  return (msize, mtime)

-- | Try to get the filesize and modification time of an http file
--
-- Global Manager version.
--
-- Raises an error if the http request fails.
--
-- @since 0.1.10
httpFileSizeTime' :: String -> IO (Maybe Integer, Maybe UTCTime)
httpFileSizeTime' :: String -> IO (Maybe Integer, Maybe UTCTime)
httpFileSizeTime' String
url = do
  headers <- String -> IO ResponseHeaders
httpFileHeaders' String
url
  let msize = String -> Integer
forall a. Read a => String -> a
read (String -> Integer)
-> (ByteString -> String) -> ByteString -> Integer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> String
B.unpack (ByteString -> Integer) -> Maybe ByteString -> Maybe Integer
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hContentLength ResponseHeaders
headers
      mdate = HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hLastModified ResponseHeaders
headers
      mtime = HTTPDate -> UTCTime
httpDateToUTC (HTTPDate -> UTCTime) -> Maybe HTTPDate -> Maybe UTCTime
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (ByteString -> Maybe HTTPDate
parseHTTPDate (ByteString -> Maybe HTTPDate)
-> Maybe ByteString -> Maybe HTTPDate
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< Maybe ByteString
mdate)
  return (msize, mtime)

-- | Try to get the filesize and modification time together of an http file
-- Unlike httpFileSizeTime', it combines the results into one Maybe.
--
-- Uses global Manager.
--
-- Raises an error if the http request fails.
--
-- @since 0.1.11
httpFileSizeAndTime' :: String -> IO (Maybe (Integer, UTCTime))
httpFileSizeAndTime' :: String -> IO (Maybe (Integer, UTCTime))
httpFileSizeAndTime' String
url = do
  headers <- String -> IO ResponseHeaders
httpFileHeaders' String
url
  return $ msizetime headers
  where
    msizetime :: ResponseHeaders -> Maybe (a, UTCTime)
msizetime ResponseHeaders
headers = do
      size <- String -> a
forall a. Read a => String -> a
read (String -> a) -> (ByteString -> String) -> ByteString -> a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> String
B.unpack (ByteString -> a) -> Maybe ByteString -> Maybe a
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> HeaderName -> ResponseHeaders -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
hContentLength ResponseHeaders
headers
      date <- lookup hLastModified headers
      time <- httpDateToUTC <$> parseHTTPDate date
      return (size,time)

-- | Return the HTTP headers for a file
--
-- Raises an error if the http request fails.
--
-- @since 0.1.10
httpFileHeaders :: Manager -> String -> IO ResponseHeaders
httpFileHeaders :: Manager -> String -> IO ResponseHeaders
httpFileHeaders Manager
mgr String
url = do
  response <- Manager -> String -> IO (Response ())
httpHead Manager
mgr String
url
  checkResponse url response
  return $ responseHeaders response

-- | Return the HTTP headers of an http file.
--
-- Global Manager version.
--
-- Raises an error if the http request fails.
--
-- @since 0.1.10
httpFileHeaders' :: String -> IO ResponseHeaders
httpFileHeaders' :: String -> IO ResponseHeaders
httpFileHeaders' String
url = do
  response <- String -> IO (Response ())
httpHead' String
url
  checkResponse url response
  return $ responseHeaders response

-- conflicts with Request
checkResponse :: String -> Response r -> IO ()
checkResponse :: forall r. String -> Response r -> IO ()
checkResponse String
url Response r
response =
  Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Status -> Int
statusCode (Response r -> Status
forall body. Response body -> Status
responseStatus Response r
response) Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= Int
200) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
    String -> IO ()
putStrLn String
url
    String -> IO ()
forall a. String -> a
error' (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ Status -> String
forall a. Show a => a -> String
show (Status -> String) -> Status -> String
forall a b. (a -> b) -> a -> b
$ Response r -> Status
forall body. Response body -> Status
responseStatus Response r
response

-- | Alias for 'newManager tlsManagerSettings'
-- so one does not need to import http-client etc
--
-- @since 0.1.2
httpManager :: IO Manager
httpManager :: IO Manager
httpManager =
  ManagerSettings -> IO Manager
newManager ManagerSettings
tlsManagerSettings

-- | Returns the list of http redirects for an url in reverse order
-- (ie last redirect is listed first)
httpRedirects :: Manager -> String -> IO [B.ByteString]
httpRedirects :: Manager -> String -> IO [ByteString]
httpRedirects Manager
mgr String
url = do
  request <- String -> IO Request
parseRequestHead String
url
  respHist <- responseOpenHistory request mgr
  return $ reverse $ mapMaybe (lookup hLocation . responseHeaders . snd) $ hrRedirects respHist

-- | Return final redirect for an url
httpRedirect :: Manager -> String -> IO (Maybe B.ByteString)
httpRedirect :: Manager -> String -> IO (Maybe ByteString)
httpRedirect Manager
mgr String
url =
  [ByteString] -> Maybe ByteString
forall a. [a] -> Maybe a
listToMaybe ([ByteString] -> Maybe ByteString)
-> IO [ByteString] -> IO (Maybe ByteString)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Manager -> String -> IO [ByteString]
httpRedirects Manager
mgr String
url

-- | Like httpRedirect but uses global Manager.
--
-- @since 0.1.4
httpRedirect' :: String -> IO (Maybe B.ByteString)
httpRedirect' :: String -> IO (Maybe ByteString)
httpRedirect' String
url = do
  mgr <- IO Manager
httpManager
  listToMaybe <$> httpRedirects mgr url

parseRequestHead :: String -> IO Request
parseRequestHead :: String -> IO Request
parseRequestHead String
url = do
  request <- String -> IO Request
forall (m :: * -> *). MonadThrow m => String -> m Request
parseRequest String
url
  return $
    S.addRequestHeader hAccept "*/*" $
    request {method = methodHead}

httpHead :: Manager -> String -> IO (Response ())
httpHead :: Manager -> String -> IO (Response ())
httpHead Manager
mgr String
url = do
  request <- String -> IO Request
parseRequestHead String
url
  httpNoBody request mgr

httpHead' :: String -> IO (Response ())
httpHead' :: String -> IO (Response ())
httpHead' String
url = do
  request <- String -> IO Request
parseRequestHead String
url
  S.httpNoBody request

-- | Test if string starts with http[s]:
--
-- @since 0.1.5
isHttpUrl :: String -> Bool
isHttpUrl :: String -> Bool
isHttpUrl String
loc = String
"http:" String -> String -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` String
loc Bool -> Bool -> Bool
|| String
"https:" String -> String -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` String
loc

-- | Make sure an url ends with "\/"
--
-- @
-- trailingSlash "url" == "url\/"
-- trailingSlash "url\/" == "url\/"
-- @
--
-- @since 0.1.6
trailingSlash :: String -> String
trailingSlash :: String -> String
trailingSlash String
"" = String
""
trailingSlash String
str =
  if String -> Char
forall a. HasCallStack => [a] -> a
last String
str Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/' then String
str else String
str String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"/"

-- | Remove all trailing slashes from filename or url
--
-- @
-- noTrailingSlash "dir\/" == "dir"
-- noTrailingSlash "dir\/\/" == "dir"
-- @
--
-- @since 0.1.6
noTrailingSlash :: Text -> Text
noTrailingSlash :: Text -> Text
noTrailingSlash = (Char -> Bool) -> Text -> Text
T.dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/')

-- from simple-cmd
error' :: String -> a
error' :: forall a. String -> a
error' = String -> a
forall a. String -> a
errorWithoutStackTrace

-- | This +\/+ eats extra slashes.
--
-- @
-- "dir\/\/" +\/+ "\/subdir\/" = "dir\/subdir\/"
-- @
--
-- @since 0.1.9
infixr 5 +/+
(+/+) :: String -> String -> String
String
s +/+ :: String -> String -> String
+/+ String
t =
  case (String
s,String
t) of
    (String
"",String
_) -> String
t
    (String
_,String
"") -> String
s
    (String
_,String
_) -> (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
L.dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/') String
s String -> String -> String
forall a. [a] -> [a] -> [a]
++ Char
'/' Char -> String -> String
forall a. a -> [a] -> [a]
: (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
L.dropWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/') String
t

#if !MIN_VERSION_base(4,11,0)
infixl 1 <&>

(<&>) :: Functor f => f a -> (a -> b) -> f b
as <&> f = f <$> as
#endif