go/nix/bcacheup: init utility for uploading things to a binary cache
This commit is contained in:
parent
a0400126fe
commit
98f53c5cd6
23 changed files with 1040 additions and 8 deletions
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
module hg.lukegb.com/lukegb/depot/go
|
module hg.lukegb.com/lukegb/depot/go
|
||||||
|
|
||||||
go 1.14
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2
|
github.com/coreos/go-systemd/v22 v22.3.2
|
||||||
|
@ -25,3 +25,8 @@ require (
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||||
|
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||||
|
)
|
||||||
|
|
|
@ -509,6 +509,8 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
|
||||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
@ -606,6 +608,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
||||||
|
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
|
455
go/nix/bcacheup/bcacheup.go
Normal file
455
go/nix/bcacheup/bcacheup.go
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
// Binary bcachegc garbage collects a Nix binary cache.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/numtide/go-nix/nixbase32"
|
||||||
|
"github.com/ulikunitz/xz"
|
||||||
|
"gocloud.dev/blob"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
"hg.lukegb.com/lukegb/depot/go/nix/nar"
|
||||||
|
"hg.lukegb.com/lukegb/depot/go/nix/nar/narinfo"
|
||||||
|
"hg.lukegb.com/lukegb/depot/go/nix/nixstore"
|
||||||
|
|
||||||
|
_ "gocloud.dev/blob/fileblob"
|
||||||
|
_ "gocloud.dev/blob/gcsblob"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
blobURLFlag = flag.String("cache_url", "", "Cache URL")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hashExtractRegexp = regexp.MustCompile(`(^|/)([0-9a-df-np-sv-z]{32})([-.].*)?$`)
|
||||||
|
|
||||||
|
trustedCaches = []string{
|
||||||
|
"https://cache.nixos.org",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func hashExtract(s string) string {
|
||||||
|
res := hashExtractRegexp.FindStringSubmatch(s)
|
||||||
|
if len(res) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return res[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
type state int
|
||||||
|
|
||||||
|
const (
|
||||||
|
stateUnknown state = iota
|
||||||
|
stateCheckingShouldUpload
|
||||||
|
stateUploadingReferences
|
||||||
|
stateUploadingContent
|
||||||
|
stateCopyingContent
|
||||||
|
stateUploadingNarinfo
|
||||||
|
stateSkipped
|
||||||
|
stateFailed
|
||||||
|
stateUploaded
|
||||||
|
stateMax
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s state) Terminal() bool {
|
||||||
|
return s == stateSkipped || s == stateUploaded || s == stateFailed
|
||||||
|
}
|
||||||
|
func (s state) String() string {
|
||||||
|
return map[state]string{
|
||||||
|
stateUnknown: "unknown",
|
||||||
|
stateCheckingShouldUpload: "determining if upload required",
|
||||||
|
stateUploadingReferences: "uploading references",
|
||||||
|
stateUploadingContent: "uploading content",
|
||||||
|
stateCopyingContent: "copying content",
|
||||||
|
stateUploadingNarinfo: "uploading narinfo",
|
||||||
|
stateSkipped: "skipped",
|
||||||
|
stateFailed: "failed",
|
||||||
|
stateUploaded: "uploaded",
|
||||||
|
}[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
type stateInfo struct {
|
||||||
|
Current state
|
||||||
|
Since time.Time
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type stateTracker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
pathState map[string]stateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *stateTracker) SetState(p string, s state) {
|
||||||
|
si := stateInfo{
|
||||||
|
Current: s,
|
||||||
|
Since: time.Now(),
|
||||||
|
Path: p,
|
||||||
|
}
|
||||||
|
t.mu.Lock()
|
||||||
|
if t.pathState == nil {
|
||||||
|
t.pathState = make(map[string]stateInfo)
|
||||||
|
}
|
||||||
|
t.pathState[p] = si
|
||||||
|
t.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *stateTracker) CurrentState() map[string]stateInfo {
|
||||||
|
out := make(map[string]stateInfo, len(t.pathState))
|
||||||
|
t.mu.Lock()
|
||||||
|
for k, v := range t.pathState {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
t.mu.Unlock()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *stateTracker) StateSummary() string {
|
||||||
|
states := t.CurrentState()
|
||||||
|
|
||||||
|
countByState := map[state]int{}
|
||||||
|
var oldestActive []stateInfo
|
||||||
|
for _, s := range states {
|
||||||
|
countByState[s.Current]++
|
||||||
|
if !s.Current.Terminal() && s.Current != stateUploadingReferences {
|
||||||
|
oldestActive = append(oldestActive, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(oldestActive, func(i, j int) bool {
|
||||||
|
a, b := oldestActive[i], oldestActive[j]
|
||||||
|
return a.Since.Before(b.Since)
|
||||||
|
})
|
||||||
|
|
||||||
|
var firstLineBits []string
|
||||||
|
for n := stateUnknown; n < stateMax; n++ {
|
||||||
|
c := countByState[n]
|
||||||
|
if c != 0 {
|
||||||
|
firstLineBits = append(firstLineBits, fmt.Sprintf("%d %s", c, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := []string{
|
||||||
|
strings.Join(firstLineBits, ", "),
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := 0; n < len(oldestActive) && n < 20; n++ {
|
||||||
|
si := oldestActive[n]
|
||||||
|
lines = append(lines, fmt.Sprintf("\t%s: %s (for %s)", si.Path, si.Current, time.Since(si.Since).Truncate(time.Second)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
type uploader struct {
|
||||||
|
bucket *blob.Bucket
|
||||||
|
store *nixstore.DB
|
||||||
|
storePath string
|
||||||
|
st stateTracker
|
||||||
|
|
||||||
|
uploadSF singleflight.Group
|
||||||
|
}
|
||||||
|
|
||||||
|
type byteCounterWriter struct{ n uint64 }
|
||||||
|
|
||||||
|
func (w *byteCounterWriter) Write(b []byte) (int, error) {
|
||||||
|
w.n += uint64(len(b))
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) inStore(ctx context.Context, path string) (bool, error) {
|
||||||
|
// Check if the narinfo exists.
|
||||||
|
key, err := keyForPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("computing narinfo key for %v: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.bucket.Exists(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) inTrustedCaches(ctx context.Context, path string) (bool, error) {
|
||||||
|
key, err := keyForPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("computing narinfo key for %v: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range trustedCaches {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "HEAD", fmt.Sprintf("%v/%v", c, key), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("constructing request for %v/%v: %v", c, key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("making request for %v/%v: %v", c, key, err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) shouldUpload(ctx context.Context, path string) (bool, error) {
|
||||||
|
inStore, err := u.inStore(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if inStore {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inTrustedCaches, err := u.inTrustedCaches(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if inTrustedCaches {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) uploadContent(ctx context.Context, ni *narinfo.NarInfo, path string, dst io.Writer) error {
|
||||||
|
if !ni.NarHash.Valid() {
|
||||||
|
return fmt.Errorf("nar hash for %v is not valid", path)
|
||||||
|
}
|
||||||
|
narHasher := ni.NarHash.Algorithm.Hash()
|
||||||
|
fileHasher := ni.NarHash.Algorithm.Hash()
|
||||||
|
|
||||||
|
fileByteCounter := &byteCounterWriter{}
|
||||||
|
|
||||||
|
xzWriter, err := xz.NewWriter(io.MultiWriter(fileHasher, fileByteCounter, dst))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating xz writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := io.MultiWriter(narHasher, xzWriter)
|
||||||
|
narSize, err := nar.Pack(w, nar.DirFS(u.storePath), filepath.Base(path))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("packing %v as NAR: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := xzWriter.Close(); err != nil {
|
||||||
|
return fmt.Errorf("compressing with xz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the NAR hash is correct.
|
||||||
|
if uint64(narSize) != ni.NarSize {
|
||||||
|
return fmt.Errorf("uploaded nar was %d bytes; expected %d bytes", narSize, ni.NarSize)
|
||||||
|
}
|
||||||
|
narHash := narinfo.Hash{
|
||||||
|
Hash: narHasher.Sum(nil),
|
||||||
|
Algorithm: ni.NarHash.Algorithm,
|
||||||
|
}
|
||||||
|
if len(narHash.Hash) != len(ni.NarHash.Hash) {
|
||||||
|
return fmt.Errorf("uploaded nar hash length was %d bytes; expected %d bytes", len(narHash.Hash), len(ni.NarHash.Hash))
|
||||||
|
}
|
||||||
|
if got, want := narHash.String(), ni.NarHash.String(); got != want {
|
||||||
|
return fmt.Errorf("uploaded nar hash was %v; wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
ni.Compression = narinfo.CompressionXz
|
||||||
|
ni.FileHash = narinfo.Hash{
|
||||||
|
Hash: fileHasher.Sum(nil),
|
||||||
|
Algorithm: ni.NarHash.Algorithm,
|
||||||
|
}
|
||||||
|
ni.FileSize = fileByteCounter.n
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyForPath(storePath string) (string, error) {
|
||||||
|
fileHash := hashExtract(storePath)
|
||||||
|
if fileHash == "" {
|
||||||
|
return "", fmt.Errorf("store path %v seems to be invalid: couldn't extract hash", storePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s.narinfo", fileHash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) uploadNARInfo(ctx context.Context, ni *narinfo.NarInfo) error {
|
||||||
|
key, err := keyForPath(ni.StorePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.bucket.WriteAll(ctx, key, []byte(ni.String()), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) uploadRefs(ctx context.Context, current string, refs []string) error {
|
||||||
|
if len(refs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eg, egctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for _, ref := range refs {
|
||||||
|
refPath := filepath.Join(u.storePath, ref)
|
||||||
|
if current == refPath {
|
||||||
|
// We depend on ourselves, which is fine.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eg.Go(func() error {
|
||||||
|
return u.Upload(egctx, refPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) upload(ctx context.Context, path string) error {
|
||||||
|
u.st.SetState(path, stateCheckingShouldUpload)
|
||||||
|
|
||||||
|
if ok, err := u.shouldUpload(ctx, path); err != nil {
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("determining if we should upload %v: %w", path, err)
|
||||||
|
} else if !ok {
|
||||||
|
u.st.SetState(path, stateSkipped)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Uploading %v", path)
|
||||||
|
|
||||||
|
ni, err := u.store.NARInfo(path)
|
||||||
|
if err != nil {
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("getting narinfo for %v: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.st.SetState(path, stateUploadingReferences)
|
||||||
|
if err := u.uploadRefs(ctx, ni.StorePath, ni.References); err != nil {
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("uploading references for %v: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.st.SetState(path, stateUploadingContent)
|
||||||
|
if !ni.NarHash.Valid() {
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("nar hash is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpPath := fmt.Sprintf("tmp-uploading/%s", filepath.Base(path))
|
||||||
|
dst, err := u.bucket.NewWriter(ctx, tmpPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("creating new writer for upload of %v: %w", path, err)
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
if err := u.uploadContent(ctx, ni, path, dst); err != nil {
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
if err := dst.Close(); err == nil {
|
||||||
|
u.bucket.Delete(ctx, tmpPath)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dst.Close(); err != nil {
|
||||||
|
u.bucket.Delete(ctx, tmpPath)
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("completing tmp write of %v: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to the "correct" place.
|
||||||
|
u.st.SetState(path, stateCopyingContent)
|
||||||
|
finalDstKey := fmt.Sprintf("nar/%s.nar.xz", nixbase32.EncodeToString(ni.FileHash.Hash))
|
||||||
|
if err := u.bucket.Copy(ctx, finalDstKey, tmpPath, nil); err != nil {
|
||||||
|
u.bucket.Delete(ctx, tmpPath)
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("copying tmp write of %v from %v to %v: %w", path, tmpPath, finalDstKey, err)
|
||||||
|
}
|
||||||
|
if err := u.bucket.Delete(ctx, tmpPath); err != nil {
|
||||||
|
u.bucket.Delete(ctx, finalDstKey)
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("cleaning up tmp write of %v at %v: %w", path, tmpPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ni.URL = finalDstKey
|
||||||
|
|
||||||
|
u.st.SetState(path, stateUploadingNarinfo)
|
||||||
|
if err := u.uploadNARInfo(ctx, ni); err != nil {
|
||||||
|
u.bucket.Delete(ctx, finalDstKey)
|
||||||
|
u.st.SetState(path, stateFailed)
|
||||||
|
return fmt.Errorf("uploading narinfo for %v: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.st.SetState(path, stateUploaded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uploader) Upload(ctx context.Context, path string) error {
|
||||||
|
resCh := u.uploadSF.DoChan(path, func() (any, error) {
|
||||||
|
err := u.upload(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Uploading %v: %v", path, err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case res := <-resCh:
|
||||||
|
return res.Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
log.Printf("Using cache URL %q", *blobURLFlag)
|
||||||
|
bucket, err := blob.OpenBucket(ctx, *blobURLFlag)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("opening bucket %q: %v", *blobURLFlag, err)
|
||||||
|
}
|
||||||
|
defer bucket.Close()
|
||||||
|
|
||||||
|
store, err := nixstore.Open(nixstore.DefaultStoreDB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("opening Nix store: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
u := &uploader{
|
||||||
|
bucket: bucket,
|
||||||
|
store: store,
|
||||||
|
storePath: "/nix/store",
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(1 * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
log.Print(u.st.StateSummary())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, p := range flag.Args() {
|
||||||
|
realPath, err := os.Readlink(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Readlink(%q): %v", p, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.Upload(ctx, realPath); err != nil {
|
||||||
|
log.Fatalf("upload(%q): %v", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
go/nix/bcacheup/default.nix
Normal file
22
go/nix/bcacheup/default.nix
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
{ depot, ... }:
|
||||||
|
depot.third_party.buildGo.program {
|
||||||
|
name = "bcacheup";
|
||||||
|
srcs = [
|
||||||
|
./bcacheup.go
|
||||||
|
];
|
||||||
|
deps = with depot; [
|
||||||
|
third_party.gopkgs."gocloud.dev".blob
|
||||||
|
third_party.gopkgs."gocloud.dev".blob.fileblob
|
||||||
|
third_party.gopkgs."gocloud.dev".blob.gcsblob
|
||||||
|
third_party.gopkgs."golang.org".x.sync.errgroup
|
||||||
|
third_party.gopkgs."golang.org".x.sync.singleflight
|
||||||
|
third_party.gopkgs."github.com".ulikunitz.xz
|
||||||
|
go.nix.nar
|
||||||
|
go.nix.nar.narinfo
|
||||||
|
go.nix.nixstore
|
||||||
|
];
|
||||||
|
}
|
|
@ -5,5 +5,7 @@
|
||||||
args:
|
args:
|
||||||
{
|
{
|
||||||
nar = import ./nar args;
|
nar = import ./nar args;
|
||||||
|
nixstore = import ./nixstore args;
|
||||||
bcachegc = import ./bcachegc args;
|
bcachegc = import ./bcachegc args;
|
||||||
|
bcacheup = import ./bcacheup args;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,14 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
args:
|
{ depot, ... }@args:
|
||||||
{
|
(depot.third_party.buildGo.package {
|
||||||
|
name = "nar";
|
||||||
|
path = "hg.lukegb.com/lukegb/depot/go/nix/nar";
|
||||||
|
srcs = [
|
||||||
|
./nar.go
|
||||||
|
./dirfs.go
|
||||||
|
];
|
||||||
|
}) // {
|
||||||
narinfo = import ./narinfo args;
|
narinfo = import ./narinfo args;
|
||||||
}
|
}
|
||||||
|
|
56
go/nix/nar/dirfs.go
Normal file
56
go/nix/nar/dirfs.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package nar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DirFS string
|
||||||
|
|
||||||
|
func (dir DirFS) Open(name string) (fs.File, error) {
|
||||||
|
if !fs.ValidPath(name) {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
||||||
|
}
|
||||||
|
f, err := os.Open(dir.Join(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir DirFS) Stat(name string) (fs.FileInfo, error) {
|
||||||
|
if !fs.ValidPath(name) {
|
||||||
|
return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
|
||||||
|
}
|
||||||
|
f, err := os.Stat(dir.Join(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir DirFS) Lstat(name string) (fs.FileInfo, error) {
|
||||||
|
if !fs.ValidPath(name) {
|
||||||
|
return nil, &fs.PathError{Op: "lstat", Path: name, Err: fs.ErrInvalid}
|
||||||
|
}
|
||||||
|
f, err := os.Lstat(dir.Join(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir DirFS) Readlink(name string) (string, error) {
|
||||||
|
if !fs.ValidPath(name) {
|
||||||
|
return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid}
|
||||||
|
}
|
||||||
|
f, err := os.Readlink(dir.Join(name))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir DirFS) Join(name string) string {
|
||||||
|
return string(dir) + string(os.PathSeparator) + name
|
||||||
|
}
|
196
go/nix/nar/nar.go
Normal file
196
go/nix/nar/nar.go
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
package nar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serializeWriter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w serializeWriter) WritePadding(n int64) (int64, error) {
|
||||||
|
if n%8 > 0 {
|
||||||
|
n, err := w.Write(make([]byte, 8-(n%8)))
|
||||||
|
return int64(n), err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w serializeWriter) WriteUint64(n uint64) (int64, error) {
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(buf, n)
|
||||||
|
wrote, err := w.Write(buf)
|
||||||
|
return int64(wrote), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w serializeWriter) WriteString(s string) (int64, error) {
|
||||||
|
nSize, err := w.WriteUint64(uint64(len(s)))
|
||||||
|
if err != nil {
|
||||||
|
return int64(nSize), err
|
||||||
|
}
|
||||||
|
|
||||||
|
nData, err := w.Write([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
return int64(nSize) + int64(nData), err
|
||||||
|
}
|
||||||
|
|
||||||
|
nPad, err := w.WritePadding(int64(len(s)))
|
||||||
|
return int64(nSize) + int64(nData) + int64(nPad), err
|
||||||
|
}
|
||||||
|
|
||||||
|
type FS interface {
|
||||||
|
Open(string) (fs.File, error)
|
||||||
|
Stat(string) (fs.FileInfo, error)
|
||||||
|
Lstat(string) (fs.FileInfo, error)
|
||||||
|
Readlink(string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func packFile(sw serializeWriter, root FS, fn string, stat fs.FileInfo) (int64, error) {
|
||||||
|
|
||||||
|
var nSoFar int64
|
||||||
|
write := func(data ...any) (int64, error) {
|
||||||
|
for _, datum := range data {
|
||||||
|
var n int64
|
||||||
|
var err error
|
||||||
|
switch datum := datum.(type) {
|
||||||
|
case string:
|
||||||
|
n, err = sw.WriteString(datum)
|
||||||
|
case uint64:
|
||||||
|
n, err = sw.WriteUint64(datum)
|
||||||
|
default:
|
||||||
|
return nSoFar, fmt.Errorf("unknown data type %T (%s)", datum, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar + n, err
|
||||||
|
}
|
||||||
|
nSoFar += n
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := write("("); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case stat.Mode()&fs.ModeDir != 0:
|
||||||
|
// Directory.
|
||||||
|
if n, err := write("type", "directory"); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := root.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
dirF, ok := f.(fs.ReadDirFile)
|
||||||
|
if !ok {
|
||||||
|
return nSoFar, fmt.Errorf("%v didn't get me a ReadDirFile", fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
dents, err := dirF.ReadDir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar, fmt.Errorf("reading dents from %v: %w", fn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(dents, func(i, j int) bool {
|
||||||
|
return dents[i].Name() < dents[j].Name()
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, dent := range dents {
|
||||||
|
if n, err := write("entry", "(", "name", dent.Name(), "node"); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dentStat, err := dent.Info()
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar, fmt.Errorf("stat for %v: %w", path.Join(fn, dent.Name()), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := packFile(sw, root, path.Join(fn, dent.Name()), dentStat)
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar + n, err
|
||||||
|
}
|
||||||
|
nSoFar += n
|
||||||
|
|
||||||
|
if n, err := write(")"); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case stat.Mode()&fs.ModeSymlink != 0:
|
||||||
|
// Symlink.
|
||||||
|
target, err := root.Readlink(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := write("type", "symlink", "target", target); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
case stat.Mode().Type() != 0:
|
||||||
|
return 0, fmt.Errorf("not implemented (other: %s)", stat.Mode())
|
||||||
|
default:
|
||||||
|
// Regular file.
|
||||||
|
if n, err := write("type", "regular"); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if stat.Mode()&0o100 != 0 {
|
||||||
|
// Executable.
|
||||||
|
if n, err := write("executable", ""); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := write("contents", uint64(stat.Size())); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := root.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
wrote, err := io.Copy(sw, f)
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar + wrote, err
|
||||||
|
}
|
||||||
|
nSoFar += wrote
|
||||||
|
|
||||||
|
n, err := sw.WritePadding(wrote)
|
||||||
|
if err != nil {
|
||||||
|
return nSoFar + n, err
|
||||||
|
}
|
||||||
|
nSoFar += n
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := write(")"); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nSoFar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pack(w io.Writer, fs FS, fn string) (int64, error) {
|
||||||
|
sw := serializeWriter{w}
|
||||||
|
|
||||||
|
n, err := sw.WriteString("nix-archive-1")
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := fs.Lstat(fn)
|
||||||
|
if err != nil {
|
||||||
|
return n, fmt.Errorf("lstat(%q): %w", fn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
npf, err := packFile(sw, fs, fn, stat)
|
||||||
|
return npf + n, err
|
||||||
|
}
|
79
go/nix/nar/nar_test.go
Normal file
79
go/nix/nar/nar_test.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package nar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeader(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
sw := serializeWriter{&b}
|
||||||
|
wrote, err := sw.WriteString("nix-archive-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteString: %v", err)
|
||||||
|
}
|
||||||
|
if want := int64(0x18); wrote != want {
|
||||||
|
t.Errorf("wrote = 0x%x; want 0x%x", wrote, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantBuf := append(append([]byte{
|
||||||
|
// Length
|
||||||
|
0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
}, []byte("nix-archive-1")...), []byte{
|
||||||
|
// Padding
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
}...)
|
||||||
|
if diff := cmp.Diff(b.Bytes(), wantBuf); diff != "" {
|
||||||
|
t.Errorf("b.Bytes() diff (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\n%s", hex.Dump(b.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPack(t *testing.T) {
|
||||||
|
fs := DirFS("testdata")
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
f string
|
||||||
|
golden string
|
||||||
|
}{{
|
||||||
|
f: "f.txt",
|
||||||
|
golden: "f.txt.nar",
|
||||||
|
}, {
|
||||||
|
f: "dir",
|
||||||
|
golden: "dir.nar",
|
||||||
|
}, {
|
||||||
|
f: "dirWithBadSymlink",
|
||||||
|
golden: "dirWithBadSymlink.nar",
|
||||||
|
}} {
|
||||||
|
t.Run(tc.f, func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
wrote, err := Pack(&b, fs, tc.f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pack: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
golden, err := fs.Open(tc.golden)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("opening golden %v: %v", tc.golden, err)
|
||||||
|
}
|
||||||
|
defer golden.Close()
|
||||||
|
want, err := io.ReadAll(golden)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reading golden %v: %v", tc.golden, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wrote != int64(len(b.Bytes())) {
|
||||||
|
t.Errorf("wrote (%d) != len(b.Bytes()) (%d)", wrote, int64(len(b.Bytes())))
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(b.Bytes(), want); diff != "" {
|
||||||
|
t.Errorf("b.Bytes() diff (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,7 +94,7 @@ func (a HashAlgorithm) String() string {
|
||||||
return "!!unknown!!"
|
return "!!unknown!!"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a HashAlgorithm) hash() hash.Hash {
|
func (a HashAlgorithm) Hash() hash.Hash {
|
||||||
switch a {
|
switch a {
|
||||||
case HashMd5:
|
case HashMd5:
|
||||||
return md5.New()
|
return md5.New()
|
||||||
|
@ -109,7 +109,7 @@ func (a HashAlgorithm) hash() hash.Hash {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a HashAlgorithm) sizes() sizes {
|
func (a HashAlgorithm) sizes() sizes {
|
||||||
sz := a.hash().Size()
|
sz := a.Hash().Size()
|
||||||
return sizes{
|
return sizes{
|
||||||
rawLen: sz,
|
rawLen: sz,
|
||||||
base16Len: hex.EncodedLen(sz),
|
base16Len: hex.EncodedLen(sz),
|
||||||
|
@ -144,7 +144,7 @@ func (h Hash) String() string {
|
||||||
return fmt.Sprintf("%s:%s", h.Algorithm, nixbase32.EncodeToString(h.Hash))
|
return fmt.Sprintf("%s:%s", h.Algorithm, nixbase32.EncodeToString(h.Hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashFromString(s string) (Hash, error) {
|
func HashFromString(s string) (Hash, error) {
|
||||||
var h Hash
|
var h Hash
|
||||||
idx := strings.IndexAny(s, "-:")
|
idx := strings.IndexAny(s, "-:")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
|
@ -316,7 +316,7 @@ func ParseNarInfo(r io.Reader) (*NarInfo, error) {
|
||||||
return nil, fmt.Errorf("unknown compression method %q", value)
|
return nil, fmt.Errorf("unknown compression method %q", value)
|
||||||
}
|
}
|
||||||
case "FileHash":
|
case "FileHash":
|
||||||
h, err := hashFromString(value)
|
h, err := HashFromString(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing %q as FileHash: %w", value, err)
|
return nil, fmt.Errorf("parsing %q as FileHash: %w", value, err)
|
||||||
}
|
}
|
||||||
|
@ -328,7 +328,7 @@ func ParseNarInfo(r io.Reader) (*NarInfo, error) {
|
||||||
return nil, fmt.Errorf("parsing %q as FileSize: %w", value, err)
|
return nil, fmt.Errorf("parsing %q as FileSize: %w", value, err)
|
||||||
}
|
}
|
||||||
case "NarHash":
|
case "NarHash":
|
||||||
h, err := hashFromString(value)
|
h, err := HashFromString(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing %q as NarHash: %w", value, err)
|
return nil, fmt.Errorf("parsing %q as NarHash: %w", value, err)
|
||||||
}
|
}
|
||||||
|
|
BIN
go/nix/nar/testdata/dir.nar
vendored
Normal file
BIN
go/nix/nar/testdata/dir.nar
vendored
Normal file
Binary file not shown.
1
go/nix/nar/testdata/dir/f.txt
vendored
Normal file
1
go/nix/nar/testdata/dir/f.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
hello
|
1
go/nix/nar/testdata/dir/f2.txt
vendored
Normal file
1
go/nix/nar/testdata/dir/f2.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
f2
|
1
go/nix/nar/testdata/dir/symlink
vendored
Symbolic link
1
go/nix/nar/testdata/dir/symlink
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
f.txt
|
BIN
go/nix/nar/testdata/dirWithBadSymlink.nar
vendored
Normal file
BIN
go/nix/nar/testdata/dirWithBadSymlink.nar
vendored
Normal file
Binary file not shown.
1
go/nix/nar/testdata/dirWithBadSymlink/symlink
vendored
Symbolic link
1
go/nix/nar/testdata/dirWithBadSymlink/symlink
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/dir
|
1
go/nix/nar/testdata/f.txt
vendored
Normal file
1
go/nix/nar/testdata/f.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
hello
|
BIN
go/nix/nar/testdata/f.txt.nar
vendored
Normal file
BIN
go/nix/nar/testdata/f.txt.nar
vendored
Normal file
Binary file not shown.
16
go/nix/nixstore/default.nix
Normal file
16
go/nix/nixstore/default.nix
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
{ depot, ... }:
|
||||||
|
depot.third_party.buildGo.package {
|
||||||
|
name = "nixstore";
|
||||||
|
path = "hg.lukegb.com/lukegb/depot/go/nix/nixstore";
|
||||||
|
srcs = [
|
||||||
|
./nixstore.go
|
||||||
|
];
|
||||||
|
deps = with depot; [
|
||||||
|
go.nix.nar.narinfo
|
||||||
|
third_party.gopkgs."github.com".mattn.go-sqlite3
|
||||||
|
];
|
||||||
|
}
|
130
go/nix/nixstore/nixstore.go
Normal file
130
go/nix/nixstore/nixstore.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package nixstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hg.lukegb.com/lukegb/depot/go/nix/nar/narinfo"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultStoreDB = "/nix/var/nix/db/db.sqlite"
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) NARInfo(storePath string) (*narinfo.NarInfo, error) {
|
||||||
|
stmt, err := d.db.Prepare(`
|
||||||
|
SELECT
|
||||||
|
vp.id,
|
||||||
|
vp.path,
|
||||||
|
vp.hash,
|
||||||
|
vp.deriver,
|
||||||
|
vp.narSize,
|
||||||
|
vp.sigs,
|
||||||
|
1 FROM
|
||||||
|
ValidPaths vp
|
||||||
|
WHERE 1=1
|
||||||
|
AND vp.path = ?
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("preparing initial statement: %w", err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
ni := narinfo.NarInfo{}
|
||||||
|
|
||||||
|
var storePathID int
|
||||||
|
var dummy int
|
||||||
|
var hashStr string
|
||||||
|
var deriverStr *string
|
||||||
|
var sigsStr *string
|
||||||
|
err = stmt.QueryRow(storePath).Scan(
|
||||||
|
&storePathID,
|
||||||
|
&ni.StorePath,
|
||||||
|
&hashStr,
|
||||||
|
&deriverStr,
|
||||||
|
&ni.NarSize,
|
||||||
|
&sigsStr,
|
||||||
|
&dummy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning initial statement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ni.NarHash, err = narinfo.HashFromString(hashStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing hash %q: %w", hashStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deriverStr != nil {
|
||||||
|
ni.Deriver = path.Base(*deriverStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sigsStr != nil {
|
||||||
|
sigsBits := strings.Fields(*sigsStr)
|
||||||
|
sigs := make(map[string][]byte)
|
||||||
|
for _, sigsBit := range sigsBits {
|
||||||
|
sigsPieces := strings.Split(sigsBit, ":")
|
||||||
|
if len(sigsPieces) != 2 {
|
||||||
|
return nil, fmt.Errorf("parsing signature %q: wrong number of : separated pieces (%d)", sigsBit, len(sigsPieces))
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
sigs[sigsPieces[0]], err = base64.StdEncoding.DecodeString(sigsPieces[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing signature %q: invalid base64: %w", sigsBit, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ni.Sig = sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
referencesStmt, err := d.db.Prepare(`
|
||||||
|
SELECT
|
||||||
|
refedvp.path
|
||||||
|
FROM
|
||||||
|
Refs r
|
||||||
|
INNER JOIN
|
||||||
|
ValidPaths refedvp ON refedvp.id = r.reference
|
||||||
|
WHERE
|
||||||
|
r.referrer = ?
|
||||||
|
ORDER BY 1
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("preparing references statement: %w", err)
|
||||||
|
}
|
||||||
|
defer referencesStmt.Close()
|
||||||
|
|
||||||
|
referencesRows, err := referencesStmt.Query(storePathID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("querying references: %w", err)
|
||||||
|
}
|
||||||
|
defer referencesRows.Close()
|
||||||
|
|
||||||
|
for referencesRows.Next() {
|
||||||
|
var refStorePath string
|
||||||
|
if err := referencesRows.Scan(&refStorePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning references: %w", err)
|
||||||
|
}
|
||||||
|
ni.References = append(ni.References, path.Base(refStorePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ni, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(dbPath string) (*DB, error) {
|
||||||
|
sqlDB, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &DB{
|
||||||
|
db: sqlDB,
|
||||||
|
}, nil
|
||||||
|
}
|
18
go/nix/nixstore/nixstore_test.go
Normal file
18
go/nix/nixstore/nixstore_test.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package nixstore
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNARInfo(t *testing.T) {
|
||||||
|
db, err := Open(DefaultStoreDB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ni, err := db.NARInfo("/nix/store/yk8ps7v1jhwpj82pigmqjb68ln7bgjbn-acl-2.3.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NARInfo: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("%#v", ni)
|
||||||
|
t.Log(ni.String())
|
||||||
|
t.Error("meep")
|
||||||
|
}
|
23
third_party/gopkgs/github.com/mattn/go-sqlite3/default.nix
vendored
Normal file
23
third_party/gopkgs/github.com/mattn/go-sqlite3/default.nix
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
{ depot, pkgs, ... }:
|
||||||
|
depot.third_party.buildGo.external {
|
||||||
|
path = "github.com/mattn/go-sqlite3";
|
||||||
|
src = depot.third_party.nixpkgs.fetchFromGitHub {
|
||||||
|
owner = "mattn";
|
||||||
|
repo = "go-sqlite3";
|
||||||
|
rev = "v1.14.15";
|
||||||
|
hash = "sha256:0rkila12zj2q3bzyljg3l6jya9n1i0z625pjblzgmm0xyrkk1i1v";
|
||||||
|
};
|
||||||
|
tags = [ "linux" "libsqlite3" ];
|
||||||
|
cgo = true;
|
||||||
|
cgodeps = with pkgs; [
|
||||||
|
sqlite
|
||||||
|
];
|
||||||
|
|
||||||
|
deps = with depot.third_party; [
|
||||||
|
#gopkgs."github.com".mattn.go-isatty
|
||||||
|
];
|
||||||
|
}
|
14
third_party/gopkgs/github.com/ulikunitz/xz/default.nix
vendored
Normal file
14
third_party/gopkgs/github.com/ulikunitz/xz/default.nix
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
{ depot, ... }:
|
||||||
|
depot.third_party.buildGo.external {
|
||||||
|
path = "github.com/ulikunitz/xz";
|
||||||
|
src = depot.third_party.nixpkgs.fetchFromGitHub {
|
||||||
|
owner = "ulikunitz";
|
||||||
|
repo = "xz";
|
||||||
|
rev = "v0.5.10";
|
||||||
|
hash = "sha256:07vynk0sh8i8g7x9p9x04dj8wylvxaf8ypbi43yvcv7j6zd63c72";
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue