on master help sign in
Pick Store from the develop branch
commite5c97d978bf34bdc98d97bf42ee2be29a5af4242 authorJasper Van der Jeugt <m@jaspervdj.be> dateMon 29 Oct 2012, 15:01 (14 years ago) parent5f805f1
6 files changed+212−160
src/Hakyll/Core/Compiler.hs+14−13 src/Hakyll/Core/Resource/Provider.hs+27−12 src/Hakyll/Core/Run.hs+7−6 src/Hakyll/Core/Store.hs+120−87 tests/Hakyll/Core/Store/Tests.hs+41−40 tests/TestSuite/Util.hs+3−2
src/Hakyll/Core/Compiler.hs+14−13
@@ -60,7 +60,7 @@
6060-- Let's look at it in detail:
6161--
6262-- > (Binary a, Typeable a, Writable a)
63−--
63+--
6464-- These are constraints for the @a@ type. @a@ (the template) needs to have
6565-- certain properties for it to be required.
6666--
@@ -138,10 +138,11 @@ import Hakyll.Core.Writable
138138import Hakyll.Core.Resource
139139import Hakyll.Core.Resource.Provider
140140import Hakyll.Core.Compiler.Internal
141−import Hakyll.Core.Store
141+import Hakyll.Core.Store (Store)
142142import Hakyll.Core.Rules.Internal
143143import Hakyll.Core.Routes
144144import Hakyll.Core.Logger
145+import qualified Hakyll.Core.Store as Store
145146
146147-- | Run a compiler, yielding the resulting target and it's dependencies. This
147148-- version of 'runCompilerJob' also stores the result
@@ -167,8 +168,7 @@ runCompiler compiler id' provider universe routes store modified logger = do
167168 -- before we return control. This makes sure the compiled item can later
168169 -- be accessed by e.g. require.
169170 Right (CompileRule (CompiledItem x)) ->
170− storeSet store "Hakyll.Core.Compiler.runCompiler"
171− (castIdentifier id') x
171+ Store.set store ["Hakyll.Core.Compiler.runCompiler", show id'] x
172172
173173 -- Otherwise, we do nothing here
174174 _ -> return ()
@@ -227,12 +227,13 @@ getResourceWith reader = fromJob $ \r -> CompilerM $ do
227227getDependency :: (Binary a, Writable a, Typeable a)
228228 => Identifier a -> CompilerM a
229229getDependency id' = CompilerM $ do
230− store <- compilerStore <$> ask
231− result <- liftIO $ storeGet store "Hakyll.Core.Compiler.runCompiler" id'
230+ store <- compilerStore <$> ask
231+ result <- liftIO $
232+ Store.get store ["Hakyll.Core.Compiler.runCompiler", show id']
232233 case result of
233− NotFound -> throwError notFound
234− WrongType e r -> throwError $ wrongType e r
235− Found x -> return x
234+ Store.NotFound -> throwError notFound
235+ Store.WrongType e r -> throwError $ wrongType e r
236+ Store.Found x -> return x
236237 where
237238 notFound =
238239 "Hakyll.Core.Compiler.getDependency: " ++ show id' ++ " was " ++
@@ -310,11 +311,11 @@ cached name (Compiler d j) = Compiler d $ const $ CompilerM $ do
310311 report logger $ "Checking cache: " ++ if modified then "modified" else "OK"
311312 if modified
312313 then do v <- unCompilerM $ j $ fromIdentifier identifier
313− liftIO $ storeSet store name identifier v
314+ liftIO $ Store.set store [name, show identifier] v
314315 return v
315− else do v <- liftIO $ storeGet store name identifier
316− case v of Found v' -> return v'
317− _ -> throwError (error' progName)
316+ else do v <- liftIO $ Store.get store [name, show identifier]
317+ case v of Store.Found v' -> return v'
318+ _ -> throwError (error' progName)
318319 where
319320 error' progName =
320321 "Hakyll.Core.Compiler.cached: Cache corrupt! " ++
src/Hakyll/Core/Resource/Provider.hs+27−12
@@ -1,3 +1,4 @@
1+--------------------------------------------------------------------------------
12-- | This module provides an API for resource providers. Resource providers
23-- allow Hakyll to get content from resources; the type of resource depends on
34-- the concrete instance.
@@ -19,22 +20,30 @@ module Hakyll.Core.Resource.Provider
1920 , resourceModified
2021 ) where
2122
23+
24+--------------------------------------------------------------------------------
2225import Control.Applicative ((<$>))
2326import Control.Concurrent (MVar, readMVar, modifyMVar_, newMVar)
2427import Data.Map (Map)
2528import qualified Data.Map as M
2629import qualified Data.Set as S
2730
31+
32+--------------------------------------------------------------------------------
2833import Data.Time (UTCTime)
2934import qualified Crypto.Hash.MD5 as MD5
3035import qualified Data.ByteString as B
3136import qualified Data.ByteString.Lazy as LB
3237
33−import Hakyll.Core.Store
38+
39+--------------------------------------------------------------------------------
40+import Hakyll.Core.Store (Store)
3441import Hakyll.Core.Resource
42+import qualified Hakyll.Core.Store as Store
43+
3544
45+--------------------------------------------------------------------------------
3646-- | A value responsible for retrieving and listing resources
37−--
3847data ResourceProvider = ResourceProvider
3948 { -- | A set of all resources this provider is able to provide
4049 resourceSet :: S.Set Resource
@@ -48,8 +57,9 @@ data ResourceProvider = ResourceProvider
4857 resourceModifiedCache :: MVar (Map Resource Bool)
4958 }
5059
60+
61+--------------------------------------------------------------------------------
5162-- | Create a resource provider
52−--
5363makeResourceProvider :: [Resource] -- ^ Resource list
5464 -> (Resource -> IO String) -- ^ String reader
5565 -> (Resource -> IO LB.ByteString) -- ^ ByteString reader
@@ -58,22 +68,27 @@ makeResourceProvider :: [Resource] -- ^ Resource list
5868makeResourceProvider l s b t =
5969 ResourceProvider (S.fromList l) s b t <$> newMVar M.empty
6070
71+
72+--------------------------------------------------------------------------------
6173-- | Get the list of all resources
6274resourceList :: ResourceProvider -> [Resource]
6375resourceList = S.toList . resourceSet
6476
77+
78+--------------------------------------------------------------------------------
6579-- | Check if a given identifier has a resource
66−--
6780resourceExists :: ResourceProvider -> Resource -> Bool
6881resourceExists provider = flip S.member $ resourceSet provider
6982
83+
84+--------------------------------------------------------------------------------
7085-- | Retrieve a digest for a given resource
71−--
7286resourceDigest :: ResourceProvider -> Resource -> IO B.ByteString
7387resourceDigest provider = fmap MD5.hashlazy . resourceLBS provider
7488
89+
90+--------------------------------------------------------------------------------
7591-- | Check if a resource was modified
76−--
7792resourceModified :: ResourceProvider -> Store -> Resource -> IO Bool
7893resourceModified provider store r = do
7994 cache <- readMVar mvar
@@ -90,21 +105,21 @@ resourceModified provider store r = do
90105 where
91106 mvar = resourceModifiedCache provider
92107
108+
109+--------------------------------------------------------------------------------
93110-- | Check if a resource digest was modified
94−--
95111digestModified :: ResourceProvider -> Store -> Resource -> IO Bool
96112digestModified provider store r = do
97113 -- Get the latest seen digest from the store
98− lastDigest <- storeGet store itemName identifier
114+ lastDigest <- Store.get store key
99115 -- Calculate the digest for the resource
100116 newDigest <- resourceDigest provider r
101117 -- Check digests
102− if Found newDigest == lastDigest
118+ if Store.Found newDigest == lastDigest
103119 -- All is fine, not modified
104120 then return False
105121 -- Resource modified; store new digest
106− else do storeSet store itemName identifier newDigest
122+ else do Store.set store key newDigest
107123 return True
108124 where
109− identifier = toIdentifier r
110− itemName = "Hakyll.Core.ResourceProvider.digestModified"
125+ key = ["Hakyll.Core.ResourceProvider.digestModified", unResource r]
src/Hakyll/Core/Run.hs+7−6
@@ -30,9 +30,10 @@ import Hakyll.Core.Resource.Provider
3030import Hakyll.Core.Resource.Provider.File
3131import Hakyll.Core.Routes
3232import Hakyll.Core.Rules.Internal
33−import Hakyll.Core.Store
33+import Hakyll.Core.Store (Store)
3434import Hakyll.Core.Util.File
3535import Hakyll.Core.Writable
36+import qualified Hakyll.Core.Store as Store
3637
3738-- | Run all rules needed, return the rule set used
3839--
@@ -42,15 +43,15 @@ run configuration rules = do
4243
4344 section logger "Initialising"
4445 store <- timed logger "Creating store" $
45− makeStore (inMemoryCache configuration) $ storeDirectory configuration
46+ Store.new (inMemoryCache configuration) $ storeDirectory configuration
4647 provider <- timed logger "Creating provider" $
4748 fileResourceProvider configuration
4849
4950 -- Fetch the old graph from the store. If we don't find it, we consider this
5051 -- to be the first run
51− graph <- storeGet store "Hakyll.Core.Run.run" "dependencies"
52− let (firstRun, oldGraph) = case graph of Found g -> (False, g)
53− _ -> (True, mempty)
52+ graph <- Store.get store ["Hakyll.Core.Run.run", "dependencies"]
53+ let (firstRun, oldGraph) = case graph of Store.Found g -> (False, g)
54+ _ -> (True, mempty)
5455
5556 let ruleSet = runRules rules provider
5657 compilers = rulesCompilers ruleSet
@@ -77,7 +78,7 @@ run configuration rules = do
7778 thrown logger e
7879 Right ((), state') ->
7980 -- We want to save the final dependency graph for the next run
80− storeSet store "Hakyll.Core.Run.run" "dependencies" $
81+ Store.set store ["Hakyll.Core.Run.run", "dependencies"] $
8182 analyzerGraph $ hakyllAnalyzer state'
8283
8384 -- Flush and return
src/Hakyll/Core/Store.hs+120−87
@@ -1,111 +1,120 @@
1−-- | A store for stroing and retreiving items
2−--
3−{-# LANGUAGE ExistentialQuantification, ScopedTypeVariables #-}
1+--------------------------------------------------------------------------------
2+-- | A store for storing and retreiving items
3+{-# LANGUAGE ExistentialQuantification #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
45module Hakyll.Core.Store
56 ( Store
6− , StoreGet (..)
7− , makeStore
8− , storeSet
9− , storeGet
7+ , Result (..)
8+ , new
9+ , set
10+ , get
11+ , delete
1012 ) where
1113
12−import Control.Applicative ((<$>))
13−import System.FilePath ((</>))
14−import System.Directory (doesFileExist)
15−import Data.Maybe (fromMaybe)
1614
17−import Data.Binary (Binary, encodeFile, decodeFile)
18−import Data.Typeable (Typeable, TypeRep, cast, typeOf)
15+--------------------------------------------------------------------------------
16+import Control.Applicative ((<$>))
17+import Control.Exception (IOException, handle)
18+import qualified Crypto.Hash.MD5 as MD5
19+import Data.Binary (Binary, decodeFile, encodeFile)
20+import qualified Data.ByteString as B
21+import qualified Data.Cache.LRU.IO as Lru
22+import Data.List (intercalate)
23+import qualified Data.Text as T
24+import qualified Data.Text.Encoding as T
25+import Data.Typeable (TypeRep, Typeable, cast, typeOf)
26+import System.Directory (createDirectoryIfMissing)
27+import System.Directory (doesFileExist, removeFile)
28+import System.FilePath ((</>))
29+import Text.Printf (printf)
1930
20−import Hakyll.Core.Identifier
21−import Hakyll.Core.Util.File
22−import qualified Data.Cache.LRU.IO as LRU
2331
24−-- | Items we can store
25−--
26−data Storable = forall a. (Binary a, Typeable a) => Storable a
32+--------------------------------------------------------------------------------
33+-- | Simple wrapper type
34+data Box = forall a. Typeable a => Box a
2735
28−-- | Result when an item from the store
29−--
30−data StoreGet a = Found a
31− | NotFound
32− | WrongType TypeRep TypeRep
33− deriving (Show, Eq)
3436
35−-- | Data structure used for the store
36−--
37+--------------------------------------------------------------------------------
3738data Store = Store
3839 { -- | All items are stored on the filesystem
3940 storeDirectory :: FilePath
40− , -- | And some items are also kept in-memory
41− storeLRU :: Maybe (LRU.AtomicLRU FilePath Storable)
41+ , -- | Optionally, items are also kept in-memory
42+ storeMap :: Maybe (Lru.AtomicLRU FilePath Box)
4243 }
4344
44−-- | The size of the in-memory cache to use in items.
45−storeLRUSize :: Maybe Integer
46−storeLRUSize = Just 500
4745
46+--------------------------------------------------------------------------------
47+-- | Result of a store query
48+data Result a
49+ = Found a -- ^ Found, result
50+ | NotFound -- ^ Not found
51+ | WrongType TypeRep TypeRep -- ^ Expected, true type
52+ deriving (Show, Eq)
53+
54+
55+--------------------------------------------------------------------------------
4856-- | Initialize the store
49−--
50−makeStore :: Bool -- ^ Use in-memory caching
51− -> FilePath -- ^ Directory to use for hard disk storage
52− -> IO Store -- ^ Store
53−makeStore inMemory directory = do
54− lru <- if inMemory
55− then Just <$> LRU.newAtomicLRU storeLRUSize
56− else return Nothing
57+new :: Bool -- ^ Use in-memory caching
58+ -> FilePath -- ^ Directory to use for hard disk storage
59+ -> IO Store -- ^ Store
60+new inMemory directory = do
61+ createDirectoryIfMissing True directory
62+ ref <- if inMemory then Just <$> Lru.newAtomicLRU csize else return Nothing
5763 return Store
5864 { storeDirectory = directory
59− , storeLRU = lru
65+ , storeMap = ref
6066 }
61−
62−-- | Auxiliary: add an item to the map
63−--
64−cacheInsert :: (Binary a, Typeable a) => Store -> FilePath -> a -> IO ()
65−cacheInsert (Store _ Nothing) _ _ = return ()
66−cacheInsert (Store _ (Just lru)) path value =
67− LRU.insert path (Storable value) lru
68−
69−-- | Auxiliary: get an item from the cache
70−--
71−cacheLookup :: forall a. (Binary a, Typeable a)
72− => Store -> FilePath -> IO (StoreGet a)
73−cacheLookup (Store _ Nothing) _ = return NotFound
74−cacheLookup (Store _ (Just lru)) path = do
75− res <- LRU.lookup path lru
76− case res of
77− Nothing -> return NotFound
78− Just (Storable s) -> return $ case cast s of
79− Nothing -> WrongType (typeOf s) $ typeOf (undefined :: a)
80− Just s' -> Found s'
81−
82−-- | Create a path
83−--
84−makePath :: Store -> String -> Identifier a -> FilePath
85−makePath store name identifier = storeDirectory store </> name
86− </> group </> toFilePath identifier </> "hakyllstore"
8767 where
88− group = fromMaybe "" $ identifierGroup identifier
68+ csize = Just 500
69+
70+
71+--------------------------------------------------------------------------------
72+-- | Auxiliary: add an item to the in-memory cache
73+cacheInsert :: Typeable a => Store -> String -> a -> IO ()
74+cacheInsert (Store _ Nothing) _ _ = return ()
75+cacheInsert (Store _ (Just lru)) key x =
76+ Lru.insert key (Box x) lru
77+
78+
79+--------------------------------------------------------------------------------
80+-- | Auxiliary: get an item from the in-memory cache
81+cacheLookup :: forall a. Typeable a => Store -> String -> IO (Result a)
82+cacheLookup (Store _ Nothing) _ = return NotFound
83+cacheLookup (Store _ (Just lru)) key = do
84+ res <- Lru.lookup key lru
85+ return $ case res of
86+ Nothing -> NotFound
87+ Just (Box x) -> case cast x of
88+ Just x' -> Found x'
89+ Nothing -> WrongType (typeOf (undefined :: a)) (typeOf x)
90+
91+
92+--------------------------------------------------------------------------------
93+-- | Auxiliary: delete an item from the in-memory cache
94+cacheDelete :: Store -> String -> IO ()
95+cacheDelete (Store _ Nothing) _ = return ()
96+cacheDelete (Store _ (Just lru)) key = do
97+ _ <- Lru.delete key lru
98+ return ()
8999
100+
101+--------------------------------------------------------------------------------
90102-- | Store an item
91−--
92−storeSet :: (Binary a, Typeable a)
93− => Store -> String -> Identifier a -> a -> IO ()
94−storeSet store name identifier value = do
95− makeDirectories path
96− encodeFile path value
97− cacheInsert store path value
103+set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
104+set store identifier value = do
105+ encodeFile (storeDirectory store </> key) value
106+ cacheInsert store key value
98107 where
99− path = makePath store name identifier
108+ key = hash identifier
109+
100110
111+--------------------------------------------------------------------------------
101112-- | Load an item
102−--
103−storeGet :: (Binary a, Typeable a)
104− => Store -> String -> Identifier a -> IO (StoreGet a)
105−storeGet store name identifier = do
113+get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
114+get store identifier = do
106115 -- First check the in-memory map
107− mv <- cacheLookup store path
108− case mv of
116+ ref <- cacheLookup store key
117+ case ref of
109118 -- Not found in the map, try the filesystem
110119 NotFound -> do
111120 exists <- doesFileExist path
@@ -113,10 +122,34 @@ storeGet store name identifier = do
113122 -- Not found in the filesystem either
114123 then return NotFound
115124 -- Found in the filesystem
116− else do v <- decodeFile path
117− cacheInsert store path v
118− return $ Found v
119− -- Found in the in-memory map, just return
125+ else do
126+ v <- decodeFile path
127+ cacheInsert store key v
128+ return $ Found v
129+ -- Found in the in-memory map (or wrong type), just return
120130 s -> return s
121131 where
122− path = makePath store name identifier
132+ key = hash identifier
133+ path = storeDirectory store </> key
134+
135+
136+--------------------------------------------------------------------------------
137+-- | Delete an item
138+delete :: Store -> [String] -> IO ()
139+delete store identifier = do
140+ cacheDelete store key
141+ deleteFile $ storeDirectory store </> key
142+ where
143+ key = hash identifier
144+
145+
146+--------------------------------------------------------------------------------
147+hash :: [String] -> String
148+hash = concatMap (printf "%02x") . B.unpack .
149+ MD5.hash . T.encodeUtf8 . T.pack . intercalate "/"
150+
151+
152+--------------------------------------------------------------------------------
153+-- | Delete a file unless it doesn't exist...
154+deleteFile :: FilePath -> IO ()
155+deleteFile = handle (\(_ :: IOException) -> return ()) . removeFile
tests/Hakyll/Core/Store/Tests.hs+41−40
@@ -1,67 +1,68 @@
1+--------------------------------------------------------------------------------
12{-# LANGUAGE OverloadedStrings #-}
23module Hakyll.Core.Store.Tests
34 ( tests
45 ) where
56
6−import Control.Applicative ((<$>))
7−import Control.Monad (replicateM)
87
9−import Test.Framework
10−import Test.Framework.Providers.QuickCheck2
11−import Test.Framework.Providers.HUnit
12−import Test.QuickCheck
13−import Test.QuickCheck.Monadic
14−import qualified Test.HUnit as H
8+--------------------------------------------------------------------------------
9+import Data.Typeable (typeOf)
10+import Test.Framework
11+import Test.Framework.Providers.HUnit
12+import Test.Framework.Providers.QuickCheck2
13+import qualified Test.HUnit as H
14+import Test.QuickCheck
15+import Test.QuickCheck.Monadic
1516
16−import Hakyll.Core.Identifier
17−import Hakyll.Core.Store
18−import TestSuite.Util
1917
18+--------------------------------------------------------------------------------
19+import qualified Hakyll.Core.Store as Store
20+import TestSuite.Util
21+
22+
23+--------------------------------------------------------------------------------
2024tests :: [Test]
2125tests =
22− [ testProperty "simple storeGet . storeSet" simpleSetGet
23− , testProperty "persistent storeGet . storeSet" persistentSetGet
24− , testCase "WrongType storeGet . storeSet" wrongType
26+ [ testProperty "simple get . set" simpleSetGet
27+ , testProperty "persistent get . set" persistentSetGet
28+ , testCase "WrongType get . set" wrongType
2529 ]
2630
31+
32+--------------------------------------------------------------------------------
2733simpleSetGet :: Property
2834simpleSetGet = monadicIO $ do
29− identifier <- parseIdentifier . unFileName <$> pick arbitrary
30− FileName name <- pick arbitrary
35+ key <- pick arbitrary
3136 value <- pick arbitrary
3237 store <- run $ makeStoreTest
33− run $ storeSet store name identifier (value :: String)
34− value' <- run $ storeGet store name identifier
35− assert $ Found value == value'
38+ run $ Store.set store key (value :: String)
39+ value' <- run $ Store.get store key
40+ assert $ Store.Found value == value'
41+
3642
43+--------------------------------------------------------------------------------
3744persistentSetGet :: Property
3845persistentSetGet = monadicIO $ do
39− identifier <- parseIdentifier . unFileName <$> pick arbitrary
40− FileName name <- pick arbitrary
41− value <- pick arbitrary
46+ key <- pick arbitrary
47+ value <- pick arbitrary
4248 store1 <- run $ makeStoreTest
43− run $ storeSet store1 name identifier (value :: String)
49+ run $ Store.set store1 key (value :: String)
4450 -- Now Create another store from the same dir to test persistence
4551 store2 <- run $ makeStoreTest
46− value' <- run $ storeGet store2 name identifier
47− assert $ Found value == value'
52+ value' <- run $ Store.get store2 key
53+ assert $ Store.Found value == value'
54+
4855
56+--------------------------------------------------------------------------------
4957wrongType :: H.Assertion
5058wrongType = do
5159 store <- makeStoreTest
5260 -- Store a string and try to fetch an int
53− storeSet store "foo" "bar" ("qux" :: String)
54− value <- storeGet store "foo" "bar" :: IO (StoreGet Int)
55− H.assert $ case value of WrongType _ _ -> True
56− _ -> False
57−
58−newtype FileName = FileName {unFileName :: String}
59− deriving (Show)
60−
61−instance Arbitrary FileName where
62− arbitrary = do
63− length' <- choose (5, 100)
64− str <- replicateM length' $ elements cs
65− return $ FileName str
66− where
67− cs = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ ".- "
61+ Store.set store ["foo", "bar"] ("qux" :: String)
62+ value <- Store.get store ["foo", "bar"] :: IO (Store.Result Int)
63+ print value
64+ H.assert $ case value of
65+ Store.WrongType e t ->
66+ e == typeOf (undefined :: Int) &&
67+ t == typeOf (undefined :: String)
68+ _ -> False
tests/TestSuite/Util.hs+3−2
@@ -16,7 +16,8 @@ import Hakyll.Core.Compiler.Internal
1616import Hakyll.Core.Identifier
1717import Hakyll.Core.Logger
1818import Hakyll.Core.Resource.Provider
19−import Hakyll.Core.Store
19+import Hakyll.Core.Store (Store)
20+import qualified Hakyll.Core.Store as Store
2021
2122fromAssertions :: String -- ^ Name
2223 -> [Assertion] -- ^ Cases
@@ -28,7 +29,7 @@ fromAssertions name = zipWith testCase names
2829-- | Create a store for testing
2930--
3031makeStoreTest :: IO Store
31−makeStoreTest = makeStore True "_store"
32+makeStoreTest = Store.new True "_store"
3233
3334-- | Testing for 'runCompilerJob'
3435--