internal/tempdir/tempdir.go (79 lines of code) (raw):
package tempdir
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage/mode"
"gitlab.com/gitlab-org/gitaly/v16/internal/log"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
)
// Dir is a storage-scoped temporary directory.
type Dir struct {
logger log.Logger
path string
doneCh chan struct{}
}
// Path returns the absolute path of the temporary directory.
func (d Dir) Path() string {
return d.path
}
// New returns the path of a new temporary directory for the given storage. The directory is removed
// asynchronously with os.RemoveAll when the context expires.
func New(ctx context.Context, storageName string, logger log.Logger, locator storage.Locator) (Dir, error) {
return NewWithPrefix(ctx, storageName, "repo", logger, locator)
}
// NewWithPrefix returns the path of a new temporary directory for the given storage with a specific
// prefix used to create the temporary directory's name. The directory is removed asynchronously
// with os.RemoveAll when the context expires.
func NewWithPrefix(ctx context.Context, storageName, prefix string, logger log.Logger, locator storage.Locator) (Dir, error) {
dir, err := newDirectory(ctx, storageName, prefix, logger, locator)
if err != nil {
return Dir{}, err
}
go dir.cleanupOnDone(ctx)
return dir, nil
}
// NewWithoutContext returns a temporary directory for the given storage suitable which is not
// storage scoped. The temporary directory will thus not get cleaned up when the context expires,
// but instead when the temporary directory is older than MaxAge.
func NewWithoutContext(storageName string, logger log.Logger, locator storage.Locator) (Dir, error) {
prefix := fmt.Sprintf("%s-repositories.old.%d.", storageName, time.Now().Unix())
return newDirectory(context.Background(), storageName, prefix, logger, locator)
}
// NewRepository is the same as New, but it returns a *gitalypb.Repository for the created directory
// as well as the bare path as a string.
func NewRepository(ctx context.Context, storageName string, logger log.Logger, locator storage.Locator) (*gitalypb.Repository, Dir, error) {
storagePath, err := locator.GetStorageByName(ctx, storageName)
if err != nil {
return nil, Dir{}, err
}
dir, err := New(ctx, storageName, logger, locator)
if err != nil {
return nil, Dir{}, err
}
newRepo := &gitalypb.Repository{StorageName: storageName}
newRepo.RelativePath, err = filepath.Rel(storagePath, dir.Path())
if err != nil {
return nil, Dir{}, err
}
return newRepo, dir, nil
}
func newDirectory(ctx context.Context, storageName string, prefix string, logger log.Logger, loc storage.Locator) (Dir, error) {
root, err := loc.TempDir(storageName)
if err != nil {
return Dir{}, fmt.Errorf("temp directory: %w", err)
}
if err := os.MkdirAll(root, mode.Directory); err != nil {
return Dir{}, err
}
tempDir, err := os.MkdirTemp(root, prefix)
if err != nil {
return Dir{}, err
}
return Dir{
logger: logger,
path: tempDir,
doneCh: make(chan struct{}),
}, err
}
func (d Dir) cleanupOnDone(ctx context.Context) {
<-ctx.Done()
if err := os.RemoveAll(d.Path()); err != nil {
d.logger.WithError(err).WithField("temporary_directory", d.Path).ErrorContext(ctx, "failed to cleanup temp dir")
}
close(d.doneCh)
}
// WaitForCleanup waits until the temporary directory got removed via the asynchronous cleanupOnDone
// call. This is mainly intended for use in tests.
func (d Dir) WaitForCleanup() {
<-d.doneCh
}