packages feed

HAppS-Server-0.9.2: src/HAppS/Server/HTTP/FileServe.hs

{-# OPTIONS -fglasgow-exts #-}
module HAppS.Server.HTTP.FileServe
    (
     MimeMap,fileServe, mimeTypes,isDot, blockDotFiles,doIndex,errorwrapper
    ) where

import Control.Exception
import Control.Monad.Reader
import Control.Monad.Trans
import Data.List
import Data.Maybe
import HAppS.Server.SimpleHTTP
import System.Directory
import System.IO
import System.Locale(defaultTimeLocale)
import System.Log.Logger
import System.Time -- (formatCalendarTime, toUTCTime,TOD(..))
import qualified Data.ByteString.Char8 as P
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.Map as Map
import qualified HAppS.Server.SimpleHTTP as SH

-- mapResult :: (IO Result -> IO Result) -> [ServerPart] -> ServerPart
-- mapResult fn parts = ReaderT $ \rq -> fmap fn (runReaderT (multi parts) rq

errorwrapper :: MonadIO m => String -> String -> ServerPartT m Response
errorwrapper binarylocation loglocation
    = require getErrorLog $ \errorLog ->
      --[method () $ SH.ok errorLog]
      [anyRequest $ liftIO $ return $ toResponse errorLog]
    where getErrorLog
              = liftIO $
                handleJust ioErrors (const (return Nothing)) $
                do bintime <- getModificationTime binarylocation
                   logtime <- getModificationTime loglocation
                   if (logtime > bintime)
                     then fmap Just $ readFile loglocation -- fileServe [loglocation] [] "./"
                     else return Nothing

type MimeMap = Map.Map String String

doIndex [] _mime _rq _fp = do setResponseCode 403
                              return $ toResponse "Directory index forbidden"
doIndex (index:rest) mime rq fp =
    do
    let path = fp++'/':index
    --print path
    fe <- liftIO $ doesFileExist path
    if fe then retFile path else doIndex rest mime rq fp
    where retFile = returnFile mime rq
defaultIxFiles= ["index.html","index.xml","index.gif"]

fileServe :: MonadIO m => [FilePath] -> FilePath -> ServerPartT m Response
fileServe ixFiles localpath  = 
    withRequest (fileServe' localpath (doIndex (ixFiles++defaultIxFiles)) mimeTypes)

-- | Serve files with a mime type map under a directory.
--   Uses the function to transform URIs to FilePaths.
fileServe' localpath fdir mime rq = do
    let fp2 = takeWhile (/=',') fp
        fp = filepath
        safepath = filter (\x->not (null x) && head x /= '.') (rqPaths rq)
        filepath = intercalate "/"  (localpath:safepath)
        fp' = if null safepath then "" else last safepath
    if "TESTH" `isPrefixOf` fp'
        then renderResponse mime rq $ fakeFile $ read $ drop 5 $ fp' 
        else do
    fe <- liftIO $ doesFileExist fp
    fe2 <- liftIO $ doesFileExist fp2
    de <- liftIO $ doesDirectoryExist fp
    -- error $ "show ilepath: " ++show (fp,de)
    let status | de   = "DIR"
               | fe   = "file"
               | fe2  = "group"
               | True = "NOT FOUND"
    liftIO $ logM "HAppS.Server.HTTP.FileServe" INFO ("fileServe: "++show fp++" \t"++status)
    if de then fdir mime rq fp else do
    getFile mime fp >>= flip either (renderResponse mime rq) 
                (const $ returnGroup localpath mime rq safepath)

returnFile mime rq fp =  
    getFile mime fp >>=  either fileNotFound (renderResponse mime rq)

--- #if fp has , separated then return concatenation with content-type of last
-- #and last modified of latest
tr a b list = map (\x->if x==a then b else x) list
ltrim = dropWhile (flip elem " \t\r")   

returnGroup localPath mime rq fp = do
  let fps0 = map ((:[]). ltrim) $ lines $ tr ',' '\n' $ last fp
      fps = map (intercalate "/" . ((localPath:init fp) ++)) fps0

  -- if (head $ head fps0)=="TEST" then   renderResponse mime rq fakeFile else do

  mbFiles <-  mapM (getFile mime) $ fps
  let notFounds = [x | Left x <- mbFiles]
      files = [x | Right x <- mbFiles]
  if not $ null notFounds 
    then fileNotFound $ drop (length localPath) $ head notFounds else do
  let totSize = sum $ map (snd . fst) files
      maxTime::ClockTime = maximum $ map (fst . fst) files

  renderResponse mime rq ((maxTime,totSize),(fst $ snd $ head files,
                                             L.concat $ map (snd . snd) files))



fileNotFound fp = do setResponseCode 404 
                     return $ toResponse $ "File not found "++ fp
--fakeLen = 71* 1024
fakeFile fakeLen = ((TOD 0 0,L.length body),("text/javascript",body))
    where
      body = L.pack $ (("//"++(show len)++" ") ++ ) $ (take len $ repeat '0') ++ "\n"
      len = fromIntegral fakeLen

getFile mime fp = do
  let ct = Map.findWithDefault "text/plain" (getExt fp) mime
  fe <- liftIO $ doesFileExist fp
  if not fe then return $ Left fp else do
  
  time <- liftIO  $ getModificationTime fp
  h <- liftIO $ openBinaryFile fp ReadMode
  size <- liftIO $ hFileSize h
  lbs <- liftIO $ L.hGetContents h
  return $ Right ((time,size),(ct,lbs))



renderResponse mime rq ((modtime,size),(ct,body)) = do

  let notmodified = getHeader "if-modified-since" rq == Just (P.pack $ repr)
      repr = formatCalendarTime defaultTimeLocale 
             "%a, %d %b %Y %X GMT" (toUTCTime modtime)
  -- "Mon, 07 Jan 2008 19:51:02 GMT"
  -- when (isJust $ getHeader "if-modified-since"  rq) $ error $ show $ getHeader "if-modified-since" rq
  if notmodified then do setResponseCode 304 ; return $ toResponse "" else do
  let mod = getHeader "if-modified-since" rq
  modifyResponse (setHeader "HUH" $ show $ (fmap P.unpack mod == Just repr,mod,Just repr))
  modifyResponse (setHeader "Last-modified" repr)
  -- if %Z or UTC are in place of GMT below, wget complains that the last-modified header is invalid
  modifyResponse (setHeader "Content-Length" (show size))
  modifyResponse (setHeader "Content-Type" ct)  
  return $ resultBS 200 body

              


getExt fPath = reverse $ takeWhile (/='.') $ reverse fPath

-- | Ready collection of common mime types.
mimeTypes :: MimeMap
mimeTypes = Map.fromList
	    [("xml","application/xml")
	    ,("xsl","application/xml")
	    ,("js","text/javascript")
	    ,("html","text/html")
	    ,("css","text/css")
	    ,("gif","image/gif")
	    ,("jpg","image/jpeg")
	    ,("png","image/png")
	    ,("txt","text/plain")
	    ,("doc","application/msword")
	    ,("exe","application/octet-stream")
	    ,("pdf","application/pdf")
	    ,("zip","application/zip")
	    ,("gz","application/x-gzip")
	    ,("ps","application/postscript")
	    ,("rtf","application/rtf")
	    ,("wav","application/x-wav")
	    ,("hs","text/plain")]



blockDotFiles :: (Request -> IO Response) -> Request -> IO Response
blockDotFiles fn rq
    | isDot (intercalate "/" (rqPaths rq)) = return $ result 403 "Dot files not allowed."
    | otherwise = fn rq

isDot = isD . reverse
    where
    isD ('.':'/':_) = True
    isD ['.']       = True
    --isD ('/':_)     = False
    isD (_:cs)      = isD cs
    isD []          = False