@@ -85,15 +85,13 @@ import Data.Function (on)
85import Data.Ord (comparing)
85import Data.Ord (comparing)
86import qualified Data.ByteString.Base64 as B64
86import qualified Data.ByteString.Base64 as B64
87import Data.Either (rights)
87import Data.Either (rights)
88+import Data.Maybe (isJust, fromJust)
90import Debug.Trace
91import Debug.Trace
91debug :: (Show a) => a -> a
92debug :: (Show a) => a -> a
92debug a = trace ("DEBUG: " ++ show a) a
93debug a = trace ("DEBUG: " ++ show a) a
98-- | A list of all possible extensions, starting from the largest.
96-- | A list of all possible extensions, starting from the largest.
99takeExtensions :: FilePath -> [String]
97takeExtensions :: FilePath -> [String]
@@ -189,8 +187,9 @@ mimeTypeByExt mm def =
189defaultMimeTypeByExt :: FilePath -> MimeType
187defaultMimeTypeByExt :: FilePath -> MimeType
190defaultMimeTypeByExt = mimeTypeByExt defaultMimeTypes defaultMimeType
188defaultMimeTypeByExt = mimeTypeByExt defaultMimeTypes defaultMimeType
192−data CheckPieces
190+data CheckPieces =
191+ -- | Just the etag hash or Nothing for no etag hash
193− = Redirect Pieces
192+ Redirect Pieces (Maybe ByteString)
194 | Forbidden
193 | Forbidden
195 | NotFound
194 | NotFound
196 | FileResponse File H.ResponseHeaders
195 | FileResponse File H.ResponseHeaders
@@ -251,7 +250,7 @@ checkPieces :: (Pieces -> IO FileLookup) -- ^ file lookup function
251checkPieces fileLookup indices pieces req maxAge
250checkPieces fileLookup indices pieces req maxAge
252 | any (unsafe . piecePretty) pieces = return Forbidden
251 | any (unsafe . piecePretty) pieces = return Forbidden
253 | any (T.null . piecePretty) $ safeInit pieces =
252 | any (T.null . piecePretty) $ safeInit pieces =
254− return $ Redirect $ filterButLast (not . T.null . piecePretty) pieces
253+ return $ Redirect (filterButLast (not . T.null . piecePretty) pieces) Nothing
255 | otherwise = do
254 | otherwise = do
256 let (isFile, isFolder) =
255 let (isFile, isFolder) =
257 case () of
256 case () of
@@ -264,16 +263,17 @@ checkPieces fileLookup indices pieces req maxAge
264 case (fl, isFile) of
263 case (fl, isFile) of
265 (Nothing, _) -> return NotFound
264 (Nothing, _) -> return NotFound
266 (Just (Right file), True) -> handleCache file
265 (Just (Right file), True) -> handleCache file
267− (Just Right{}, False) -> return $ Redirect $ init pieces
266+ (Just Right{}, False) -> return $ Redirect (init pieces) Nothing
268 (Just (Left folder@(Folder _ contents)), _) -> do
267 (Just (Left folder@(Folder _ contents)), _) -> do
269 case checkIndices $ map fileName $ rights contents of
268 case checkIndices $ map fileName $ rights contents of
270− Just index -> return $ Redirect $ setLast pieces $ Piece (T.unpack index) index
269+ Just index -> return $ Redirect (setLast pieces $ Piece (T.unpack index) index) Nothing
271 Nothing ->
270 Nothing ->
272 if isFolder
271 if isFolder
273 then return $ DirectoryResponse folder
272 then return $ DirectoryResponse folder
274− else return $ Redirect $ pieces ++ [Piece "" ""]
273+ else return $ Redirect (pieces ++ [Piece "" ""]) Nothing
276 headers = W.requestHeaders req
275 headers = W.requestHeaders req
276+ queryString = W.queryString req
278 -- HTTP caching has a cache control header that you can set an expire time for a resource.
278 -- HTTP caching has a cache control header that you can set an expire time for a resource.
279 -- Max-Age is easiest because it is a simple number
279 -- Max-Age is easiest because it is a simple number
@@ -285,29 +285,42 @@ checkPieces fileLookup indices pieces req maxAge
286 -- We should set a cache control and one of ETag or last-modifed whenever possible
286 -- We should set a cache control and one of ETag or last-modifed whenever possible
288− -- in a Yesod web application we append an etag parameter to static assets.
288+ -- In a Yesod web application we can append an etag parameter to static assets.
289− -- This should set both a max-age and ETag header
289+ -- This signals that both a max-age and ETag header should be set
290 -- if there is no etag parameter
290 -- if there is no etag parameter
291 -- * don't set the max-age
291 -- * don't set the max-age
292 -- * set ETag or last-modified
292 -- * set ETag or last-modified
293 -- * ETag must be calculated ahead of time.
293 -- * ETag must be calculated ahead of time.
294 -- * last-modified is just the file mtime.
294 -- * last-modified is just the file mtime.
295− handleCache file =
295+ handleCache file = do
296− case (lookup "if-none-match" headers, fileGetHash file) of
296+ let mGetHash = fileGetHash file
297− -- etag
297+ let etagParam = lookup "etag" queryString
299+ case (etagParam, mGetHash) of
298− (Just lastHash, Just getHash) -> do
300+ (Just mEtag, Just getHash) -> do
299− hash <- getHash
301+ hash <- getHash
300− if hash == lastHash
302+ if isJust mEtag && hash == fromJust mEtag
301− then return NotModified
302− else return $ FileResponse file $ ("ETag", hash):cacheControl
303+ then return $ FileResponse file $ ("ETag", hash):cacheControl
303− _ ->
304+ else return $ Redirect pieces (Just hash)
305+ -- a file used to have an etag parameter, but no longer does
306+ (Just _, Nothing) -> return $ Redirect pieces Nothing
309+ case (lookup "if-none-match" headers, mGetHash) of
311+ (Just lastHash, Just getHash) -> do
314+ then return NotModified
315+ else return $ FileResponse file $ [("ETag", hash)]
304− case (lookup "if-modified-since" headers >>= parseDate, fileGetModified file) of
317+ case (lookup "if-modified-since" headers >>= parseDate, fileGetModified file) of
305− -- last-modified
318+ -- last-modified
306− (Just lastSent, Just modified) -> do
319+ (Just lastSent, Just modified) -> do
307− if lastSent >= modified
320+ if lastSent >= modified
308− then return NotModified
321+ then return NotModified
309− else respond file
322+ else respond file
310− _ -> respond file
323+ _ -> respond file
312 respond file = do
325 respond file = do
313 mhash <- maybe (return Nothing) (fmap Just) $ fileGetHash file
326 mhash <- maybe (return Nothing) (fmap Just) $ fileGetHash file
@@ -519,6 +532,18 @@ status304, statusNotModified :: H.Status
519status304 = H.Status 304 "Not Modified"
532status304 = H.Status 304 "Not Modified"
520statusNotModified = status304
533statusNotModified = status304
535+-- alist helper functions
536+replace :: Eq a => a -> b -> [(a, b)] -> [(a, b)]
537+replace k v [] = [(k,v)]
538+replace k v (x:xs) | fst x == k = (k,v):xs
539+ | otherwise = x:replace k v xs
541+remove :: Eq a => a -> [(a, b)] -> [(a, b)]
543+remove k (x:xs) | fst x == k = xs
544+ | otherwise = x:remove k xs
522staticAppPieces :: StaticSettings -> Pieces -> W.Application
547staticAppPieces :: StaticSettings -> Pieces -> W.Application
523staticAppPieces _ _ req
548staticAppPieces _ _ req
524 | W.requestMethod req /= "GET" = return $ W.responseLBS
549 | W.requestMethod req /= "GET" = return $ W.responseLBS
@@ -559,12 +584,16 @@ staticAppPieces ss pieces req = liftIO $ do
559 [ ("Content-Type", mt)
584 [ ("Content-Type", mt)
560 -- TODO: set Content-Length
585 -- TODO: set Content-Length
562− Redirect pieces' -> do
588+ Redirect pieces' mHash -> do
563 let loc = (ssMkRedirect ss) pieces' $ toByteString (H.encodePathSegments $ map piecePretty pieces')
589 let loc = (ssMkRedirect ss) pieces' $ toByteString (H.encodePathSegments $ map piecePretty pieces')
590+ let qString = case mHash of
591+ Just hash -> replace "etag" (Just hash) (W.queryString req)
592+ Nothing -> remove "etag" (W.queryString req)
565 return $ W.responseLBS H.status301
594 return $ W.responseLBS H.status301
566 [ ("Content-Type", "text/plain")
595 [ ("Content-Type", "text/plain")
567− , ("Location", loc)
596+ , ("Location", S8.append loc $ H.renderQuery True qString)
568 ] "Redirect"
597 ] "Redirect"
569 Forbidden -> return $ W.responseLBS H.status403
598 Forbidden -> return $ W.responseLBS H.status403
570 [ ("Content-Type", "text/plain")
599 [ ("Content-Type", "text/plain")