packages feed

cachix-1.4.1: src/Cachix/Client/Store.hs

{-# LANGUAGE OverloadedStrings #-}

module Cachix.Client.Store
  ( -- * Opening a store
    LocalStoreOptions (..),
    withLocalStore,
    withStore,
    Store,

    -- * Working store contents
    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')

-- | Options for `withLocalStore`.
data LocalStoreOptions = LocalStoreOptions
  { -- | The path to the Nix store directory. Typically: @"/nix"@
    storePrefix :: !Text,
    -- | Whether to use SQLite Write-Ahead Logging (WAL) mode.
    --
    -- This needs to match the ambient configuration, because otherwise, the db
    -- may be corrupted: https://github.com/cachix/cachix/issues/475
    useSqliteWAL :: !Bool
  }

-- | Run an 'IO' action while retaining a 'Store' resource for the duration of the action.
--
-- This does not rely on the @nix@ command being available.
withLocalStore :: LocalStoreOptions -> (Store -> IO a) -> IO a
withLocalStore opts =
  bracket open close
  where
    uri = toS (storePrefix opts) <> "/var/nix/db/db.sqlite"
    flags = [SQLite.SQLOpenReadOnly]
    close (Store _ db) = SQLite.close db
    vfs =
      if useSqliteWAL opts
        then SQLite.SQLVFSDefault
        else SQLite.SQLVFSUnixDotFile
    open = do
      conn <- SQLite.open2 uri flags vfs
      return $ Store (storePrefix opts) conn

-- | 'withLocalStore' but infers 'useSqliteWAL' from the @nix show-config@ command.
withStore :: Text -> (Store -> IO a) -> IO a
withStore storePrefix_ f = do
  useWAL <- do
    (_, out, _) <- readProcessWithExitCode "nix" ["show-config", "--extra-experimental-features", "nix-command"] mempty
    pure (not ("use-sqlite-wal = false" `T.isInfixOf` toS out))
  withLocalStore
    LocalStoreOptions
      { storePrefix = storePrefix_,
        useSqliteWAL = useWAL
      }
    f

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