packages feed

nix-cache-server-0.1.0.0: src/System/Nix/Cache/Server.hs

{-# LANGUAGE DuplicateRecordFields #-}

{- | Nix binary cache server integration using nix-serve-ng

This module provides a WAI application for serving a Nix binary cache using
the <https://github.com/aristanetworks/nix-serve-ng nix-serve-ng> library.

= Usage

The typical flow is:

1. Generate or load cache signing keys using "System.Nix.Cache.Keys"
2. Call 'makeCacheApplication' with the keys to create a WAI Application
3. Mount the application in your web server (e.g., at @\/cache@)

The resulting cache server exposes standard Nix cache endpoints like:

* @\/nix-cache-info@ - Cache metadata
* @\/*.narinfo@ - NAR info files
* @\/nar\/*.nar@ - NAR archives

This integrates with nix-serve-ng which handles all the Nix-specific
protocols and signing of NAR files.
-}
module System.Nix.Cache.Server (
  -- * Cache server
  makeCacheServer,
  cacheMiddleware,
) where

import Data.ByteString qualified as ByteString
import Network.Wai (Application, Middleware, pathInfo, rawPathInfo)
import Nix qualified
import NixServeNg (ApplicationOptions (..), makeApplication)
import System.Nix.Cache.Keys (SecretKey, secretKeyByteString)

{- | Create a cache server WAI Application

This function:

1. Initializes the Nix store using the @nix@ library
2. Retrieves the Nix store directory path
3. Creates a nix-serve-ng application with the provided signing key

The returned WAI 'Application' can be mounted at any path in your web server
(commonly @\/cache@). It will handle all Nix binary cache protocol requests.

The cache is configured with a hardcoded priority of 30 (lower numbers have
higher priority in Nix's cache resolution).

Example:

@
CacheKeys {secretKey, publicKey} <- ensureCacheKeys "\/path\/to\/keys"
cacheApp <- makeCacheServer secretKey
-- Mount cacheApp at \/cache in your WAI server
-- Use publicKey for UI display
@
-}
makeCacheServer :: SecretKey -> IO Application
makeCacheServer secretKey = do
  -- Initialize Nix store
  Nix.initStore

  -- Get store directory
  storePath <- Nix.getStoreDir

  -- Create application options with hardcoded priority
  let options =
        ApplicationOptions
          { priority = 30 -- Lower is higher priority
          , storeDirectory = storePath
          , secretKey = Just (secretKeyByteString secretKey)
          }

  -- Return the application
  pure $ makeApplication options

{- | Middleware to mount cache server at a path prefix

This middleware intercepts requests starting with the given path prefix
(e.g., @\/cache\/*@) and routes them to the cache application, while allowing
requests to the prefix itself (e.g., @\/cache@) to pass through to the main
application.

This is useful when you want to serve a UI page at @\/cache@ while serving
the actual cache protocol at @\/cache\/nix-cache-info@, @\/cache\/*.narinfo@, etc.

Example:

@
cacheApp <- makeCacheServer secretKey
let middleware = cacheMiddleware "cache" cacheApp
@
-}
cacheMiddleware :: Text -> Application -> Middleware
cacheMiddleware prefix cacheApp app req respond =
  case pathInfo req of
    (p : rest@(_ : _)) | p == prefix -> do
      -- Only intercept if there's at least one path segment after prefix
      -- This allows /prefix (UI page) to pass through to servant
      -- while /prefix/nix-cache-info, /prefix/*.narinfo, etc go to cache app
      let rawPath = rawPathInfo req
          prefixBytes = "/" <> encodeUtf8 prefix
          rawPath' = fromMaybe rawPath $ ByteString.stripPrefix prefixBytes rawPath
          req' = req {pathInfo = rest, rawPathInfo = rawPath'}
      cacheApp req' respond
    _ -> app req respond