cachix-1.4: src/Cachix/Client/Store.hs
{-# LANGUAGE OverloadedStrings #-}
module Cachix.Client.Store (withStore, Store, PathInfo (..), StorePath (..), base16to32, computeClosure, queryPathInfo, followLinksToStorePath, getStorePathHash, getStorePathBaseName, getPath) where
import Cachix.Client.ProcessGraph (processGraph)
import Data.ByteArray.Encoding (Base (..), convertFromBase)
import qualified Data.Set as Set
import qualified Data.Text as T
import Database.SQLite3 (SQLData)
import qualified Database.SQLite3 as SQLite
import Protolude hiding (toS)
import Protolude.Conv
import System.Console.Pretty (Color (..), color)
import System.Directory (canonicalizePath)
import qualified System.Nix.Base32
import System.Process (readProcessWithExitCode)
type StorePrefix = Text
data Store = Store StorePrefix SQLite.Database
data StorePath = StorePath Text
deriving (Eq, Ord)
data PathInfo = PathInfo
{ deriver :: Maybe Text,
narSize :: Int64,
narHash :: Text,
references :: [Text]
}
followLinksToStorePath :: Store -> FilePath -> IO FilePath
followLinksToStorePath (Store prefix _) path = do
storePath <- canonicalizePath path
let storePath' = T.drop (T.length prefix) (toS storePath)
return $ toS $ prefix <> T.intercalate "/" (take 3 $ T.splitOn "/" storePath')
withStore :: Text -> (Store -> IO ()) -> IO ()
withStore storePrefix =
bracket open close
where
uri = "file:" <> toS storePrefix <> "/var/nix/db/db.sqlite?immutable=1"
flags = [SQLite.SQLOpenReadOnly, SQLite.SQLOpenURI]
close (Store _ db) = SQLite.close db
open = do
(_, out, _) <- readProcessWithExitCode "nix" ["show-config", "--extra-experimental-features", "nix-command"] mempty
let vfs =
if "use-sqlite-wal = false" `T.isInfixOf` toS out
then SQLite.SQLVFSUnixDotFile
else SQLite.SQLVFSDefault
conn <- SQLite.open2 uri flags vfs
return $ Store storePrefix conn
queryNarinfo :: Text
queryNarinfo = "select id, hash, deriver, narSize from ValidPaths where path = :path"
queryReferences :: Text
queryReferences = "select path from Refs join ValidPaths on reference = id where referrer = :id"
query :: Store -> Text -> [(Text, SQLData)] -> IO [[SQLite.SQLData]]
query (Store _ conn) txt bindings =
bracket (SQLite.prepare conn txt) SQLite.finalize $ \stmt -> do
SQLite.bindNamed stmt bindings
getRows stmt
getRows :: SQLite.Statement -> IO [[SQLite.SQLData]]
getRows stmt = do
SQLite.step stmt >>= \case
SQLite.Row -> do
row <- SQLite.columns stmt
rows <- getRows stmt
return $ row : rows
SQLite.Done -> do
return []
queryPathInfo :: Store -> Text -> IO (Either Text PathInfo)
queryPathInfo store path = do
rows <- query store queryNarinfo [(":path", SQLite.SQLText path)]
case rows of
[] -> return $ Left $ "no such path " <> path
[[id_, SQLite.SQLText hash_, deriver, SQLite.SQLInteger narSize]] -> do
references <- query store queryReferences [(":id", id_)]
refs <- traverse go references
return $
Right $
PathInfo
{ deriver = getDeriver deriver,
narSize = narSize,
narHash = hash_,
references = refs
}
_ -> return $ Left $ "got invalid narinfo from nix " <> show rows
where
go [SQLite.SQLText path_] = return path_
go a = throwIO $ FatalError $ "invalid reference type " <> show a
getDeriver :: SQLite.SQLData -> Maybe Text
getDeriver (SQLite.SQLText deriver) = Just deriver
getDeriver _ = Nothing
computeClosure :: Store -> [StorePath] -> IO [StorePath]
computeClosure store initialPaths = do
allPaths <-
processGraph (getPath <$> initialPaths) $ \path -> do
queryPathInfo store path >>= \case
Left _ -> do
hPutStrLn stderr $ color Yellow $ "Warning: " <> path <> " is not valid, skipping"
return []
Right pathInfo -> pure $ references pathInfo
return $ StorePath <$> Set.toList allPaths
getStorePathHash :: Store -> StorePath -> Text
getStorePathHash store storePath =
T.take 32 $ getStorePathBaseName store storePath
getPath :: StorePath -> Text
getPath (StorePath storePath) = storePath
getStorePathBaseName :: Store -> StorePath -> Text
getStorePathBaseName (Store storePrefix _) (StorePath storePath) =
dropPrefix (dropSuffix "/" storePrefix <> "/store/") storePath
where
dropPrefix :: Text -> Text -> Text
dropPrefix prefix str =
fromMaybe str (T.stripPrefix prefix str)
dropSuffix :: Text -> Text -> Text
dropSuffix suffix str =
fromMaybe str (T.stripSuffix suffix str)
base16to32 :: Text -> Either Text Text
base16to32 path =
case T.splitOn ":" path of
[_, path_] -> convert path_
[] -> convert path
_ -> Left $ "can't split : for " <> path
where
convert :: Text -> Either Text Text
convert stripped =
case convertFromBase Base16 (toS stripped :: ByteString) of
Left err -> Left $ toS err
Right decoded -> Right $ ("sha256:" <>) $ System.Nix.Base32.encode decoded