module HashStore ( hashStore , hashStoreWithContent ) where import Crypto.Hash.BLAKE2.BLAKE2b (hash) import Data.ByteString.Base16 (encode) import Data.ByteString.Char8 (ByteString) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (()) import qualified Data.ByteString.Char8 as BS -- | Blake2b hash encoded in base16. newtype Hash = Hash { unHash :: ByteString } deriving (Eq) -- | Perform 'ByteString' hashing. hashContent :: ByteString -> Hash hashContent = Hash . encode . hash 64 mempty {-# INLINE hashContent #-} -- | Build the new 'FilePath' adding the hash of the provided content. hashFile :: ByteString -> FilePath -> FilePath hashFile content file = BS.unpack (unHash $ hashContent content) ++ "-" ++ file {-# INLINE hashFile #-} {- | @'hashStore' storePath action (name, content)@ computes @hash@ of @content@ and performs @action@ if file @storePath/hash-name@ doesn't exist, writing new content in this file. The property is that for any item @(name, hash)@, we get a valid filepath by @storePath/hash-name@. This functions solves the following problem. The caller doesn't need the content. Then it is better not to return the bytestring (we avoid the overhrad of reading the file in the cache hit case). -} hashStore :: FilePath -- ^ Directory to store file contents with new hash -> (ByteString -> IO ByteString) -- ^ Action to be performed if hash is different -> (String, ByteString) -- ^ File name -> IO String -- ^ File name with hash hashStore storePath action (name, actionInput) = do createDirectoryIfMissing True storePath let hashName = hashFile actionInput name let storeName = storePath hashName isUpToDate <- doesFileExist storeName if isUpToDate then pure hashName else do actionOutput <- action actionInput BS.writeFile storeName actionOutput pure hashName {- | Like 'hashStore' but also returns file content of hashed file. This functions solves the following problem. The caller needs the file content. Then it's better to return the bytestring (we avoid the overhead of reading the file in the cache miss case). -} hashStoreWithContent :: FilePath -- ^ Directory to store file contents with new hash -> (ByteString -> IO ByteString) -- ^ Action to be performed if hash is different -> (String, ByteString) -- ^ File name -> IO (String, ByteString) -- ^ File name with hash and content of file hashStoreWithContent storePath action (name, actionInput) = do -- code duplication with `hashStore` could be avoided, but it doesn't -- actually make code shorter or simpler createDirectoryIfMissing True storePath let hashName = hashFile actionInput name let storeName = storePath hashName isUpToDate <- doesFileExist storeName if isUpToDate then do fileContent <- BS.readFile storeName pure (hashName, fileContent) else do newFileContent <- action actionInput BS.writeFile storeName newFileContent pure (hashName, newFileContent)