module Data.ContentStore(ContentStore,
CsError(..),
CsMonad,
runCsMonad,
contentStoreDigest,
contentStoreValid,
fetchByteString,
fetchByteStringC,
fetchFile,
fetchLazyByteString,
fetchLazyByteStringC,
mkContentStore,
openContentStore,
storeByteString,
storeByteStringC,
storeByteStringSink,
storeDirectory,
storeFile,
storeLazyByteString,
storeLazyByteStringC,
storeLazyByteStringSink)
where
import Conduit
import Control.Conditional(ifM, unlessM, whenM)
import Control.Monad(forM, forM_, void)
import Control.Monad.Base(MonadBase(..))
import Control.Monad.Except(ExceptT, MonadError, catchError, runExceptT, throwError)
import Control.Monad.IO.Class(MonadIO, liftIO)
import Control.Monad.Trans.Control(MonadBaseControl(..))
import Control.Monad.Trans.Resource(MonadResource, MonadThrow, ResourceT, runResourceT)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Conduit.Binary(sinkFileCautious)
import Data.Conduit.Lzma(compress, decompress)
import Data.Maybe(isNothing)
import System.Directory(canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeFile, renameFile)
import System.FilePath((</>))
import System.IO(Handle, SeekMode(..))
import System.IO.Temp(openTempFile)
import System.Posix.IO(FileLock, LockRequest(..), OpenMode(..), closeFd, defaultFileFlags, fdToHandle, getLock, handleToFd, openFd, setLock, waitToSetLock)
import Data.ContentStore.Config(Config(..), defaultConfig, readConfig, writeConfig)
import Data.ContentStore.Digest
data ContentStore = ContentStore {
csConfig :: Config,
csRoot :: FilePath,
csHash :: DigestAlgorithm
}
data CsError =
CsError String
| CsErrorCollision String
| CsErrorConfig String
| CsErrorInvalid String
| CsErrorMissing
| CsErrorNoSuchObject String
| CsErrorUnsupportedHash String
deriving (Eq, Show)
newtype CsMonad a = CsMonad { getCsMonad :: ResourceT (ExceptT CsError IO) a }
deriving (Applicative, Functor, Monad, MonadBase IO, MonadError CsError, MonadIO, MonadResource, MonadThrow)
instance MonadBaseControl IO CsMonad where
type StM CsMonad a = StM (ResourceT (ExceptT CsError IO)) a
liftBaseWith f = CsMonad $ liftBaseWith $ \r -> f (r . getCsMonad)
restoreM = CsMonad . restoreM
runCsMonad :: CsMonad a -> IO (Either CsError a)
runCsMonad x = runExceptT $ runResourceT $ getCsMonad x
csSubdirs :: [String]
csSubdirs = ["objects", "tmp", "lock"]
ensureObjectSubdirectory :: ContentStore -> String -> IO ()
ensureObjectSubdirectory cs subdir =
createDirectoryIfMissing True (objectSubdirectoryPath cs subdir)
objectSubdirectoryPath :: ContentStore -> String -> FilePath
objectSubdirectoryPath ContentStore{..} subdir =
csRoot </> "objects" </> subdir
storedObjectDestination :: ObjectDigest -> (String, String)
storedObjectDestination = storedObjectLocation . toHex
storedObjectLocation :: String -> (String, String)
storedObjectLocation = splitAt 2
findObject :: (MonadError CsError m, MonadIO m) => ContentStore -> ObjectDigest -> m FilePath
findObject cs digest = do
let (subdir, filename) = storedObjectDestination digest
path = objectSubdirectoryPath cs subdir </> filename
ifM (liftIO $ doesFileExist path)
(return path)
(throwError $ CsErrorNoSuchObject $ toHex digest)
startStore :: ContentStore -> IO (FilePath, Handle)
startStore ContentStore{..} = do
(path, fd) <- withGlobalLock csRoot $ do
(path, handle) <- openTempFile (csRoot </> "tmp") "import"
fd <- handleToFd handle
setLock fd fullLock
return (path, fd)
handle' <- fdToHandle fd
return (path, handle')
finishStore :: ContentStore -> (FilePath, Handle) -> ObjectDigest -> IO ()
finishStore cs (tmpPath, handle) digest = do
let (subdir, filename) = storedObjectDestination digest
let path = objectSubdirectoryPath cs subdir </> filename
ensureObjectSubdirectory cs subdir
renameFile tmpPath path
fd <- handleToFd handle
setLock fd fullUnlock
closeFd fd
doStore :: MonadResource m => ContentStore -> (BS.ByteString -> ObjectDigest) -> Conduit BS.ByteString m ObjectDigest
doStore cs hasher = awaitForever $ \object -> do
let digest = hasher object
let (subdir, filename) = storedObjectDestination digest
path = objectSubdirectoryPath cs subdir </> filename
liftIO $ ensureObjectSubdirectory cs subdir
liftIO $ unlessM (doesFileExist path) $ do
(tmpPath, handle) <- startStore cs
void $ runConduitRes $ yield object .| maybeCompress cs .| sinkHandle handle
finishStore cs (tmpPath, handle) digest
yield digest
doStoreSink :: MonadResource m => ContentStore -> (DigestContext -> BS.ByteString -> DigestContext) -> Sink BS.ByteString m ObjectDigest
doStoreSink cs hasher = do
(tmpPath, handle) <- liftIO $ startStore cs
let initctx = digestInit $ csHash cs
(_, digest) <- getZipConduit ((,) <$> ZipConduit (maybeCompress cs .| sinkHandle handle)
<*> ZipConduit (digestSink initctx))
let (subdir, _) = storedObjectDestination digest
liftIO $ ensureObjectSubdirectory cs subdir
liftIO $ finishStore cs (tmpPath, handle) digest
return digest
where
digestSink ctx = await >>= \case
Nothing -> return $ digestFinalize ctx
Just chunk -> digestSink $ hasher ctx chunk
fullLock :: FileLock
fullLock = (WriteLock, AbsoluteSeek, 0, 0)
fullUnlock :: FileLock
fullUnlock = (Unlock, AbsoluteSeek, 0, 0)
withGlobalLock :: MonadIO m => FilePath -> m a -> m a
withGlobalLock csRoot action = do
let lockFile = csRoot </> "lock" </> "lockfile"
fd <- liftIO $ openFd lockFile WriteOnly (Just 0o644) defaultFileFlags
liftIO $ waitToSetLock fd fullLock
ret <- action
liftIO $ setLock fd fullUnlock >> closeFd fd
return ret
cleanupTmp :: FilePath -> IO ()
cleanupTmp csRoot = withGlobalLock csRoot $ listDirectory (csRoot </> "tmp") >>= mapM_ cleanupOne
where
cleanupOne :: FilePath -> IO ()
cleanupOne tmpFile = do
let fullPath = csRoot </> tmpFile
fd <- openFd fullPath ReadOnly Nothing defaultFileFlags
whenM (isNothing <$> getLock fd fullLock) $ removeFile fullPath
identityC :: Monad m => Conduit a m a
identityC = mapC id
maybeCompress :: MonadResource m => ContentStore -> Conduit BS.ByteString m BS.ByteString
maybeCompress cs =
if confCompressed . csConfig $ cs then compress Nothing else identityC
maybeDecompress :: MonadResource m => ContentStore -> Conduit BS.ByteString m BS.ByteString
maybeDecompress cs =
if confCompressed . csConfig $ cs then decompress Nothing else identityC
contentStoreValid :: (MonadError CsError m, MonadIO m) => FilePath -> m Bool
contentStoreValid fp = do
unlessM (liftIO $ doesDirectoryExist fp) $
throwError CsErrorMissing
unlessM (liftIO $ doesFileExist $ fp </> "config") $
throwError $ CsErrorInvalid "config"
forM_ csSubdirs $ \subdir ->
unlessM (liftIO $ doesDirectoryExist $ fp </> subdir) $
throwError $ CsErrorInvalid subdir
return True
contentStoreDigest :: ContentStore -> DigestAlgorithm
contentStoreDigest ContentStore{..} = csHash
mkContentStore :: (MonadError CsError m, MonadIO m) => FilePath -> m ContentStore
mkContentStore fp = do
path <- liftIO $ canonicalizePath fp
csExists <- contentStoreValid path `catchError` \_ -> return False
if csExists then openContentStore path
else do
mapM_ (\d -> liftIO $ createDirectoryIfMissing True (path </> d))
csSubdirs
liftIO $ writeConfig (path </> "config") defaultConfig
openContentStore path
openContentStore :: (MonadError CsError m, MonadIO m) => FilePath -> m ContentStore
openContentStore fp = do
path <- liftIO $ canonicalizePath fp
void $ contentStoreValid path
liftIO $ cleanupTmp path
conf <- liftIO (readConfig $ path </> "config") >>= \case
Left e -> throwError $ CsErrorConfig (show e)
Right c -> return c
let algo = confHash conf
case getDigestAlgorithm algo of
Nothing -> throwError $ CsErrorUnsupportedHash (show algo)
Just da -> return ContentStore { csRoot=path, csConfig=conf, csHash=da }
fetchByteString :: (MonadBaseControl IO m, MonadError CsError m, MonadIO m, MonadThrow m) =>
ContentStore
-> ObjectDigest
-> m BS.ByteString
fetchByteString cs digest =
fmap BS.concat (runConduitRes (yield digest .| fetchByteStringC cs .| sinkList))
fetchByteStringC :: (MonadError CsError m, MonadResource m) => ContentStore -> Conduit ObjectDigest m BS.ByteString
fetchByteStringC cs = awaitForever $ \digest -> do
f <- findObject cs digest
contents <- sourceFile f .| maybeDecompress cs .| sinkList
yield $ BS.concat contents
storeByteString :: (MonadBaseControl IO m, MonadError CsError m, MonadIO m, MonadThrow m) =>
ContentStore
-> BS.ByteString
-> m ObjectDigest
storeByteString cs bs =
runConduitRes (yield bs .| storeByteStringC cs .| headC) >>= \case
Nothing -> throwError $ CsError "Failed to store object"
Just d -> return d
storeByteStringC :: (MonadError CsError m, MonadResource m) => ContentStore -> Conduit BS.ByteString m ObjectDigest
storeByteStringC cs = doStore cs (digestByteString $ csHash cs)
storeByteStringSink :: MonadResource m => ContentStore -> Sink BS.ByteString m ObjectDigest
storeByteStringSink cs = doStoreSink cs digestUpdate
fetchLazyByteString :: (MonadBaseControl IO m, MonadError CsError m, MonadIO m, MonadThrow m) =>
ContentStore
-> ObjectDigest
-> m LBS.ByteString
fetchLazyByteString cs digest =
fmap LBS.concat (runConduitRes (yield digest .| fetchLazyByteStringC cs .| sinkList))
fetchLazyByteStringC :: (MonadError CsError m, MonadResource m) => ContentStore -> Conduit ObjectDigest m LBS.ByteString
fetchLazyByteStringC cs = awaitForever $ \digest -> do
f <- findObject cs digest
contents <- sourceFile f .| maybeDecompress cs .| sinkList
yield $ LBS.fromStrict $ BS.concat contents
storeLazyByteString :: (MonadBaseControl IO m, MonadError CsError m, MonadIO m, MonadThrow m) =>
ContentStore
-> LBS.ByteString
-> m ObjectDigest
storeLazyByteString cs bs =
runConduitRes (yield bs .| storeLazyByteStringC cs .| headC) >>= \case
Nothing -> throwError $ CsError "Failed to store object"
Just d -> return d
storeLazyByteStringC :: (MonadError CsError m, MonadResource m) => ContentStore -> Conduit LBS.ByteString m ObjectDigest
storeLazyByteStringC cs = mapC LBS.toStrict .| doStore cs (digestByteString $ csHash cs)
storeLazyByteStringSink :: MonadResource m => ContentStore -> Sink LBS.ByteString m ObjectDigest
storeLazyByteStringSink cs = mapC LBS.toStrict .| doStoreSink cs digestUpdate
storeDirectory :: (MonadBaseControl IO m, MonadError CsError m, MonadResource m) =>
ContentStore
-> FilePath
-> m [(FilePath, ObjectDigest)]
storeDirectory cs fp = do
entries <- runConduit $ sourceDirectoryDeep False fp .| sinkList
forM entries $ \entry -> do
digest <- storeFile cs entry
return (entry, digest)
fetchFile :: (MonadBaseControl IO m, MonadError CsError m, MonadResource m) =>
ContentStore
-> ObjectDigest
-> FilePath
-> m ()
fetchFile cs digest dest =
runConduitRes $ yield digest .| fetchByteStringC cs .| sinkFileCautious dest
storeFile :: (MonadBaseControl IO m, MonadError CsError m, MonadResource m) =>
ContentStore
-> FilePath
-> m ObjectDigest
storeFile cs fp = do
lbs <- liftIO $ LBS.readFile fp
storeLazyByteString cs lbs