2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | A store for storing and retreiving items
|
|
|
|
{-# LANGUAGE ExistentialQuantification #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2010-12-26 15:12:57 +00:00
|
|
|
module Hakyll.Core.Store
|
|
|
|
( Store
|
2012-10-29 14:01:58 +00:00
|
|
|
, Result (..)
|
2013-02-06 19:50:44 +00:00
|
|
|
, toMaybe
|
2012-10-29 14:01:58 +00:00
|
|
|
, new
|
|
|
|
, set
|
|
|
|
, get
|
2013-05-04 09:51:58 +00:00
|
|
|
, isMember
|
2012-10-29 14:01:58 +00:00
|
|
|
, delete
|
2013-01-06 17:33:00 +00:00
|
|
|
, hash
|
2010-12-26 15:12:57 +00:00
|
|
|
) where
|
|
|
|
|
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
2013-05-03 14:21:14 +00:00
|
|
|
import Control.Applicative ((<$>))
|
|
|
|
import Control.Exception (IOException, handle)
|
|
|
|
import qualified Crypto.Hash.MD5 as MD5
|
|
|
|
import Data.Binary (Binary, decode, encodeFile)
|
|
|
|
import qualified Data.ByteString as B
|
|
|
|
import qualified Data.ByteString.Lazy as BL
|
|
|
|
import qualified Data.Cache.LRU.IO as Lru
|
|
|
|
import Data.List (intercalate)
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Data.Text.Encoding as T
|
|
|
|
import Data.Typeable (TypeRep, Typeable, cast, typeOf)
|
|
|
|
import System.Directory (createDirectoryIfMissing)
|
|
|
|
import System.Directory (doesFileExist, removeFile)
|
|
|
|
import System.FilePath ((</>))
|
|
|
|
import System.IO (IOMode (..), hClose, openFile)
|
|
|
|
import Text.Printf (printf)
|
2010-12-26 15:12:57 +00:00
|
|
|
|
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Simple wrapper type
|
|
|
|
data Box = forall a. Typeable a => Box a
|
2011-03-20 14:51:20 +00:00
|
|
|
|
2011-05-17 08:57:37 +00:00
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
2010-12-26 15:12:57 +00:00
|
|
|
data Store = Store
|
2011-01-04 12:09:45 +00:00
|
|
|
{ -- | All items are stored on the filesystem
|
|
|
|
storeDirectory :: FilePath
|
2012-10-29 14:01:58 +00:00
|
|
|
, -- | Optionally, items are also kept in-memory
|
|
|
|
storeMap :: Maybe (Lru.AtomicLRU FilePath Box)
|
2010-12-26 15:12:57 +00:00
|
|
|
}
|
|
|
|
|
2012-08-07 23:41:29 +00:00
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Result of a store query
|
|
|
|
data Result a
|
|
|
|
= Found a -- ^ Found, result
|
|
|
|
| NotFound -- ^ Not found
|
|
|
|
| WrongType TypeRep TypeRep -- ^ Expected, true type
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
|
2013-02-06 19:50:44 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Convert result to 'Maybe'
|
|
|
|
toMaybe :: Result a -> Maybe a
|
|
|
|
toMaybe (Found x) = Just x
|
|
|
|
toMaybe _ = Nothing
|
|
|
|
|
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
2010-12-26 15:12:57 +00:00
|
|
|
-- | Initialize the store
|
2012-10-29 14:01:58 +00:00
|
|
|
new :: Bool -- ^ Use in-memory caching
|
|
|
|
-> FilePath -- ^ Directory to use for hard disk storage
|
|
|
|
-> IO Store -- ^ Store
|
|
|
|
new inMemory directory = do
|
|
|
|
createDirectoryIfMissing True directory
|
|
|
|
ref <- if inMemory then Just <$> Lru.newAtomicLRU csize else return Nothing
|
2011-01-04 12:09:45 +00:00
|
|
|
return Store
|
|
|
|
{ storeDirectory = directory
|
2012-10-29 14:01:58 +00:00
|
|
|
, storeMap = ref
|
2011-01-04 12:09:45 +00:00
|
|
|
}
|
2011-04-12 08:09:03 +00:00
|
|
|
where
|
2012-10-29 14:01:58 +00:00
|
|
|
csize = Just 500
|
|
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Auxiliary: add an item to the in-memory cache
|
|
|
|
cacheInsert :: Typeable a => Store -> String -> a -> IO ()
|
|
|
|
cacheInsert (Store _ Nothing) _ _ = return ()
|
|
|
|
cacheInsert (Store _ (Just lru)) key x =
|
|
|
|
Lru.insert key (Box x) lru
|
|
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Auxiliary: get an item from the in-memory cache
|
|
|
|
cacheLookup :: forall a. Typeable a => Store -> String -> IO (Result a)
|
|
|
|
cacheLookup (Store _ Nothing) _ = return NotFound
|
|
|
|
cacheLookup (Store _ (Just lru)) key = do
|
|
|
|
res <- Lru.lookup key lru
|
|
|
|
return $ case res of
|
|
|
|
Nothing -> NotFound
|
|
|
|
Just (Box x) -> case cast x of
|
|
|
|
Just x' -> Found x'
|
|
|
|
Nothing -> WrongType (typeOf (undefined :: a)) (typeOf x)
|
|
|
|
|
|
|
|
|
2013-05-04 09:51:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
2013-05-04 12:42:28 +00:00
|
|
|
cacheIsMember :: Store -> String -> IO Bool
|
|
|
|
cacheIsMember (Store _ Nothing) _ = return False
|
|
|
|
cacheIsMember (Store _ (Just lru)) key = do
|
|
|
|
res <- Lru.lookup key lru
|
|
|
|
case res of
|
|
|
|
Nothing -> return False
|
|
|
|
_ -> return True
|
2013-05-04 09:51:58 +00:00
|
|
|
|
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Auxiliary: delete an item from the in-memory cache
|
|
|
|
cacheDelete :: Store -> String -> IO ()
|
|
|
|
cacheDelete (Store _ Nothing) _ = return ()
|
|
|
|
cacheDelete (Store _ (Just lru)) key = do
|
|
|
|
_ <- Lru.delete key lru
|
|
|
|
return ()
|
2010-12-26 15:12:57 +00:00
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
2010-12-26 15:12:57 +00:00
|
|
|
-- | Store an item
|
2012-10-29 14:01:58 +00:00
|
|
|
set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
|
|
|
|
set store identifier value = do
|
|
|
|
encodeFile (storeDirectory store </> key) value
|
|
|
|
cacheInsert store key value
|
2010-12-26 15:12:57 +00:00
|
|
|
where
|
2012-10-29 14:01:58 +00:00
|
|
|
key = hash identifier
|
|
|
|
|
2010-12-26 15:12:57 +00:00
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
2010-12-26 15:12:57 +00:00
|
|
|
-- | Load an item
|
2012-10-29 14:01:58 +00:00
|
|
|
get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
|
|
|
|
get store identifier = do
|
2011-01-04 12:09:45 +00:00
|
|
|
-- First check the in-memory map
|
2012-10-29 14:01:58 +00:00
|
|
|
ref <- cacheLookup store key
|
|
|
|
case ref of
|
2011-01-04 12:09:45 +00:00
|
|
|
-- Not found in the map, try the filesystem
|
2012-05-12 11:17:20 +00:00
|
|
|
NotFound -> do
|
2011-01-04 12:09:45 +00:00
|
|
|
exists <- doesFileExist path
|
|
|
|
if not exists
|
|
|
|
-- Not found in the filesystem either
|
2011-05-17 08:57:37 +00:00
|
|
|
then return NotFound
|
2011-01-04 12:09:45 +00:00
|
|
|
-- Found in the filesystem
|
2012-10-29 14:01:58 +00:00
|
|
|
else do
|
2013-05-03 14:21:14 +00:00
|
|
|
v <- decodeClose
|
2012-10-29 14:01:58 +00:00
|
|
|
cacheInsert store key v
|
|
|
|
return $ Found v
|
|
|
|
-- Found in the in-memory map (or wrong type), just return
|
2012-05-12 11:17:20 +00:00
|
|
|
s -> return s
|
2010-12-26 15:12:57 +00:00
|
|
|
where
|
2012-10-29 14:01:58 +00:00
|
|
|
key = hash identifier
|
|
|
|
path = storeDirectory store </> key
|
|
|
|
|
2013-05-03 14:21:14 +00:00
|
|
|
-- 'decodeFile' from Data.Binary which closes the file ASAP
|
|
|
|
decodeClose = do
|
|
|
|
h <- openFile path ReadMode
|
|
|
|
lbs <- BL.hGetContents h
|
|
|
|
BL.length lbs `seq` hClose h
|
|
|
|
return $ decode lbs
|
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
|
2013-05-04 09:51:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Strict function
|
|
|
|
isMember :: Store -> [String] -> IO Bool
|
2013-05-04 12:42:28 +00:00
|
|
|
isMember store identifier = do
|
|
|
|
inCache <- cacheIsMember store key
|
|
|
|
if inCache then return True else doesFileExist path
|
2013-05-04 09:51:58 +00:00
|
|
|
where
|
|
|
|
key = hash identifier
|
|
|
|
path = storeDirectory store </> key
|
|
|
|
|
2012-10-29 14:01:58 +00:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Delete an item
|
|
|
|
delete :: Store -> [String] -> IO ()
|
|
|
|
delete store identifier = do
|
|
|
|
cacheDelete store key
|
|
|
|
deleteFile $ storeDirectory store </> key
|
|
|
|
where
|
|
|
|
key = hash identifier
|
|
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Delete a file unless it doesn't exist...
|
|
|
|
deleteFile :: FilePath -> IO ()
|
|
|
|
deleteFile = handle (\(_ :: IOException) -> return ()) . removeFile
|
2013-01-06 17:33:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | Mostly meant for internal usage
|
|
|
|
hash :: [String] -> String
|
|
|
|
hash = concatMap (printf "%02x") . B.unpack .
|
|
|
|
MD5.hash . T.encodeUtf8 . T.pack . intercalate "/"
|