`code.tvl.fyi/tvix/store/protos` now points to a directory that only contains the `.proto` files, while all golang tooling and .pb.go files live in tvix/store-go. As discussed in https://cl.tvl.fyi/c/depot/+/9787/comment/fc5d155c_1bd38e3a/, the amount of people currently using this is still small, so rename the go.mod now, while it doesn't yet hurt. Also, use code.tvl.fyi/tvix/castore-go instead of code.tvl.fyi/tvix/ castore/protos, to make use of cl/9791. Change-Id: I9ea89957d7c29dfae4c893b9aae8ac8a0bad2d8e Reviewed-on: https://cl.tvl.fyi/c/depot/+/9792 Autosubmit: flokli <flokli@flokli.de> Tested-by: BuildkiteCI Reviewed-by: Connor Brewster <cbrewster@hey.com>
		
			
				
	
	
		
			273 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package storev1
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"path"
 | |
| 
 | |
| 	castorev1pb "code.tvl.fyi/tvix/castore-go"
 | |
| 	"github.com/nix-community/go-nix/pkg/nar"
 | |
| )
 | |
| 
 | |
| type DirectoryLookupFn func([]byte) (*castorev1pb.Directory, error)
 | |
| type BlobLookupFn func([]byte) (io.ReadCloser, error)
 | |
| 
 | |
| // Export will traverse a given root node, and write the contents in NAR format
 | |
| // to the passed Writer.
 | |
| // It uses directoryLookupFn and blobLookupFn to resolve references.
 | |
| func Export(
 | |
| 	w io.Writer,
 | |
| 	rootNode *castorev1pb.Node,
 | |
| 	directoryLookupFn DirectoryLookupFn,
 | |
| 	blobLookupFn BlobLookupFn,
 | |
| ) error {
 | |
| 	// initialize a NAR writer
 | |
| 	narWriter, err := nar.NewWriter(w)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("unable to initialize nar writer: %w", err)
 | |
| 	}
 | |
| 	defer narWriter.Close()
 | |
| 
 | |
| 	// populate rootHeader
 | |
| 	rootHeader := &nar.Header{
 | |
| 		Path: "/",
 | |
| 	}
 | |
| 
 | |
| 	// populate a stack
 | |
| 	// we will push paths and directories to it when entering a directory,
 | |
| 	// and emit individual elements to the NAR writer, draining the Directory object.
 | |
| 	// once it's empty, we can pop it off the stack.
 | |
| 	var stackPaths = []string{}
 | |
| 	var stackDirectories = []*castorev1pb.Directory{}
 | |
| 
 | |
| 	// peek at the pathInfo root and assemble the root node and write to writer
 | |
| 	// in the case of a regular file, we retrieve and write the contents, close and exit
 | |
| 	// in the case of a symlink, we write the symlink, close and exit
 | |
| 	if fileNode := rootNode.GetFile(); fileNode != nil {
 | |
| 		rootHeader.Type = nar.TypeRegular
 | |
| 		rootHeader.Size = int64(fileNode.GetSize())
 | |
| 		rootHeader.Executable = fileNode.GetExecutable()
 | |
| 		err := narWriter.WriteHeader(rootHeader)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to write root header: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// if it's a regular file, retrieve and write the contents
 | |
| 		blobReader, err := blobLookupFn(fileNode.GetDigest())
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to lookup blob: %w", err)
 | |
| 		}
 | |
| 		defer blobReader.Close()
 | |
| 
 | |
| 		_, err = io.Copy(narWriter, blobReader)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to read from blobReader: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		err = blobReader.Close()
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to close content reader: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		err = narWriter.Close()
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to close nar reader: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	} else if symlinkNode := rootNode.GetSymlink(); symlinkNode != nil {
 | |
| 		rootHeader.Type = nar.TypeSymlink
 | |
| 		rootHeader.LinkTarget = string(symlinkNode.GetTarget())
 | |
| 		err := narWriter.WriteHeader(rootHeader)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to write root header: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		err = narWriter.Close()
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to close nar reader: %w", err)
 | |
| 		}
 | |
| 	} else if directoryNode := rootNode.GetDirectory(); directoryNode != nil {
 | |
| 		// We have a directory at the root, look it up and put in on the stack.
 | |
| 		directory, err := directoryLookupFn(directoryNode.GetDigest())
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to lookup directory: %w", err)
 | |
| 		}
 | |
| 		stackDirectories = append(stackDirectories, directory)
 | |
| 		stackPaths = append(stackPaths, "/")
 | |
| 
 | |
| 		err = narWriter.WriteHeader(&nar.Header{
 | |
| 			Path: "/",
 | |
| 			Type: nar.TypeDirectory,
 | |
| 		})
 | |
| 
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("error writing header: %w", err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		panic("invalid type") // unreachable
 | |
| 	}
 | |
| 
 | |
| 	// as long as the stack is not empty, we keep running.
 | |
| 	for {
 | |
| 		if len(stackDirectories) == 0 {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		// Peek at the current top of the stack.
 | |
| 		topOfStack := stackDirectories[len(stackDirectories)-1]
 | |
| 		topOfStackPath := stackPaths[len(stackPaths)-1]
 | |
| 
 | |
| 		// get the next element that's lexicographically smallest, and drain it from
 | |
| 		// the current directory on top of the stack.
 | |
| 		nextNode := drainNextNode(topOfStack)
 | |
| 
 | |
| 		// If nextNode returns nil, there's nothing left in the directory node, so we
 | |
| 		// can emit it from the stack.
 | |
| 		// Contrary to the import case, we don't emit the node popping from the stack, but when pushing.
 | |
| 		if nextNode == nil {
 | |
| 			// pop off stack
 | |
| 			stackDirectories = stackDirectories[:len(stackDirectories)-1]
 | |
| 			stackPaths = stackPaths[:len(stackPaths)-1]
 | |
| 
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		switch n := (nextNode).(type) {
 | |
| 		case *castorev1pb.DirectoryNode:
 | |
| 			err := narWriter.WriteHeader(&nar.Header{
 | |
| 				Path: path.Join(topOfStackPath, string(n.GetName())),
 | |
| 				Type: nar.TypeDirectory,
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to write nar header: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			d, err := directoryLookupFn(n.GetDigest())
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to lookup directory: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			// add to stack
 | |
| 			stackDirectories = append(stackDirectories, d)
 | |
| 			stackPaths = append(stackPaths, path.Join(topOfStackPath, string(n.GetName())))
 | |
| 		case *castorev1pb.FileNode:
 | |
| 			err := narWriter.WriteHeader(&nar.Header{
 | |
| 				Path:       path.Join(topOfStackPath, string(n.GetName())),
 | |
| 				Type:       nar.TypeRegular,
 | |
| 				Size:       int64(n.GetSize()),
 | |
| 				Executable: n.GetExecutable(),
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to write nar header: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			// copy file contents
 | |
| 			contentReader, err := blobLookupFn(n.GetDigest())
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to get blob: %w", err)
 | |
| 			}
 | |
| 			defer contentReader.Close()
 | |
| 
 | |
| 			_, err = io.Copy(narWriter, contentReader)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to copy contents from contentReader: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			err = contentReader.Close()
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to close content reader: %w", err)
 | |
| 			}
 | |
| 		case *castorev1pb.SymlinkNode:
 | |
| 			err := narWriter.WriteHeader(&nar.Header{
 | |
| 				Path:       path.Join(topOfStackPath, string(n.GetName())),
 | |
| 				Type:       nar.TypeSymlink,
 | |
| 				LinkTarget: string(n.GetTarget()),
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("unable to write nar header: %w", err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // drainNextNode will drain a directory message with one of its child nodes,
 | |
| // whichever comes first alphabetically.
 | |
| func drainNextNode(d *castorev1pb.Directory) interface{} {
 | |
| 	switch v := (smallestNode(d)).(type) {
 | |
| 	case *castorev1pb.DirectoryNode:
 | |
| 		d.Directories = d.Directories[1:]
 | |
| 		return v
 | |
| 	case *castorev1pb.FileNode:
 | |
| 		d.Files = d.Files[1:]
 | |
| 		return v
 | |
| 	case *castorev1pb.SymlinkNode:
 | |
| 		d.Symlinks = d.Symlinks[1:]
 | |
| 		return v
 | |
| 	case nil:
 | |
| 		return nil
 | |
| 	default:
 | |
| 		panic("invalid type encountered")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // smallestNode will return the node from a directory message,
 | |
| // whichever comes first alphabetically.
 | |
| func smallestNode(d *castorev1pb.Directory) interface{} {
 | |
| 	childDirectories := d.GetDirectories()
 | |
| 	childFiles := d.GetFiles()
 | |
| 	childSymlinks := d.GetSymlinks()
 | |
| 
 | |
| 	if len(childDirectories) > 0 {
 | |
| 		if len(childFiles) > 0 {
 | |
| 			if len(childSymlinks) > 0 {
 | |
| 				// directories,files,symlinks
 | |
| 				return smallerNode(smallerNode(childDirectories[0], childFiles[0]), childSymlinks[0])
 | |
| 			} else {
 | |
| 				// directories,files,!symlinks
 | |
| 				return smallerNode(childDirectories[0], childFiles[0])
 | |
| 			}
 | |
| 		} else {
 | |
| 			// directories,!files
 | |
| 			if len(childSymlinks) > 0 {
 | |
| 				// directories,!files,symlinks
 | |
| 				return smallerNode(childDirectories[0], childSymlinks[0])
 | |
| 			} else {
 | |
| 				// directories,!files,!symlinks
 | |
| 				return childDirectories[0]
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		// !directories
 | |
| 		if len(childFiles) > 0 {
 | |
| 			// !directories,files
 | |
| 			if len(childSymlinks) > 0 {
 | |
| 				// !directories,files,symlinks
 | |
| 				return smallerNode(childFiles[0], childSymlinks[0])
 | |
| 			} else {
 | |
| 				// !directories,files,!symlinks
 | |
| 				return childFiles[0]
 | |
| 			}
 | |
| 		} else {
 | |
| 			//!directories,!files
 | |
| 			if len(childSymlinks) > 0 {
 | |
| 				//!directories,!files,symlinks
 | |
| 				return childSymlinks[0]
 | |
| 			} else {
 | |
| 				//!directories,!files,!symlinks
 | |
| 				return nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // smallerNode compares two nodes by their name,
 | |
| // and returns the one with the smaller name.
 | |
| // both nodes may not be nil, we do check for these cases in smallestNode.
 | |
| func smallerNode(a interface{ GetName() []byte }, b interface{ GetName() []byte }) interface{ GetName() []byte } {
 | |
| 	if string(a.GetName()) < string(b.GetName()) {
 | |
| 		return a
 | |
| 	} else {
 | |
| 		return b
 | |
| 	}
 | |
| }
 |