2023-10-03 13:49:18 +02:00
|
|
|
package http
|
2022-11-19 21:34:49 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/fs"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2023-10-17 22:41:33 +02:00
|
|
|
storev1pb "code.tvl.fyi/tvix/store-go"
|
2022-11-19 21:34:49 +01:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
nixhash "github.com/nix-community/go-nix/pkg/hash"
|
|
|
|
"github.com/nix-community/go-nix/pkg/nixbase32"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
)
|
|
|
|
|
2023-10-03 13:51:07 +02:00
|
|
|
// renderNarinfo writes narinfo contents to a passed io.Writer, or a returns a
|
2022-11-19 21:34:49 +01:00
|
|
|
// (wrapped) io.ErrNoExist error if something doesn't exist.
|
|
|
|
// if headOnly is set to true, only the existence is checked, but no content is
|
|
|
|
// actually written.
|
|
|
|
func renderNarinfo(
|
|
|
|
ctx context.Context,
|
|
|
|
log *log.Entry,
|
|
|
|
pathInfoServiceClient storev1pb.PathInfoServiceClient,
|
|
|
|
narHashToPathInfoMu *sync.Mutex,
|
2023-10-11 12:28:10 +02:00
|
|
|
narHashToPathInfo map[string]*narData,
|
2022-11-19 21:34:49 +01:00
|
|
|
outputHash []byte,
|
|
|
|
w io.Writer,
|
|
|
|
headOnly bool,
|
|
|
|
) error {
|
|
|
|
pathInfo, err := pathInfoServiceClient.Get(ctx, &storev1pb.GetPathInfoRequest{
|
|
|
|
ByWhat: &storev1pb.GetPathInfoRequest_ByOutputHash{
|
|
|
|
ByOutputHash: outputHash,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
st, ok := status.FromError(err)
|
|
|
|
if ok {
|
|
|
|
if st.Code() == codes.NotFound {
|
|
|
|
return fmt.Errorf("output hash %v not found: %w", base64.StdEncoding.EncodeToString(outputHash), fs.ErrNotExist)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("unable to get pathinfo, code %v: %w", st.Code(), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("unable to get pathinfo: %w", err)
|
|
|
|
}
|
|
|
|
|
2023-10-11 12:52:33 +02:00
|
|
|
log = log.WithField("pathInfo", pathInfo)
|
|
|
|
|
|
|
|
if _, err := pathInfo.Validate(); err != nil {
|
|
|
|
log.WithError(err).Error("unable to validate PathInfo")
|
|
|
|
|
|
|
|
return fmt.Errorf("unable to validate PathInfo: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if pathInfo.GetNarinfo() == nil {
|
|
|
|
log.Error("PathInfo doesn't contain Narinfo field")
|
|
|
|
|
|
|
|
return fmt.Errorf("PathInfo doesn't contain Narinfo field")
|
|
|
|
}
|
|
|
|
|
2023-10-11 19:12:10 +02:00
|
|
|
// extract the NARHash. This must succeed, as Validate() did succeed.
|
2023-10-11 12:52:33 +02:00
|
|
|
narHash, err := nixhash.FromHashTypeAndDigest(0x12, pathInfo.GetNarinfo().GetNarSha256())
|
2022-11-19 21:34:49 +01:00
|
|
|
if err != nil {
|
2023-10-11 19:12:10 +02:00
|
|
|
panic("must parse NarHash")
|
2022-11-19 21:34:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// add things to the lookup table, in case the same process didn't handle the NAR hash yet.
|
|
|
|
narHashToPathInfoMu.Lock()
|
2023-10-11 12:28:10 +02:00
|
|
|
narHashToPathInfo[narHash.SRIString()] = &narData{
|
|
|
|
rootNode: pathInfo.GetNode(),
|
|
|
|
narSize: pathInfo.GetNarinfo().GetNarSize(),
|
|
|
|
}
|
2022-11-19 21:34:49 +01:00
|
|
|
narHashToPathInfoMu.Unlock()
|
|
|
|
|
|
|
|
if headOnly {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-10-09 17:23:43 +02:00
|
|
|
// convert the PathInfo to NARInfo.
|
|
|
|
narInfo, err := ToNixNarInfo(pathInfo)
|
2022-11-19 21:34:49 +01:00
|
|
|
|
2023-10-09 17:23:43 +02:00
|
|
|
// Write it out to the client.
|
2022-11-19 21:34:49 +01:00
|
|
|
_, err = io.Copy(w, strings.NewReader(narInfo.String()))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to write narinfo to client: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func registerNarinfoGet(s *Server) {
|
2024-04-14 21:17:11 +02:00
|
|
|
// GET/HEAD $outHash.narinfo looks up the PathInfo from the tvix-store,
|
|
|
|
// and, if it's a GET request, render a .narinfo file to the client.
|
|
|
|
// In both cases it will keep the PathInfo in the lookup map,
|
|
|
|
// so a subsequent GET/HEAD /nar/ $narhash.nar request can find it.
|
|
|
|
genNarinfoHandler := func(isHead bool) func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
log := log.WithField("outputhash", chi.URLParamFromCtx(ctx, "outputhash"))
|
|
|
|
|
|
|
|
// parse the output hash sent in the request URL
|
|
|
|
outputHash, err := nixbase32.DecodeString(chi.URLParamFromCtx(ctx, "outputhash"))
|
2022-11-19 21:34:49 +01:00
|
|
|
if err != nil {
|
2024-04-14 21:17:11 +02:00
|
|
|
log.WithError(err).Error("unable to decode output hash from url")
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_, err := w.Write([]byte("unable to decode output hash from url"))
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Errorf("unable to write error message to client")
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
2022-11-19 21:34:49 +01:00
|
|
|
}
|
|
|
|
|
2024-04-14 21:17:11 +02:00
|
|
|
err = renderNarinfo(ctx, log, s.pathInfoServiceClient, &s.narDbMu, s.narDb, outputHash, w, isHead)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
} else {
|
|
|
|
log.WithError(err).Warn("unable to render narinfo")
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
}
|
2022-11-19 21:34:49 +01:00
|
|
|
}
|
|
|
|
}
|
2024-04-14 21:17:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
s.handler.Get("/{outputhash:^["+nixbase32.Alphabet+"]{32}}.narinfo", genNarinfoHandler(false))
|
|
|
|
s.handler.Head("/{outputhash:^["+nixbase32.Alphabet+"]{32}}.narinfo", genNarinfoHandler(true))
|
2022-11-19 21:34:49 +01:00
|
|
|
}
|