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 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
|
package backup
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"strconv"
"strings"
"gitlab.com/gitlab-org/gitaly/v16/internal/helper/text"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
)
// LegacyLocator locates backup paths for historic backups. This is the
// structure that gitlab used before incremental backups were introduced.
//
// Existing backup files are expected to be overwritten by the latest backup
// files.
//
// Structure:
//
// <repo relative path>.bundle
// <repo relative path>.refs
// <repo relative path>/custom_hooks.tar
type LegacyLocator struct{}
// BeginFull returns the static paths for a legacy repository backup
func (l LegacyLocator) BeginFull(ctx context.Context, repo *gitalypb.Repository, backupID string) *Step {
return l.newFull(repo)
}
// BeginIncremental is not supported for legacy backups
func (l LegacyLocator) BeginIncremental(ctx context.Context, repo *gitalypb.Repository, backupID string) (*Step, error) {
return nil, errors.New("legacy layout: begin incremental: not supported")
}
// Commit is unused as the locations are static
func (l LegacyLocator) Commit(ctx context.Context, full *Step) error {
return nil
}
// FindLatest returns the static paths for a legacy repository backup
func (l LegacyLocator) FindLatest(ctx context.Context, repo *gitalypb.Repository) (*Backup, error) {
return &Backup{
Steps: []Step{
*l.newFull(repo),
},
}, nil
}
func (l LegacyLocator) newFull(repo *gitalypb.Repository) *Step {
backupPath := strings.TrimSuffix(repo.RelativePath, ".git")
return &Step{
SkippableOnNotFound: true,
BundlePath: backupPath + ".bundle",
RefPath: backupPath + ".refs",
CustomHooksPath: filepath.Join(backupPath, "custom_hooks.tar"),
}
}
// PointerLocator locates backup paths where each full backup is put into a
// unique timestamp directory and the latest backup taken is pointed to by a
// file named LATEST.
//
// Structure:
//
// <repo relative path>/LATEST
// <repo relative path>/<backup id>/LATEST
// <repo relative path>/<backup id>/<nnn>.bundle
// <repo relative path>/<backup id>/<nnn>.refs
// <repo relative path>/<backup id>/<nnn>.custom_hooks.tar
type PointerLocator struct {
Sink Sink
Fallback Locator
}
// BeginFull returns a tentative first step needed to create a new full backup.
func (l PointerLocator) BeginFull(ctx context.Context, repo *gitalypb.Repository, backupID string) *Step {
repoPath := strings.TrimSuffix(repo.RelativePath, ".git")
return &Step{
BundlePath: filepath.Join(repoPath, backupID, "001.bundle"),
RefPath: filepath.Join(repoPath, backupID, "001.refs"),
CustomHooksPath: filepath.Join(repoPath, backupID, "001.custom_hooks.tar"),
}
}
// BeginIncremental returns a tentative step needed to create a new incremental
// backup. The incremental backup is always based off of the latest full
// backup. If there is no latest backup, a new full backup step is returned
// using fallbackBackupID
func (l PointerLocator) BeginIncremental(ctx context.Context, repo *gitalypb.Repository, fallbackBackupID string) (*Step, error) {
repoPath := strings.TrimSuffix(repo.RelativePath, ".git")
backupID, err := l.findLatestID(ctx, repoPath)
if err != nil {
if errors.Is(err, ErrDoesntExist) {
return l.BeginFull(ctx, repo, fallbackBackupID), nil
}
return nil, fmt.Errorf("pointer locator: begin incremental: %w", err)
}
backup, err := l.find(ctx, repo, backupID)
if err != nil {
return nil, err
}
if len(backup.Steps) < 1 {
return nil, fmt.Errorf("pointer locator: begin incremental: no full backup")
}
previous := backup.Steps[len(backup.Steps)-1]
backupPath := filepath.Dir(previous.BundlePath)
bundleName := filepath.Base(previous.BundlePath)
incrementID := bundleName[:len(bundleName)-len(filepath.Ext(bundleName))]
id, err := strconv.Atoi(incrementID)
if err != nil {
return nil, fmt.Errorf("pointer locator: begin incremental: determine increment ID: %w", err)
}
id++
return &Step{
BundlePath: filepath.Join(backupPath, fmt.Sprintf("%03d.bundle", id)),
RefPath: filepath.Join(backupPath, fmt.Sprintf("%03d.refs", id)),
PreviousRefPath: previous.RefPath,
CustomHooksPath: filepath.Join(backupPath, fmt.Sprintf("%03d.custom_hooks.tar", id)),
}, nil
}
// Commit persists the step so that it can be looked up by FindLatest
func (l PointerLocator) Commit(ctx context.Context, step *Step) error {
backupPath := filepath.Dir(step.BundlePath)
bundleName := filepath.Base(step.BundlePath)
repoPath := filepath.Dir(backupPath)
backupID := filepath.Base(backupPath)
incrementID := bundleName[:len(bundleName)-len(filepath.Ext(bundleName))]
if err := l.writeLatest(ctx, backupPath, incrementID); err != nil {
return fmt.Errorf("pointer locator: commit: %w", err)
}
if err := l.writeLatest(ctx, repoPath, backupID); err != nil {
return fmt.Errorf("pointer locator: commit: %w", err)
}
return nil
}
// FindLatest returns the paths committed by the latest call to CommitFull.
//
// If there is no `LATEST` file, the result of the `Fallback` is used.
func (l PointerLocator) FindLatest(ctx context.Context, repo *gitalypb.Repository) (*Backup, error) {
repoPath := strings.TrimSuffix(repo.RelativePath, ".git")
backupID, err := l.findLatestID(ctx, repoPath)
if err != nil {
if l.Fallback != nil && errors.Is(err, ErrDoesntExist) {
return l.Fallback.FindLatest(ctx, repo)
}
return nil, fmt.Errorf("pointer locator: find latest: %w", err)
}
backup, err := l.find(ctx, repo, backupID)
if err != nil {
return nil, fmt.Errorf("pointer locator: find latest: %w", err)
}
return backup, nil
}
// find returns the repository backup at the given backupID. If the backup does
// not exist then the error ErrDoesntExist is returned.
func (l PointerLocator) find(ctx context.Context, repo *gitalypb.Repository, backupID string) (*Backup, error) {
repoPath := strings.TrimSuffix(repo.RelativePath, ".git")
backupPath := filepath.Join(repoPath, backupID)
latestIncrementID, err := l.findLatestID(ctx, backupPath)
if err != nil {
return nil, fmt.Errorf("find: %w", err)
}
max, err := strconv.Atoi(latestIncrementID)
if err != nil {
return nil, fmt.Errorf("find: determine increment ID: %w", err)
}
var backup Backup
for i := 1; i <= max; i++ {
var previousRefPath string
if i > 1 {
previousRefPath = filepath.Join(backupPath, fmt.Sprintf("%03d.refs", i-1))
}
backup.Steps = append(backup.Steps, Step{
BundlePath: filepath.Join(backupPath, fmt.Sprintf("%03d.bundle", i)),
RefPath: filepath.Join(backupPath, fmt.Sprintf("%03d.refs", i)),
PreviousRefPath: previousRefPath,
CustomHooksPath: filepath.Join(backupPath, fmt.Sprintf("%03d.custom_hooks.tar", i)),
})
}
return &backup, nil
}
func (l PointerLocator) findLatestID(ctx context.Context, backupPath string) (string, error) {
r, err := l.Sink.GetReader(ctx, filepath.Join(backupPath, "LATEST"))
if err != nil {
return "", fmt.Errorf("find latest ID: %w", err)
}
defer r.Close()
latest, err := io.ReadAll(r)
if err != nil {
return "", fmt.Errorf("find latest ID: %w", err)
}
return text.ChompBytes(latest), nil
}
func (l PointerLocator) writeLatest(ctx context.Context, path, target string) (returnErr error) {
latest, err := l.Sink.GetWriter(ctx, filepath.Join(path, "LATEST"))
if err != nil {
return fmt.Errorf("write latest: %w", err)
}
defer func() {
if err := latest.Close(); err != nil && returnErr == nil {
returnErr = fmt.Errorf("write latest: %w", err)
}
}()
if _, err := latest.Write([]byte(target)); err != nil {
return fmt.Errorf("write latest: %w", err)
}
return nil
}
|