~alcinnz/hurl

663fb7aad3813458f3b80bebaff79ba79ddf5db5 — Adrian Cochrane 4 years ago 9faa533
Add support for cache validation.

This required redesigning the internal APIs & refactoring the code to read better.
1 files changed, 44 insertions(+), 21 deletions(-)

M src/Network/URI/Cache.hs
M src/Network/URI/Cache.hs => src/Network/URI/Cache.hs +44 -21
@@ 19,7 19,7 @@ import System.Directory
import Data.Maybe
import Data.Char (isSpace)
import Data.List as L
import Control.Monad (forM)
import Control.Monad (forM, void, when)
import Text.Read (readMaybe)

strip = C.dropWhile isSpace -- FIXME Upgrade bytestring dependency for a real strip function.


@@ 41,6 41,7 @@ shouldCacheHTTP response = -- Assume GET
        -- Also, I'll cache anything for supported response codes, regardless of explicit expiry times.

uriToString' uri = uriToString id uri ""
parseHTTPTime :: String -> Maybe UTCTime
parseHTTPTime str | ',' `L.elem` str = parseTimeM True defaultTimeLocale rfc822DateFormat str
parseHTTPTime str = parseTimeM True defaultTimeLocale "%_d %b %Y %H:%M:%S %Z" str
secondsFromNow i = do


@@ 59,37 60,57 @@ computeExpires resp
cacheHTTP :: URI -> Response Lazy.ByteString -> IO ()
cacheHTTP uri resp | shouldCacheHTTP resp = do
    expires <- computeExpires resp
    writeKV (uriToString' uri) ([
            ("mime", C.unpack $ fromMaybe "application/octet-stream" $ 
                    lookup "content-type" $ responseHeaders resp),
            ("expires", show expires)
        ], responseBody resp)
    let headers = responseHeaders resp
    writeKV (uriToString' uri) (
        [("expires", show expires)] ++ getHeader "content-type" "mime" ++
            getHeader "ETag" "etag" ++ getHeader "Last-Modified" "modified",
        responseBody resp)
  where
    getHeader header key | Just value <- lookup header $ responseHeaders resp = [(key, C.unpack value)]
        | otherwise = []
cacheHTTP _ _ = return ()

readCacheHTTP :: URI -> IO (Maybe (String, Lazy.ByteString))
readCacheHTTP :: URI -> IO (Maybe (String, Lazy.ByteString), Maybe ResponseHeaders)
readCacheHTTP uri = do
    (headers, body) <- readKV (uriToString' uri)
    let mime = fromMaybe "application/octet-stream" $ lookup "mime" headers
    case readMaybe =<< lookup "expires" headers of
        Nothing -> return Nothing
        Just expiry -> do
    cached <- readKV $ uriToString' uri
    case cached of
        Just (headers, body) | Just expiry <- readMaybe =<< lookup "expires" headers -> do
            let mime = fromMaybe "application/octet-stream" $ lookup "mime" headers
            now <- getCurrentTime
            if expiry <= now then return $ Just (mime, body)
            else do
                -- TODO validate via a HEAD request
                return Nothing

            -- Headers for a validation request & whether should be sent.
            let headers' = if expiry <= now then Nothing else Just (
                    [("If-Modified-Since", C.pack val) | ("modified", val) <- headers,
                        isJust $ parseHTTPTime val] ++
                    [("If-None-Match", C.pack val) | ("etag", val) <- headers])
            -- Cache entry has expired, delete.
            when (isJust headers') $ deleteKV $ uriToString' uri

            return (Just (mime, body), headers')

        _ -> return (Nothing, Just [])



------
--- Key-value storage
------

readKV :: String -> IO ([(String, String)], Lazy.ByteString)
readKV :: String -> IO (Maybe ([(String, String)], Lazy.ByteString))
writeKV :: String -> ([(String, String)], Lazy.ByteString) -> IO ()
openKV :: String -> IO.IOMode -> (Handle -> IO r) -> IO r
deleteKV :: String -> IO ()
openKV :: String -> IO.IOMode -> (Handle -> IO r) -> IO (Maybe r)
pathKV :: String -> IO FilePath

openKV key mode act = do
pathKV key = do
    dir <- getXdgDirectory XdgCache "nz.geek.adrian.hurl"
    IO.withFile (dir </> "http" </> escapeURIString isUnescapedInURIComponent key) mode act
    createDirectoryIfMissing True (dir </> "http")
    return (dir </> "http" </> escapeURIString isUnescapedInURIComponent key)

openKV key mode act = do
    path <- pathKV key
    exists <- doesFileExist path
    if exists then Just <$> IO.withFile path mode act else return Nothing

readKV key = openKV key ReadMode parseHeaders
  where


@@ 104,8 125,10 @@ readKV key = openKV key ReadMode parseHeaders
                return ((key, strip' value):headers, body)
    strip' = L.dropWhile isSpace . L.dropWhileEnd isSpace

writeKV key (headers, body) = openKV key WriteMode $ \h -> do
writeKV key (headers, body) = void $ openKV key WriteMode $ \h -> do
    forM headers $ \(key, value) -> do
        IO.hPutStrLn h (key++' ':value)
    IO.hPutStrLn h ""
    Lazy.hPut h body

deleteKV key = pathKV key >>= removeFile