aboutsummaryrefslogtreecommitdiff
path: root/app/Store.hs
blob: 9e0ef0011724ec3f7f794364942f5d1e00f9e829 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilyDependencies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- |  Read-only on-disk key-value store

module Store where

import           Control.DeepSeq                ( NFData )
import           Control.Monad.IO.Class         ( liftIO )
import qualified Control.Monad.State.Strict    as S
import qualified Data.ByteString               as BS
import qualified Data.ByteString.Short         as BSS
import qualified Data.Map.Strict               as M
import           Data.Serialize                 ( Serialize
                                                , decode
                                                , encode
                                                )
import           GHC.Generics                   ( Generic )
import           Prelude                 hiding ( lookup )
import           System.Directory               ( doesFileExist )
import           System.FilePath                ( (</>) )
import           System.IO                      ( Handle
                                                , IOMode(..)
                                                , hTell
                                                , withFile
                                                )
import           System.IO.MMap                 ( mmapFileByteString )

data Store = Store
  { index  :: M.Map BSS.ShortByteString Location
  , values :: BS.ByteString
  }

data Location = Location
  { offset :: Int
  , length :: Int
  }
  deriving (Show, Eq, Ord, Generic, NFData)

instance Serialize Location

class StoreItem item where
  toByteString :: item -> BS.ByteString
  fromByteString :: BS.ByteString -> Either String item
  type KeyArgs item = arg | arg -> item
  itemKey :: KeyArgs item -> BSS.ShortByteString

indexFileName :: FilePath
indexFileName = "index"

valuesFileName :: FilePath
valuesFileName = "values"

data ReadMode
  = ReadEntireFile
  | MemoryMapFile
  deriving (Show, Eq)

load :: FilePath -> ReadMode -> IO (Either String Store)
load directoryPath readMode = do
  let valuesFilePath = directoryPath </> valuesFileName
      indexFilePath  = directoryPath </> indexFileName
  (valuesFileExists, indexFileExists) <-
    (,) <$> doesFileExist indexFilePath <*> doesFileExist valuesFilePath
  case (valuesFileExists, indexFileExists) of
    (True, True) -> do
      indexFile  <- BS.readFile indexFilePath
      valuesFile <- case readMode of
        ReadEntireFile -> BS.readFile valuesFilePath
        MemoryMapFile  -> mmapFileByteString valuesFilePath Nothing
      let eitherIndex = decode @(M.Map BSS.ShortByteString Location) indexFile
      case eitherIndex of
        Right locMap ->
          return . Right $ Store { index = locMap, values = valuesFile }
        Left err -> return . Left $ "Error while decoding index : " ++ err
    (False, False) ->
      return . Left $ "Cannot find index and values in " ++ directoryPath
    (True , False) -> return . Left $ "Cannot find index in " ++ directoryPath
    (False, True ) -> return . Left $ "Cannot find values in " ++ directoryPath

lookup :: (StoreItem item) => KeyArgs item -> Store -> Either String item
lookup keyArgs = lookupByteString (itemKey keyArgs)

lookupByteString
  :: (StoreItem item) => BSS.ShortByteString -> Store -> Either String item
lookupByteString key store = case M.lookup key (index store) of
  Just (Location off len) ->
    fromByteString . BS.take len . BS.drop off $ values store
  Nothing -> Left $ "Cannot find item with key " ++ show key

data State = State (M.Map BSS.ShortByteString Location) Handle

add :: (StoreItem item) => item -> KeyArgs item -> S.StateT State IO ()
add item keyArgs = do
  let bs  = toByteString item
      len = BS.length bs
  State locMap handle <- S.get
  off                 <- liftIO $ fromIntegral <$> hTell handle
  _                   <- liftIO $ BS.hPut handle bs
  S.put $ State (M.insert (itemKey keyArgs) (Location off len) locMap) handle

createStore
  :: FilePath -> (Handle -> IO (M.Map BSS.ShortByteString Location)) -> IO ()
createStore directoryPath action =
  withFile (directoryPath </> valuesFileName) WriteMode $ \valuesHandle -> do
    locMap <- action valuesHandle
    BS.writeFile (directoryPath </> indexFileName) (encode locMap)

writeValues
  :: Handle
  -> M.Map BSS.ShortByteString Location
  -> S.StateT State IO ()
  -> IO (M.Map BSS.ShortByteString Location)
writeValues handle locMap action = do
  State locMap' _ <- S.execStateT action (State locMap handle)
  return locMap'