aboutsummaryrefslogtreecommitdiff
path: root/object/store/packed/quarantine.go
blob: 977a95432e05ae4caf6a266cc2a7c7169ef614e4 (about) (plain) (blame)
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package packed

import (
	"crypto/rand"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"slices"
	"strings"

	"lindenii.org/go/furgit/object/store"
)

var (
	_ store.PackQuarantiner = (*Packed)(nil)
	_ store.PackQuarantine  = (*packQuarantine)(nil)
)

var errQuarantineNamesExhausted = errors.New("object/store/packed: exhausted quarantine directory names")

// packQuarantine is one quarantined packed store
// rooted privately beneath a destination pack root.
type packQuarantine struct {
	*Packed

	parent *Packed

	tempName string
	tempRoot *os.Root
}

// BeginPackQuarantine creates one quarantined packed store
// rooted privately beneath the destination pack root.
//
// Labels: Deps-Borrowed, Life-Parent.
func (packed *Packed) BeginPackQuarantine(_ store.PackQuarantineOptions) (store.PackQuarantine, error) { //nolint:ireturn
	tempName, tempRoot, err := createPackQuarantineRoot(packed.root)
	if err != nil {
		return nil, err
	}

	quarantineStore, err := New(tempRoot, packed.objectFormat)
	if err != nil {
		_ = tempRoot.Close()
		_ = packed.root.RemoveAll(tempName)

		return nil, err
	}

	return &packQuarantine{
		Packed:   quarantineStore,
		parent:   packed,
		tempName: tempName,
		tempRoot: tempRoot,
	}, nil
}

// Promote publishes the quarantined pack artifacts into the parent store,
// refreshes the parent so the objects become available,
// and invalidates the receiver.
func (quarantine *packQuarantine) Promote() error {
	closeErr := quarantine.Close()
	promoteErr := quarantine.promoteAll()

	var refreshErr error
	if promoteErr == nil {
		refreshErr = quarantine.parent.Refresh()
	}

	tempRootErr := quarantine.tempRoot.Close()
	removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)

	return errors.Join(closeErr, promoteErr, refreshErr, tempRootErr, removeErr)
}

// Discard removes the quarantine and invalidates the receiver.
func (quarantine *packQuarantine) Discard() error {
	closeErr := quarantine.Close()
	tempRootErr := quarantine.tempRoot.Close()
	removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)

	return errors.Join(closeErr, tempRootErr, removeErr)
}

// promoteAll links every pack artifact in the quarantine into the parent store,
// in pack/rev/idx dependency order.
func (quarantine *packQuarantine) promoteAll() error {
	entries, err := fs.ReadDir(quarantine.tempRoot.FS(), ".")
	if err != nil {
		return fmt.Errorf("object/store/packed: %w", err)
	}

	slices.SortFunc(entries, func(left, right fs.DirEntry) int {
		return packPromotionPriority(left.Name()) - packPromotionPriority(right.Name())
	})

	for _, entry := range entries {
		err := quarantine.promoteFile(entry.Name())
		if err != nil {
			return err
		}
	}

	return nil
}

// promoteFile links one quarantined artifact into the parent store,
// treating an already-present destination as success.
func (quarantine *packQuarantine) promoteFile(name string) error {
	src := quarantine.tempName + "/" + name

	err := quarantine.parent.root.Link(src, name)
	if err != nil && !errors.Is(err, fs.ErrExist) {
		return fmt.Errorf("object/store/packed: promoting %q: %w", name, err)
	}

	_ = quarantine.parent.root.Remove(src)

	return nil
}

// createPackQuarantineRoot creates a private quarantine directory beneath parent
// and returns its name and an os.Root over it.
func createPackQuarantineRoot(parent *os.Root) (string, *os.Root, error) {
	for range 32 {
		name := "tmp_packq_" + rand.Text()

		err := parent.Mkdir(name, 0o700)
		if err != nil {
			if errors.Is(err, fs.ErrExist) {
				continue
			}

			return "", nil, fmt.Errorf("object/store/packed: %w", err)
		}

		root, err := parent.OpenRoot(name)
		if err != nil {
			_ = parent.RemoveAll(name)

			return "", nil, fmt.Errorf("object/store/packed: %w", err)
		}

		return name, root, nil
	}

	return "", nil, errQuarantineNamesExhausted
}

// packPromotionPriority orders pack artifacts
// so that data files are linked before the index that publishes them.
func packPromotionPriority(name string) int {
	switch {
	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".pack"):
		return 1
	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".rev"):
		return 2
	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".bloom"):
		return 2
	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".idx"):
		return 3
	default:
		return 0
	}
}