git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / gitrepo / materialize.go /
internal/gitrepo/materialize.go · 3.4 KiB · raw
view: Source ☐ force
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
// Package gitrepo bridges upspin-stored bare git repositories to go-git. A bare
// repo (HEAD, config, objects/, refs/) is copied out of upspin into a billy
// filesystem and opened with go-git — for read-only browsing and clone, and
// (via a private copy) for push write-back.
package gitrepo

import (
	"crypto/sha256"
	"errors"
	"fmt"
	"path"
	"strings"

	"github.com/go-git/go-billy/v5"

	"gitweb/internal/store"
)

// FileInfo records a materialized file's size and content hash, used by the
// push write-back to detect which files changed.
type FileInfo struct {
	Size int64
	Sum  [32]byte
}

// Manifest maps a repo-relative file path (as stored in the billy filesystem,
// using forward slashes and no leading slash) to its FileInfo.
type Manifest map[string]FileInfo

// defaultMaxRepoBytes caps the total size materialized per repo. It bounds a
// single request's footprint; very large repos are rejected rather than
// exhausting the cache filesystem.
const defaultMaxRepoBytes = 256 << 20 // 256 MiB

// ErrRepoTooLarge is returned (wrapped) by materialize when a repo exceeds the
// materialization size cap.
var ErrRepoTooLarge = errors.New("repository too large")

// materialize copies the bare-repo subtree rooted at repoPath out of the store
// into fs, with the repo's internal files (HEAD, config, objects/, refs/) at
// the filesystem root — the layout go-git's filesystem storage expects. fs
// must be empty; total copied bytes are capped at maxBytes. It mirrors the
// recursive Glob/recurse/copy walk of upspin's `upspin tar` command
// (cmd/upspin/tar.go doArchive), but goes through the store.Store interface so
// it also works against LocalStore in dev.
func materialize(st store.Store, repoPath string, fs billy.Filesystem, maxBytes int64) (Manifest, error) {
	man := Manifest{}
	var total int64
	if err := copyTree(st, strings.TrimRight(repoPath, "/"), "", fs, man, &total, maxBytes); err != nil {
		return nil, err
	}
	// Sanity-check the result really is a bare repo at the root.
	if _, err := fs.Stat("HEAD"); err != nil {
		return nil, fmt.Errorf("materialize %s: no HEAD at repo root: %w", repoPath, err)
	}
	if fi, err := fs.Stat("objects"); err != nil || !fi.IsDir() {
		return nil, fmt.Errorf("materialize %s: no objects/ dir at repo root", repoPath)
	}
	return man, nil
}

// copyTree recursively copies upspin path `src` into billy path `dst`, recording
// each copied file into man.
func copyTree(st store.Store, src, dst string, fs billy.Filesystem, man Manifest, total *int64, maxBytes int64) error {
	entries, err := st.List(src)
	if err != nil {
		return err
	}
	for _, e := range entries {
		srcChild := src + "/" + e.Name
		dstChild := e.Name
		if dst != "" {
			dstChild = path.Join(dst, e.Name)
		}
		if e.IsDir {
			if err := fs.MkdirAll(dstChild, 0o755); err != nil {
				return err
			}
			if err := copyTree(st, srcChild, dstChild, fs, man, total, maxBytes); err != nil {
				return err
			}
			continue
		}
		data, err := st.Get(srcChild)
		if err != nil {
			return err
		}
		*total += int64(len(data))
		if *total > maxBytes {
			return fmt.Errorf("%w (limit %d bytes)", ErrRepoTooLarge, maxBytes)
		}
		f, err := fs.Create(dstChild)
		if err != nil {
			return err
		}
		if _, err := f.Write(data); err != nil {
			f.Close()
			return err
		}
		if err := f.Close(); err != nil {
			return err
		}
		man[dstChild] = FileInfo{Size: int64(len(data)), Sum: sha256.Sum256(data)}
	}
	return nil
}