6 -- | Handling static files on the filesystem.
7 module Network.HTTP.Lucu.StaticFile
11 , generateETagFromFile
14 import qualified Blaze.ByteString.Builder.ByteString as BB
15 import qualified Blaze.Text.Int as BT
17 import Control.Monad.Unicode
18 import Control.Monad.Trans
19 import qualified Data.Ascii as A
20 import qualified Data.ByteString.Lazy.Char8 as B
21 import Data.Monoid.Unicode
22 import qualified Data.Text as T
23 import Data.Time.Clock.POSIX
24 import Network.HTTP.Lucu.Abortion
25 import Network.HTTP.Lucu.Config
26 import Network.HTTP.Lucu.ETag
27 import Network.HTTP.Lucu.MIMEType
28 import Network.HTTP.Lucu.MIMEType.Guess
29 import Network.HTTP.Lucu.Resource
30 import Network.HTTP.Lucu.Resource.Tree
31 import Network.HTTP.Lucu.Response
32 import Prelude.Unicode
33 import System.FilePath
34 import System.Posix.Files
36 -- | @'staticFile' fpath@ is a 'ResourceDef' which serves the file at
37 -- @fpath@ on the filesystem.
38 staticFile ∷ FilePath → ResourceDef
41 resGet = Just $ handleStaticFile True path
42 , resHead = Just $ handleStaticFile False path
45 octetStream ∷ MIMEType
46 octetStream = mkMIMEType "application" "octet-stream"
48 handleStaticFile ∷ Bool → FilePath → Resource ()
49 handleStaticFile sendContent path
50 = do exists ← liftIO $ fileExist path
52 $ foundNoEntity Nothing
54 readable ← liftIO $ fileAccess path True False False
56 $ abort Forbidden [] Nothing
58 stat ← liftIO $ getFileStatus path
59 when (isDirectory stat)
60 $ abort Forbidden [] Nothing
62 tag ← liftIO $ generateETagFromFile path
63 let lastMod = posixSecondsToUTCTime
66 $ modificationTime stat
67 foundEntity tag lastMod
70 case guessTypeByFileName (cnfExtToMIMEType conf) path of
71 Nothing → setContentType octetStream
72 Just mime → setContentType mime
75 $ liftIO (B.readFile path) ≫= output
77 -- |@'generateETagFromFile' fpath@ generates a strong entity tag from
78 -- a file. The file doesn't necessarily have to be a regular file; it
79 -- may be a FIFO or a device file. The tag is made of inode ID, size
80 -- and modification time.
82 -- Note that the tag is not strictly strong because the file could be
83 -- modified twice at a second without changing inode ID or size, but
84 -- it's not really possible to generate a strictly strong ETag from a
85 -- file as we don't want to simply grab the entire file and use it as
86 -- an ETag. It is indeed possible to hash it with SHA-1 or MD5 to
87 -- increase strictness, but it's too inefficient if the file is really
88 -- large (say, 1 TiB).
89 generateETagFromFile ∷ FilePath → IO ETag
90 generateETagFromFile path
91 = do stat ← getFileStatus path
92 let inode = fileID stat
94 lastMod = fromEnum $ modificationTime stat
95 tag = A.fromAsciiBuilder
98 ⊕ BB.fromByteString "-"
100 ⊕ BB.fromByteString "-"
101 ⊕ BT.integral lastMod
102 return $ strongETag tag
104 -- | @'staticDir' dir@ is a 'ResourceDef' which maps all files in
105 -- @dir@ and its subdirectories on the filesystem to the 'ResTree'.
107 -- Note that 'staticDir' currently doesn't have a directory-listing
108 -- capability. Requesting the content of a directory will end up being
109 -- replied with /403 Forbidden/.
110 staticDir ∷ FilePath → ResourceDef
114 , resGet = Just $ handleStaticDir True path
115 , resHead = Just $ handleStaticDir False path
118 handleStaticDir ∷ Bool → FilePath → Resource ()
119 handleStaticDir sendContent basePath
120 = do extraPath ← getPathInfo
121 securityCheck extraPath
122 let path = basePath </> joinPath (map T.unpack extraPath)
124 handleStaticFile sendContent path
126 securityCheck pathElems
127 = when (any (≡ "..") pathElems)
128 $ fail ("security error: " ⧺ show pathElems)
129 -- TODO: implement directory listing.