{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
module Data.CAS.ContentStore
(
withStore
, open
, close
, CacherM (..)
, Cacher
, defaultCacherWithIdent
, defaultIOCacherWithIdent
, cacheKleisliIO
, putInStore
, contentPath
, listAll
, listPending
, listComplete
, listItems
, query
, isMissing
, isPending
, isComplete
, lookup
, lookupOrWait
, waitUntilComplete
, constructOrAsync
, constructOrWait
, constructIfMissing
, withConstructIfMissing
, markPending
, markComplete
, removeFailed
, removeForcibly
, removeItemForcibly
, assignAlias
, lookupAlias
, removeAlias
, listAliases
, getBackReferences
, setInputs
, getInputs
, setMetadata
, getMetadata
, createMetadataFile
, getMetadataFile
, itemHash
, itemPath
, itemRelPath
, contentItem
, contentFilename
, root
, ContentStore
, Item
, Content (..)
, (^</>)
, Alias (..)
, Status (..)
, Status_
, Update (..)
, StoreError (..)
) where
import Prelude hiding (lookup)
import Control.Arrow (second)
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.MVar
import Control.Exception.Safe
import Control.Lens
import Control.Monad (forM_, forever, unless,
void, when, (<=<), (>=>),
mzero)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Control (MonadBaseControl)
import Crypto.Hash (hashUpdate)
import Data.Aeson (FromJSON, ToJSON)
import Data.Bits (complement)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C8
import Data.CAS.ContentStore.Notify
import Data.CAS.StoreOrphans ()
import Data.Foldable (asum)
import qualified Data.Hashable
import Data.List (foldl', stripPrefix)
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Monoid ((<>))
import qualified Data.Store
import Data.String (IsString (..))
import qualified Data.Text as T
import Data.Typeable (Typeable)
import Data.Void
import qualified Database.SQLite.Simple as SQL
import qualified Database.SQLite.Simple.FromField as SQL
import qualified Database.SQLite.Simple.ToField as SQL
import GHC.Generics (Generic)
import Path
import Path.IO
import System.Directory (removePathForcibly)
import System.FilePath (dropTrailingPathSeparator)
import System.IO (Handle, IOMode (..),
openFile)
import System.Posix.Files
import System.Posix.Types
import Data.CAS.ContentHashable (ContentHash,
ContentHashable (..),
DirectoryContent (..),
contentHashUpdate_fingerprint,
decodeHash, encodeHash, pathToHash,
toBytes)
import Data.CAS.Lock
import qualified Data.CAS.RemoteCache as Remote
data Status missing pending complete
= Missing missing
| Pending pending
| Complete complete
deriving (Eq, Show)
type Status_ = Status () () ()
data Update
= Completed Item
| Failed
deriving (Eq, Show)
data StoreError
= NotPending ContentHash
| AlreadyPending ContentHash
| AlreadyComplete ContentHash
| CorruptedLink ContentHash FilePath
| FailedToConstruct ContentHash
| IncompatibleStoreVersion (Path Abs Dir) Int Int
| MalformedMetadataEntry ContentHash SQL.SQLData
deriving (Show, Typeable)
instance Exception StoreError where
displayException = \case
NotPending hash ->
"The following input hash is not pending '"
++ C8.unpack (encodeHash hash)
++ "'."
AlreadyPending hash ->
"The following input hash is already pending '"
++ C8.unpack (encodeHash hash)
++ "'."
AlreadyComplete hash ->
"The following input hash is already completed '"
++ C8.unpack (encodeHash hash)
++ "'."
CorruptedLink hash fp ->
"The completed input hash '"
++ C8.unpack (encodeHash hash)
++ "' points to an invalid store item '"
++ fp
++ "'."
FailedToConstruct hash ->
"Failed to construct the input hash '"
++ C8.unpack (encodeHash hash)
++ "'."
IncompatibleStoreVersion storeDir actual expected ->
"The store in '"
++ fromAbsDir storeDir
++ "' has version "
++ show actual
++ ". This software expects version "
++ show expected
++ ". No automatic migration is available, \
\please use a fresh store location."
MalformedMetadataEntry hash key ->
"The metadtaa entry for hash '"
++ C8.unpack (encodeHash hash)
++ "' under key '"
++ show key
++ "' is malformed."
data ContentStore = ContentStore
{ storeRoot :: Path Abs Dir
, storeLock :: Lock
, storeNotifier :: Notifier
, storeDb :: SQL.Connection
}
data Item = Item { itemHash :: ContentHash }
deriving (Eq, Ord, Show, Generic)
instance Monad m => ContentHashable m Item where
contentHashUpdate ctx item =
flip contentHashUpdate_fingerprint item
>=> pure . flip hashUpdate (toBytes $ itemHash item)
$ ctx
instance FromJSON Item
instance ToJSON Item
instance Data.Hashable.Hashable Item
instance Data.Store.Store Item
data Content t where
All :: Item -> Content Dir
(:</>) :: Item -> Path Rel t -> Content t
infixr 5 :</>
deriving instance Eq (Content t)
deriving instance Show (Content t)
instance Monad m => ContentHashable m (Content Dir) where
contentHashUpdate ctx x = case x of
All i ->
flip contentHashUpdate_fingerprint x
>=> flip contentHashUpdate i
$ ctx
i :</> p ->
flip contentHashUpdate_fingerprint x
>=> flip contentHashUpdate i
>=> flip contentHashUpdate p
$ ctx
instance Monad m => ContentHashable m (Content File) where
contentHashUpdate ctx x = case x of
i :</> p ->
flip contentHashUpdate_fingerprint x
>=> flip contentHashUpdate i
>=> flip contentHashUpdate p
$ ctx
(^</>) :: Content Dir -> Path Rel t -> Content t
All item ^</> path = item :</> path
(item :</> dir) ^</> path = item :</> dir </> path
infixl 4 ^</>
newtype Alias = Alias { unAlias :: T.Text }
deriving (ContentHashable IO, Eq, Ord, Show, SQL.FromField, SQL.ToField, Data.Store.Store)
root :: ContentStore -> Path Abs Dir
root = storeRoot
itemRelPath :: Item -> Path Rel Dir
itemRelPath (Item x) = prefixHashPath itemPrefix x
itemPath :: ContentStore -> Item -> Path Abs Dir
itemPath store = mkItemPath store . itemHash
contentItem :: Content t -> Item
contentItem (All i) = i
contentItem (i :</> _) = i
contentFilename :: Content File -> Path Rel File
contentFilename (_ :</> relPath) = filename relPath
contentPath :: ContentStore -> Content t -> Path Abs t
contentPath store (All item) = itemPath store item
contentPath store (item :</> dir) = itemPath store item </> dir
open :: Path Abs Dir -> IO ContentStore
open storeRoot = do
createDirIfMissing True storeRoot
storeLock <- openLock (lockPath storeRoot)
withLock storeLock $ withWritableStoreRoot storeRoot $ do
storeDb <- SQL.open (fromAbsFile $ dbPath storeRoot)
initDb storeRoot storeDb
createDirIfMissing True (metadataPath storeRoot)
storeNotifier <- initNotifier
return ContentStore {..}
close :: ContentStore -> IO ()
close store = do
closeLock (storeLock store)
killNotifier (storeNotifier store)
SQL.close (storeDb store)
withStore :: (MonadIO m, MonadMask m)
=> Path Abs Dir -> (ContentStore -> m a) -> m a
withStore root' = bracket (liftIO $ open root') (liftIO . close)
listAll :: MonadIO m => ContentStore -> m ([ContentHash], [ContentHash], [Item])
listAll ContentStore {storeRoot} = liftIO $
foldr go ([], [], []) . fst <$> listDir storeRoot
where
go d prev@(builds, outs, items) = fromMaybe prev $ asum
[ parsePending d >>= \x -> Just (x:builds, outs, items)
, parseComplete d >>= \x -> Just (builds, x:outs, items)
, parseItem d >>= \x -> Just (builds, outs, x:items)
]
parsePending :: Path Abs Dir -> Maybe ContentHash
parsePending = pathToHash <=< stripPrefix pendingPrefix . extractDir
parseComplete :: Path Abs Dir -> Maybe ContentHash
parseComplete = pathToHash <=< stripPrefix completePrefix . extractDir
parseItem :: Path Abs Dir -> Maybe Item
parseItem = fmap Item . pathToHash <=< stripPrefix itemPrefix . extractDir
extractDir :: Path Abs Dir -> FilePath
extractDir = dropTrailingPathSeparator . fromRelDir . dirname
listPending :: MonadIO m => ContentStore -> m [ContentHash]
listPending = fmap (^._1) . listAll
listComplete :: MonadIO m => ContentStore -> m [ContentHash]
listComplete = fmap (^._2) . listAll
listItems :: MonadIO m => ContentStore -> m [Item]
listItems = fmap (^._3) . listAll
query :: MonadIO m => ContentStore -> ContentHash -> m (Status () () ())
query store hash = liftIO . withStoreLock store $
internalQuery store hash >>= pure . \case
Missing _ -> Missing ()
Pending _ -> Pending ()
Complete _ -> Complete ()
isMissing :: MonadIO m => ContentStore -> ContentHash -> m Bool
isMissing store hash = (== Missing ()) <$> query store hash
isPending :: MonadIO m => ContentStore -> ContentHash -> m Bool
isPending store hash = (== Pending ()) <$> query store hash
isComplete :: MonadIO m => ContentStore -> ContentHash -> m Bool
isComplete store hash = (== Complete ()) <$> query store hash
lookup :: MonadIO m => ContentStore -> ContentHash -> m (Status () () Item)
lookup store hash = liftIO . withStoreLock store $
internalQuery store hash >>= \case
Missing () -> return $ Missing ()
Pending _ -> return $ Pending ()
Complete item -> return $ Complete item
lookupOrWait
:: MonadIO m
=> ContentStore
-> ContentHash
-> m (Status () (Async Update) Item)
lookupOrWait store hash = liftIO . withStoreLock store $
internalQuery store hash >>= \case
Complete item -> return $ Complete item
Missing () -> return $ Missing ()
Pending _ -> Pending <$> internalWatchPending store hash
waitUntilComplete :: MonadIO m => ContentStore -> ContentHash -> m (Maybe Item)
waitUntilComplete store hash = lookupOrWait store hash >>= \case
Complete item -> return $ Just item
Missing () -> return Nothing
Pending a -> liftIO (wait a) >>= \case
Completed item -> return $ Just item
Failed -> return Nothing
constructOrAsync
:: forall m remoteCache.
(MonadIO m, MonadBaseControl IO m, MonadMask m, Remote.Cacher m remoteCache)
=> ContentStore
-> remoteCache
-> ContentHash
-> m (Status (Path Abs Dir) (Async Update) Item)
constructOrAsync store remoteCacher hash =
constructIfMissing store remoteCacher hash >>= \case
Complete item -> return $ Complete item
Missing path -> return $ Missing path
Pending _ -> Pending <$> liftIO (internalWatchPending store hash)
constructOrWait
:: (MonadIO m, MonadMask m, MonadBaseControl IO m, Remote.Cacher m remoteCache)
=> ContentStore
-> remoteCache
-> ContentHash
-> m (Status (Path Abs Dir) Void Item)
constructOrWait store remoteCacher hash = constructOrAsync store remoteCacher hash >>= \case
Pending a -> liftIO (wait a) >>= \case
Completed item -> return $ Complete item
Failed -> liftIO . throwIO $ FailedToConstruct hash
Complete item -> return $ Complete item
Missing dir -> return $ Missing dir
constructIfMissing
:: (MonadIO m, MonadBaseControl IO m, MonadMask m, Remote.Cacher m remoteCache)
=> ContentStore
-> remoteCache
-> ContentHash
-> m (Status (Path Abs Dir) () Item)
constructIfMissing store remoteCacher hash = withStoreLock store $
internalQuery store hash >>= \case
Complete item -> return $ Complete item
Missing () -> withWritableStore store $ do
let destDir :: Path Abs Dir = mkItemPath store hash
Remote.pull remoteCacher hash destDir >>= \case
Remote.PullOK () -> return $ Complete (Item hash)
Remote.NotInCache ->
Missing <$> liftIO (internalMarkPending store hash)
Remote.PullError _ ->
Missing <$> liftIO (internalMarkPending store hash)
Pending _ -> return $ Pending ()
withConstructIfMissing
:: (MonadIO m, MonadBaseControl IO m, MonadMask m, Remote.Cacher m remoteCache)
=> ContentStore
-> remoteCache
-> ContentHash
-> (Path Abs Dir -> m (Either e a))
-> m (Status e () (Maybe a, Item))
withConstructIfMissing store remoteCacher hash f =
bracketOnError
(constructIfMissing store remoteCacher hash)
(\case
Missing _ -> removeForcibly store hash
_ -> return ())
(\case
Pending () -> return (Pending ())
Complete item -> return (Complete (Nothing, item))
Missing fp -> f fp >>= \case
Left e -> do
removeFailed store hash
return (Missing e)
Right x -> do
item <- markComplete store hash
_ <- Remote.push remoteCacher (itemHash item) (Just hash) (itemPath store item)
return (Complete (Just x, item)))
markPending :: MonadIO m => ContentStore -> ContentHash -> m (Path Abs Dir)
markPending store hash = liftIO . withStoreLock store $
internalQuery store hash >>= \case
Complete _ -> throwIO (AlreadyComplete hash)
Pending _ -> throwIO (AlreadyPending hash)
Missing () -> withWritableStore store $
internalMarkPending store hash
markComplete :: MonadIO m => ContentStore -> ContentHash -> m Item
markComplete store inHash = liftIO . withStoreLock store $
internalQuery store inHash >>= \case
Missing () -> throwIO (NotPending inHash)
Complete _ -> throwIO (AlreadyComplete inHash)
Pending build -> withWritableStore store $ liftIO $ do
do
let metadataDir = mkMetadataDirPath store inHash
exists <- doesDirExist metadataDir
when exists $
unsetWritableRecursively metadataDir
outHash <- contentHash (DirectoryContent build)
let out = mkItemPath store outHash
link' = mkCompletePath store inHash
doesDirExist out >>= \case
True -> removePathForcibly (fromAbsDir build)
False -> do
renameDir build out
unsetWritableRecursively out
rel <- makeRelative (parent link') out
let from' = dropTrailingPathSeparator $ fromAbsDir link'
to' = dropTrailingPathSeparator $ fromRelDir rel
createSymbolicLink to' from'
addBackReference store inHash (Item outHash)
pure $! Item outHash
removeFailed :: MonadIO m => ContentStore -> ContentHash -> m ()
removeFailed store hash = liftIO . withStoreLock store $
internalQuery store hash >>= \case
Missing () -> throwIO (NotPending hash)
Complete _ -> throwIO (AlreadyComplete hash)
Pending build -> withWritableStore store $
removePathForcibly (fromAbsDir build)
removeForcibly :: MonadIO m => ContentStore -> ContentHash -> m ()
removeForcibly store hash = liftIO . withStoreLock store $ withWritableStore store $
internalQuery store hash >>= \case
Missing () -> pure ()
Pending build -> liftIO $ removePathForcibly (fromAbsDir build)
Complete _out -> liftIO $
removePathForcibly $
dropTrailingPathSeparator $ fromAbsDir $ mkCompletePath store hash
removeItemForcibly :: MonadIO m => ContentStore -> Item -> m ()
removeItemForcibly store item = liftIO . withStoreLock store $ withWritableStore store $
removePathForcibly (fromAbsDir $ itemPath store item)
instance SQL.FromField ContentHash where
fromField f = do
bs <- SQL.fromField f
case decodeHash bs of
Just h -> pure h
Nothing -> mzero
instance SQL.ToField ContentHash where
toField = SQL.toField . encodeHash
assignAlias :: MonadIO m => ContentStore -> Alias -> Item -> m ()
assignAlias store alias item =
liftIO . withStoreLock store $ withWritableStore store $ do
hash <- contentHash alias
SQL.executeNamed (storeDb store)
"INSERT OR REPLACE INTO\
\ aliases\
\ VALUES\
\ (:hash, :dest, :name)"
[ ":hash" SQL.:= hash
, ":dest" SQL.:= itemHash item
, ":name" SQL.:= alias
]
lookupAlias :: MonadIO m => ContentStore -> Alias -> m (Maybe Item)
lookupAlias store alias =
liftIO . withStoreLock store $ do
hash <- contentHash alias
r <- SQL.queryNamed (storeDb store)
"SELECT dest FROM aliases\
\ WHERE\
\ hash = :hash"
[ ":hash" SQL.:= hash ]
pure $! listToMaybe $ Item . SQL.fromOnly <$> r
removeAlias :: MonadIO m => ContentStore -> Alias -> m ()
removeAlias store alias =
liftIO . withStoreLock store $ withWritableStore store $ do
hash <- contentHash alias
SQL.executeNamed (storeDb store)
"DELETE FROM aliases\
\ WHERE\
\ hash = :hash"
[ ":hash" SQL.:= hash ]
listAliases :: MonadIO m => ContentStore -> m [(Alias, Item)]
listAliases store = liftIO . withStoreLock store $
fmap (map (second Item)) $
SQL.query_ (storeDb store)
"SELECT name, dest FROM aliases"
getBackReferences :: MonadIO m => ContentStore -> Item -> m [ContentHash]
getBackReferences store (Item outHash) = liftIO . withStoreLock store $
map SQL.fromOnly <$> SQL.queryNamed (storeDb store)
"SELECT hash FROM backrefs\
\ WHERE\
\ dest = :out"
[ ":out" SQL.:= outHash ]
setInputs :: MonadIO m => ContentStore -> ContentHash -> [Item] -> m ()
setInputs store hash items = liftIO $
withStoreLock store $
withWritableStore store $
internalQuery store hash >>= \case
Pending _ -> forM_ items $ \(Item input) ->
SQL.executeNamed (storeDb store)
"INSERT OR REPLACE INTO\
\ inputs (hash, input)\
\ VALUES\
\ (:hash, :input)"
[ ":hash" SQL.:= hash
, ":input" SQL.:= input
]
_ -> throwIO $ NotPending hash
getInputs :: MonadIO m => ContentStore -> ContentHash -> m [Item]
getInputs store hash = liftIO . withStoreLock store $
map (Item . SQL.fromOnly) <$> SQL.queryNamed (storeDb store)
"SELECT input FROM inputs\
\ WHERE\
\ hash = :hash"
[ ":hash" SQL.:= hash ]
setMetadata :: (SQL.ToField k, SQL.ToField v, MonadIO m )
=> ContentStore -> ContentHash -> k -> v -> m ()
setMetadata store hash k v = liftIO $
withStoreLock store $
withWritableStore store $
SQL.executeNamed (storeDb store)
"INSERT OR REPLACE INTO\
\ metadata (hash, key, value)\
\ VALUES\
\ (:hash, :key, :value)"
[ ":hash" SQL.:= hash
, ":key" SQL.:= k
, ":value" SQL.:= v
]
getMetadata :: (SQL.ToField k, SQL.FromField v, MonadIO m)
=> ContentStore -> ContentHash -> k -> m (Maybe v)
getMetadata store hash k = liftIO . withStoreLock store $ do
r <- SQL.queryNamed (storeDb store)
"SELECT value FROM metadata\
\ WHERE\
\ (hash = :hash AND key = :key)"
[ ":hash" SQL.:= hash
, ":key" SQL.:= k
]
case r of
[] -> pure Nothing
[[v]] -> pure $ Just v
_ -> throwIO $ MalformedMetadataEntry hash (SQL.toField k)
createMetadataFile
:: MonadIO m
=> ContentStore -> ContentHash -> Path Rel File -> m (Path Abs File, Handle)
createMetadataFile store hash file = liftIO . withStoreLock store $
internalQuery store hash >>= \case
Pending _ -> do
let path = mkMetadataFilePath store hash file
createDirIfMissing True (parent path)
handle <- openFile (fromAbsFile path) WriteMode
pure (path, handle)
_ -> throwIO $ NotPending hash
getMetadataFile
:: MonadIO m
=> ContentStore -> ContentHash -> Path Rel File -> m (Maybe (Path Abs File))
getMetadataFile store hash file = liftIO . withStoreLock store $ do
let path = mkMetadataFilePath store hash file
exists <- doesFileExist path
if exists then
pure $ Just path
else
pure Nothing
lockPath :: Path Abs Dir -> Path Abs Dir
lockPath = (</> [reldir|lock|])
dbPath :: Path Abs Dir -> Path Abs File
dbPath = (</> [relfile|metadata.db|])
metadataPath :: Path Abs Dir -> Path Abs Dir
metadataPath = (</> [reldir|metadata|])
withStoreLock :: MonadBaseControl IO m => ContentStore -> m a -> m a
withStoreLock store = withLock (storeLock store)
prefixHashPath :: C8.ByteString -> ContentHash -> Path Rel Dir
prefixHashPath pref hash
| Just dir <- Path.parseRelDir $ C8.unpack $ pref <> encodeHash hash
= dir
| otherwise = error
"[Data.CAS.ContentStore.prefixHashPath] \
\Failed to construct hash path."
pendingPrefix, completePrefix, hashPrefix, itemPrefix :: IsString s => s
pendingPrefix = "pending-"
completePrefix = "complete-"
hashPrefix = "hash-"
itemPrefix = "item-"
mkPendingPath :: ContentStore -> ContentHash -> Path Abs Dir
mkPendingPath ContentStore {storeRoot} hash =
storeRoot </> prefixHashPath pendingPrefix hash
mkCompletePath :: ContentStore -> ContentHash -> Path Abs Dir
mkCompletePath ContentStore {storeRoot} hash =
storeRoot </> prefixHashPath completePrefix hash
mkItemPath :: ContentStore -> ContentHash -> Path Abs Dir
mkItemPath ContentStore {storeRoot} hash =
storeRoot </> prefixHashPath itemPrefix hash
mkMetadataDirPath :: ContentStore -> ContentHash -> Path Abs Dir
mkMetadataDirPath ContentStore {storeRoot} hash =
metadataPath storeRoot </> prefixHashPath hashPrefix hash
mkMetadataFilePath
:: ContentStore -> ContentHash -> Path Rel File -> Path Abs File
mkMetadataFilePath store hash file =
mkMetadataDirPath store hash </> file
internalQuery
:: MonadIO m
=> ContentStore
-> ContentHash
-> m (Status () (Path Abs Dir) Item)
internalQuery store inHash = liftIO $ do
let build = mkPendingPath store inHash
link' = mkCompletePath store inHash
buildExists <- doesDirExist build
if buildExists then
pure $! Pending build
else do
linkExists <- doesDirExist link'
if linkExists then do
out <- readSymbolicLink
(dropTrailingPathSeparator $ fromAbsDir link')
case pathToHash =<< stripPrefix itemPrefix out of
Nothing -> throwIO $ CorruptedLink inHash out
Just outHash -> return $ Complete (Item outHash)
else
pure $! Missing ()
internalMarkPending :: ContentStore -> ContentHash -> IO (Path Abs Dir)
internalMarkPending store hash = do
let dir = mkPendingPath store hash
createDir dir
setDirWritable dir
let metadataDir = mkMetadataDirPath store hash
metadirExists <- doesDirExist metadataDir
when metadirExists $
setWritableRecursively metadataDir
return dir
internalWatchPending
:: ContentStore
-> ContentHash
-> IO (Async Update)
internalWatchPending store hash = do
let build = mkPendingPath store hash
let notifier = storeNotifier store
signal <- newEmptyMVar
let giveSignal = void $ tryPutMVar signal ()
watch <- addDirWatch notifier (fromAbsDir build) giveSignal
ticker <- async $ forever $ threadDelay 3007000 >> giveSignal
let stopWatching = do
cancel ticker
removeDirWatch watch
update <- newEmptyMVar
let query' = liftIO . withStoreLock store $ internalQuery store hash
loop = takeMVar signal >> query' >>= \case
Pending _ -> loop
Complete item -> tryPutMVar update $ Completed item
Missing () -> tryPutMVar update Failed
void $ async loop
async $ takeMVar update <* stopWatching
setRootDirWritable :: MonadIO m => Path Abs Dir -> m ()
setRootDirWritable storeRoot = liftIO $
setFileMode (fromAbsDir storeRoot) writableRootDirMode
writableRootDirMode :: FileMode
writableRootDirMode = writableDirMode
setRootDirReadOnly :: MonadIO m => Path Abs Dir -> m ()
setRootDirReadOnly storeRoot = liftIO $
setFileMode (fromAbsDir storeRoot) readOnlyRootDirMode
readOnlyRootDirMode :: FileMode
readOnlyRootDirMode = writableDirMode `intersectFileModes` allButWritableMode
withWritableStoreRoot :: (MonadMask m, MonadIO m) => Path Abs Dir -> m a -> m a
withWritableStoreRoot storeRoot =
bracket_ (setRootDirWritable storeRoot) (setRootDirReadOnly storeRoot)
withWritableStore :: (MonadMask m, MonadIO m) => ContentStore -> m a -> m a
withWritableStore ContentStore {storeRoot} =
withWritableStoreRoot storeRoot
setDirWritable :: Path Abs Dir -> IO ()
setDirWritable fp = setFileMode (fromAbsDir fp) writableDirMode
writableDirMode :: FileMode
writableDirMode = foldl' unionFileModes nullFileMode
[ directoryMode, ownerModes
, groupReadMode, groupExecuteMode
, otherReadMode, otherExecuteMode
]
setWritable :: Path Abs t -> IO ()
setWritable fp = do
mode <- fileMode <$> getFileStatus (toFilePath fp)
setFileMode (toFilePath fp) $ mode `unionFileModes` ownerWriteMode
unsetWritable :: Path Abs t -> IO ()
unsetWritable fp = do
mode <- fileMode <$> getFileStatus (toFilePath fp)
setFileMode (toFilePath fp) $ mode `intersectFileModes` allButWritableMode
allButWritableMode :: FileMode
allButWritableMode = complement $ foldl' unionFileModes nullFileMode
[ownerWriteMode, groupWriteMode, otherWriteMode]
setWritableRecursively :: Path Abs Dir -> IO ()
setWritableRecursively = walkDir $ \dir _ files -> do
mapM_ setWritable files
setWritable dir
return $ WalkExclude []
unsetWritableRecursively :: Path Abs Dir -> IO ()
unsetWritableRecursively = walkDir $ \dir _ files -> do
mapM_ unsetWritable files
unsetWritable dir
return $ WalkExclude []
storeVersion :: Int
storeVersion = 1
initDb :: Path Abs Dir -> SQL.Connection -> IO ()
initDb storeDir db = do
[[version]] <- SQL.query_ db "PRAGMA user_version"
if version == 0 then
SQL.execute_ db $
"PRAGMA user_version = " <> fromString (show storeVersion)
else
unless (version == storeVersion) $
throwIO $ IncompatibleStoreVersion storeDir version storeVersion
SQL.execute_ db
"CREATE TABLE IF NOT EXISTS\
\ aliases\
\ ( hash TEXT PRIMARY KEY\
\ , dest TEXT NOT NULL\
\ , name TEXT NOT NULL\
\ )"
SQL.execute_ db
"CREATE TABLE IF NOT EXISTS\
\ backrefs\
\ ( hash TEXT PRIMARY KEY\
\ , dest TEXT NOT NULL\
\ )"
SQL.execute_ db
"CREATE TABLE IF NOT EXISTS\
\ inputs\
\ ( hash TEXT NOT NULL\
\ , input TEXT NOT NULL\
\ , UNIQUE (hash, input)\
\ )"
SQL.execute_ db
"CREATE TABLE IF NOT EXISTS\
\ metadata\
\ ( hash TEXT NOT NULL\
\ , key TEXT NOT NULL\
\ , value TEXT\
\ , PRIMARY KEY(hash, key)\
\ )"
addBackReference :: ContentStore -> ContentHash -> Item -> IO ()
addBackReference store inHash (Item outHash) =
SQL.executeNamed (storeDb store)
"INSERT OR REPLACE INTO\
\ backrefs (hash, dest)\
\ VALUES\
\ (:in, :out)"
[ ":in" SQL.:= inHash
, ":out" SQL.:= outHash
]
data CacherM m i o =
NoCache
| Cache
{
cacherKey :: Int -> i -> m ContentHash
, cacherStoreValue :: o -> ByteString
, cacherReadValue :: ByteString -> o
}
type Cacher = CacherM Identity
defaultCacherWithIdent :: forall m i o.
(ContentHashable m i, Data.Store.Store o)
=> Int
-> CacherM m i o
defaultCacherWithIdent ident = Cache
{ cacherKey = \i ident' -> contentHash (ident', ident, i)
, cacherStoreValue = Data.Store.encode
, cacherReadValue = Data.Store.decodeEx
}
defaultIOCacherWithIdent :: (MonadIO m, ContentHashable IO i, Data.Store.Store o)
=> Int
-> CacherM m i o
defaultIOCacherWithIdent ident = c{cacherKey = \x i -> liftIO $ cacherKey c x i}
where c = defaultCacherWithIdent ident
cacheKleisliIO
:: (MonadIO m, MonadBaseControl IO m, MonadMask m, Remote.Cacher m remoteCache)
=> Maybe Int
-> CacherM m i o
-> ContentStore
-> remoteCache
-> (i -> m o)
-> i
-> m o
cacheKleisliIO confIdent c@Cache{} store remoteCacher f i
| Just confIdent' <- confIdent = do
chash <- cacherKey c confIdent' i
let computeAndStore fp = do
res <- f i
liftIO $ BS.writeFile (toFilePath $ fp </> [relfile|out|])
. cacherStoreValue c $ res
return $ Right res
readItem item = do
bs <- liftIO . BS.readFile $ simpleOutPath item
return . cacherReadValue c $ bs
withConstructIfMissing store remoteCacher chash computeAndStore >>= \case
Missing e -> absurd e
Pending _ ->
liftIO (waitUntilComplete store chash) >>= \case
Just item -> readItem item
Nothing -> throwM $ FailedToConstruct chash
Complete (Just a, _) -> return a
Complete (_, item) -> readItem item
where
simpleOutPath item =
toFilePath $ itemPath store item </> [relfile|out|]
cacheKleisliIO _ _ _ _ f i = f i
putInStore
:: (MonadIO m, MonadMask m, MonadBaseControl IO m
,Remote.Cacher m remoteCacher
,ContentHashable IO t)
=> ContentStore
-> remoteCacher
-> (ContentHash -> m ())
-> (Path Abs Dir -> t -> m ())
-> t
-> m Item
putInStore store remoteCacher ifExc f x = do
chash <- liftIO $ contentHash x
constructOrWait store remoteCacher chash >>= \case
Pending y -> absurd y
Complete item -> return item
Missing fp ->
do
f fp x
finalItem <- markComplete store chash
_ <- Remote.push remoteCacher (itemHash finalItem) (Just chash) (itemPath store finalItem)
pure finalItem
`onException`
(do ifExc chash
removeFailed store chash)