-- | -- Module : Data.Git.Storage.Loose -- License : BSD-style -- Maintainer : Vincent Hanquez -- Stability : experimental -- Portability : unix -- {-# LANGUAGE OverloadedStrings, ScopedTypeVariables, ViewPatterns #-} module Data.Git.Storage.Loose ( Zipped(..) -- * marshall from and to lazy bytestring , looseUnmarshall , looseUnmarshallRaw , looseUnmarshallZipped , looseUnmarshallZippedRaw , looseMarshall -- * read and check object existence , looseRead , looseReadHeader , looseReadRaw , looseExists -- * write objects , looseWriteBlobFromFile , looseWrite -- * enumeration of loose objects , looseEnumeratePrefixes , looseEnumerateWithPrefixFilter , looseEnumerateWithPrefix ) where import Codec.Compression.Zlib import Data.Git.Ref import Data.Git.Path import Data.Git.Internal import Data.Git.OS import Data.Git.Imports import Data.Git.Storage.FileWriter import Data.Git.Storage.Object import qualified Data.Git.Parser as P import qualified Data.ByteString.Lazy as L import qualified Data.ByteString as B import Control.Exception (onException, SomeException) import qualified Control.Exception as E import Data.String import Data.Char (isHexDigit) newtype Zipped = Zipped { getZippedData :: L.ByteString } deriving (Show,Eq) readZippedFile :: LocalPath -> IO Zipped readZippedFile fp = Zipped <$> readBinaryFileLazy fp dezip :: Zipped -> L.ByteString dezip = decompress . getZippedData isObjectPrefix :: [Char] -> Bool isObjectPrefix [a,b] = isHexDigit a && isHexDigit b isObjectPrefix _ = False -- loose object parsing parseHeader :: P.Parser (ObjectHeader hash) parseHeader = do h <- P.takeWhile1 ((/=) 0x20) _ <- P.byte 0x20 sz <- P.decimal :: P.Parser Int return (objectTypeUnmarshall h, fromIntegral sz, Nothing) data HeaderType = HeaderTree | HeaderTag | HeaderCommit | HeaderBlob parseTreeHeader, parseTagHeader, parseCommitHeader, parseBlobHeader :: P.Parser HeaderType parseTreeHeader = P.string "tree " >> parseLength >> P.byte 0 >> return HeaderTree parseTagHeader = P.string "tag " >> parseLength >> P.byte 0 >> return HeaderTag parseCommitHeader = P.string "commit " >> parseLength >> P.byte 0 >> return HeaderCommit parseBlobHeader = P.string "blob " >> parseLength >> P.byte 0 >> return HeaderBlob parseLength :: P.Parser Int parseLength = P.decimal parseObject :: HashAlgorithm hash => L.ByteString -> Object hash parseObject = parseSuccess getOne where parseSuccess p = either (error . ("parseObject: " ++)) id . P.eitherParseChunks p . L.toChunks getOne = do hdrType <- parseTreeHeader <|> parseBlobHeader <|> parseCommitHeader <|> parseTagHeader case hdrType of HeaderTree -> objectParseTree HeaderTag -> objectParseTag HeaderCommit -> objectParseCommit HeaderBlob -> objectParseBlob -- | unmarshall an object (with header) from a bytestring. looseUnmarshall :: HashAlgorithm hash => L.ByteString -> Object hash looseUnmarshall = parseObject -- | unmarshall an object (with header) from a zipped stream. looseUnmarshallZipped :: HashAlgorithm hash => Zipped -> Object hash looseUnmarshallZipped = parseObject . dezip -- | unmarshall an object as (header, data) tuple from a bytestring looseUnmarshallRaw :: L.ByteString -> (ObjectHeader hash, ObjectData) looseUnmarshallRaw stream = case L.findIndex ((==) 0) stream of Nothing -> error "object not right format. missing 0" Just idx -> let (h, r) = L.splitAt (idx+1) stream in case P.maybeParseChunks parseHeader (L.toChunks h) of Nothing -> error "cannot open object" Just hdr -> (hdr, r) -- | unmarshall an object as (header, data) tuple from a zipped stream looseUnmarshallZippedRaw :: Zipped -> (ObjectHeader hash, ObjectData) looseUnmarshallZippedRaw = looseUnmarshallRaw . dezip -- | read a specific ref from a loose object and returns an header and data. looseReadRaw :: HashAlgorithm hash => LocalPath -> Ref hash -> IO (ObjectHeader hash, ObjectData) looseReadRaw repoPath ref = looseUnmarshallZippedRaw <$> readZippedFile (objectPathOfRef repoPath ref) -- | read only the header of a loose object. looseReadHeader :: HashAlgorithm hash => LocalPath -> Ref hash -> IO (ObjectHeader hash) looseReadHeader repoPath ref = toHeader <$> readZippedFile (objectPathOfRef repoPath ref) where toHeader = either (error . ("parseHeader: " ++)) id . P.eitherParseChunks parseHeader . L.toChunks . dezip -- | read a specific ref from a loose object and returns an object looseRead :: HashAlgorithm hash => LocalPath -> Ref hash -> IO (Object hash) looseRead repoPath ref = looseUnmarshallZipped <$> readZippedFile (objectPathOfRef repoPath ref) -- | check if a specific ref exists as loose object looseExists :: HashAlgorithm hash => LocalPath -> Ref hash -> IO Bool looseExists repoPath ref = isFile (objectPathOfRef repoPath ref) -- | enumarate all prefixes available in the object store. looseEnumeratePrefixes :: LocalPath -> IO [[Char]] looseEnumeratePrefixes repoPath = filter isObjectPrefix <$> getDirectoryContents (repoPath fromString "objects") -- | enumerate all references available with a specific prefix. looseEnumerateWithPrefixFilter :: HashAlgorithm hash => LocalPath -> String -> (Ref hash -> Bool) -> IO [Ref hash] looseEnumerateWithPrefixFilter repoPath prefix filterF = filter filterF . map (fromHexString . (prefix ++)) . filter isRef <$> getDir (repoPath fromString "objects" fromString prefix) where getDir p = E.catch (getDirectoryContents p) (\(_::SomeException) -> return []) isRef l = length l == 38 looseEnumerateWithPrefix :: HashAlgorithm hash => LocalPath -> String -> IO [Ref hash] looseEnumerateWithPrefix repoPath prefix = looseEnumerateWithPrefixFilter repoPath prefix (const True) -- | marshall as lazy bytestring an object except deltas. looseMarshall :: Object hash -> L.ByteString looseMarshall obj | objectIsDelta obj = error "cannot write delta object loose" | otherwise = L.concat [ L.fromChunks [hdrB], objData ] where objData = objectWrite obj hdrB = objectWriteHeader (objectToType obj) (fromIntegral $ L.length objData) -- | create a new blob on a temporary location and on success move it to -- the object store with its digest name. looseWriteBlobFromFile :: HashAlgorithm hash => LocalPath -> LocalPath -> IO (Ref hash) looseWriteBlobFromFile repoPath file = do fsz <- getSize file let hdr = objectWriteHeader TypeBlob (fromIntegral fsz) tmpPath <- objectTemporaryPath repoPath flip onException (removeFile tmpPath) $ do (ref, npath) <- withFileWriter tmpPath $ \fw -> do fileWriterOutput fw hdr withFile file ReadMode $ \h -> loop h fw digest <- fileWriterGetDigest fw return (digest, objectPathOfRef repoPath digest) exists <- isFile npath when exists $ error "destination already exists" rename tmpPath npath return ref where loop h fw = do r <- B.hGet h (32*1024) if B.null r then return () else fileWriterOutput fw r >> loop h fw -- | write an object to disk as a loose reference. -- use looseWriteBlobFromFile for efficiently writing blobs when being commited from a file. looseWrite :: HashAlgorithm hash => LocalPath -> Object hash -> IO (Ref hash) looseWrite repoPath obj = createParentDirectory path >> isFile path >>= \exists -> unless exists (writeFileLazy path $ compress content) >> return ref where path = objectPathOfRef repoPath ref content = looseMarshall obj ref = hashLBS content writeFileLazy p bs = withFile p WriteMode (\h -> L.hPut h bs) getDirectoryContents :: LocalPath -> IO [String] getDirectoryContents p = listDirectoryFilename p