wai-app-static-3.2.0: WaiAppStatic/Storage/Filesystem.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Access files on the filesystem.
module WaiAppStatic.Storage.Filesystem (
-- * Types
ETagLookup,
-- * Settings
defaultWebAppSettings,
defaultFileServerSettings,
webAppSettingsWithLookup,
) where
import Control.Exception (SomeException, try)
import Control.Monad (forM)
import Data.ByteString (ByteString)
import Data.List (foldl')
import Data.Maybe (catMaybes)
import Network.Mime
import qualified Network.Wai as W
import System.Directory (
doesDirectoryExist,
doesFileExist,
getDirectoryContents,
)
import System.FilePath ((</>))
import System.IO (IOMode (..), withBinaryFile)
import System.PosixCompat.Files (
fileSize,
getFileStatus,
isRegularFile,
modificationTime,
)
import Util
import WaiAppStatic.Listing
import WaiAppStatic.Types
#ifdef MIN_VERSION_crypton
import Data.ByteArray.Encoding
import Crypto.Hash (hashlazy, MD5, Digest)
#else
import Data.ByteString.Base64 (encode)
import Crypto.Hash.MD5 (hashlazy)
#endif
import qualified Data.ByteString.Lazy as BL (hGetContents)
import qualified Data.Text as T
-- | Construct a new path from a root and some @Pieces@.
pathFromPieces :: FilePath -> Pieces -> FilePath
pathFromPieces = foldl' (\fp p -> fp </> T.unpack (fromPiece p))
-- | Settings optimized for a web application. Files will have aggressive
-- caching applied and hashes calculated, and indices and listings are disabled.
defaultWebAppSettings
:: FilePath
-- ^ root folder to serve from
-> StaticSettings
defaultWebAppSettings root =
StaticSettings
{ ssLookupFile = webAppLookup hashFileIfExists root
, ssMkRedirect = defaultMkRedirect
, ssGetMimeType = return . defaultMimeLookup . fromPiece . fileName
, ssMaxAge = MaxAgeForever
, ssListing = Nothing
, ssIndices = []
, ssRedirectToIndex = False
, ssUseHash = True
, ssAddTrailingSlash = False
, ss404Handler = Nothing
}
-- | Settings optimized for a file server. More conservative caching will be
-- applied, and indices and listings are enabled.
defaultFileServerSettings
:: FilePath
-- ^ root folder to serve from
-> StaticSettings
defaultFileServerSettings root =
StaticSettings
{ ssLookupFile = fileSystemLookup (fmap Just . hashFile) root
, ssMkRedirect = defaultMkRedirect
, ssGetMimeType = return . defaultMimeLookup . fromPiece . fileName
, ssMaxAge = NoMaxAge
, ssListing = Just defaultListing
, ssIndices = map unsafeToPiece ["index.html", "index.htm"]
, ssRedirectToIndex = False
, ssUseHash = False
, ssAddTrailingSlash = False
, ss404Handler = Nothing
}
-- | Same as @defaultWebAppSettings@, but additionally uses a specialized
-- @ETagLookup@ in place of the standard one. This can allow you to cache your
-- hash values, or even precompute them.
webAppSettingsWithLookup
:: FilePath
-- ^ root folder to serve from
-> ETagLookup
-> StaticSettings
webAppSettingsWithLookup dir etagLookup =
(defaultWebAppSettings dir){ssLookupFile = webAppLookup etagLookup dir}
-- | Convenience wrapper for @fileHelper@.
fileHelperLR
:: ETagLookup
-> FilePath
-- ^ file location
-> Piece
-- ^ file name
-> IO LookupResult
fileHelperLR a b c = fmap (maybe LRNotFound LRFile) $ fileHelper a b c
-- | Attempt to load up a @File@ from the given path.
fileHelper
:: ETagLookup
-> FilePath
-- ^ file location
-> Piece
-- ^ file name
-> IO (Maybe File)
fileHelper hashFunc fp name = do
efs <- try $ getFileStatus fp
case efs of
Left (_ :: SomeException) -> return Nothing
Right fs
| isRegularFile fs ->
return $
Just
File
{ fileGetSize = fromIntegral $ fileSize fs
, fileToResponse = \s h -> W.responseFile s h fp Nothing
, fileName = name
, fileGetHash = hashFunc fp
, fileGetModified = Just $ modificationTime fs
}
Right _ -> return Nothing
-- | How to calculate etags. Can perform filesystem reads on each call, or use
-- some caching mechanism.
type ETagLookup = FilePath -> IO (Maybe ByteString)
-- | More efficient than @fileSystemLookup@ as it only concerns itself with
-- finding files, not folders.
webAppLookup :: ETagLookup -> FilePath -> Pieces -> IO LookupResult
webAppLookup hashFunc prefix pieces =
fileHelperLR hashFunc fp lastPiece
where
fp = pathFromPieces prefix pieces
lastPiece
| null pieces = unsafeToPiece ""
| otherwise = last pieces
-- | MD5 hash and base64-encode the file contents. Does not check if the file
-- exists.
hashFile :: FilePath -> IO ByteString
hashFile fp = withBinaryFile fp ReadMode $ \h -> do
f <- BL.hGetContents h
#ifdef MIN_VERSION_crypton
let !hash = hashlazy f :: Digest MD5
return $ convertToBase Base64 hash
#else
let !hash = hashlazy f
return . encode $ hash
#endif
hashFileIfExists :: ETagLookup
hashFileIfExists fp = do
res <- try $ hashFile fp
return $ case res of
Left (_ :: SomeException) -> Nothing
Right x -> Just x
isVisible :: FilePath -> Bool
isVisible ('.' : _) = False
isVisible "" = False
isVisible _ = True
-- | Get a proper @LookupResult@, checking if the path is a file or folder.
-- Compare with @webAppLookup@, which only deals with files.
fileSystemLookup
:: ETagLookup
-> FilePath
-> Pieces
-> IO LookupResult
fileSystemLookup hashFunc prefix pieces = do
let fp = pathFromPieces prefix pieces
fe <- doesFileExist fp
if fe
then fileHelperLR hashFunc fp lastPiece
else do
de <- doesDirectoryExist fp
if de
then do
entries' <- fmap (filter isVisible) $ getDirectoryContents fp
entries <- forM entries' $ \fpRel' -> do
let name = unsafeToPiece $ T.pack fpRel'
fp' = fp </> fpRel'
de' <- doesDirectoryExist fp'
if de'
then return $ Just $ Left name
else do
mfile <- fileHelper hashFunc fp' name
case mfile of
Nothing -> return Nothing
Just file -> return $ Just $ Right file
return $ LRFolder $ Folder $ catMaybes entries
else return LRNotFound
where
lastPiece
| null pieces = unsafeToPiece ""
| otherwise = last pieces