1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-06-02 16:48:16 -04:00
pkg-proxy/internal/storage/filesystem.go
Mati Kepa 31a9ca75b2
add Gradle Build Cache support with handler and tests (#87)
* add Gradle Build Cache support with handler and tests

* linting issue

* MR Suggestions: Add Gradle HTTP Build Cache configuration to README

* implement  minor stuff: Refactor Gradle handler to remove unnecessary URL parameter and update related tests

Co-authored-by: Copilot <copilot@github.com>

* Add Gradle build cache configuration and eviction support

- Introduced configuration options for Gradle build cache in config files and documentation.
- Implemented read-only mode and upload size limits for the Gradle build cache.
- Added cache eviction logic based on age and size, with corresponding tests.
- Enhanced storage interfaces to support listing objects by prefix.

* implement minor stuff: Refactor Gradle handler to remove unnecessary URL parameter and update related tests

* last finding fix

* fix tests and implement PR suggestions

Co-authored-by: Copilot <copilot@github.com>

* unify path

---------

Co-authored-by: Mateusz (Mati) Kepa <m.kepa@sportradar.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 11:15:16 +01:00

257 lines
5.6 KiB
Go

package storage
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
fsys "io/fs"
"os"
"path/filepath"
"strings"
"time"
)
// Filesystem implements Storage using the local filesystem.
type Filesystem struct {
root string
}
// NewFilesystem creates a new filesystem storage rooted at the given directory.
// The directory will be created if it does not exist.
func NewFilesystem(root string) (*Filesystem, error) {
absRoot, err := filepath.Abs(root)
if err != nil {
return nil, fmt.Errorf("resolving root path: %w", err)
}
if err := os.MkdirAll(absRoot, dirPermissions); err != nil {
return nil, fmt.Errorf("creating root directory: %w", err)
}
return &Filesystem{root: absRoot}, nil
}
func (fs *Filesystem) fullPath(path string) (string, error) {
full := filepath.Clean(filepath.Join(fs.root, filepath.FromSlash(path)))
if full != fs.root && !strings.HasPrefix(full, fs.root+string(filepath.Separator)) {
return "", fmt.Errorf("%w: path escapes storage root", ErrNotFound)
}
return full, nil
}
func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return 0, "", err
}
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, dirPermissions); err != nil {
return 0, "", fmt.Errorf("creating directory: %w", err)
}
// Write to temp file first for atomic operation
tmpFile, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return 0, "", fmt.Errorf("creating temp file: %w", err)
}
tmpPath := tmpFile.Name()
// Clean up temp file on error
success := false
defer func() {
if !success {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
}
}()
// Write content and compute hash
h := sha256.New()
w := io.MultiWriter(tmpFile, h)
size, err := io.Copy(w, r)
if err != nil {
return 0, "", fmt.Errorf("writing content: %w", err)
}
if err := tmpFile.Close(); err != nil {
return 0, "", fmt.Errorf("closing temp file: %w", err)
}
// Atomic rename
if err := os.Rename(tmpPath, fullPath); err != nil {
return 0, "", fmt.Errorf("renaming temp file: %w", err)
}
success = true
hash := hex.EncodeToString(h.Sum(nil))
return size, hash, nil
}
func (fs *Filesystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return nil, err
}
f, err := os.Open(fullPath)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("opening file: %w", err)
}
return f, nil
}
func (fs *Filesystem) Exists(ctx context.Context, path string) (bool, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return false, err
}
_, err = os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("checking file: %w", err)
}
return true, nil
}
func (fs *Filesystem) Delete(ctx context.Context, path string) error {
fullPath, err := fs.fullPath(path)
if err != nil {
return err
}
err = os.Remove(fullPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing file: %w", err)
}
// Try to clean up empty parent directories
dir := filepath.Dir(fullPath)
for dir != fs.root {
if err := os.Remove(dir); err != nil {
break // Directory not empty or other error
}
dir = filepath.Dir(dir)
}
return nil
}
func (fs *Filesystem) SignedURL(_ context.Context, _ string, _ time.Duration) (string, error) {
return "", ErrSignedURLUnsupported
}
func (fs *Filesystem) Size(ctx context.Context, path string) (int64, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return 0, err
}
info, err := os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return 0, ErrNotFound
}
return 0, fmt.Errorf("stat file: %w", err)
}
return info.Size(), nil
}
func (fs *Filesystem) UsedSpace(ctx context.Context) (int64, error) {
var total int64
err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
total += info.Size()
}
return nil
})
if err != nil {
return 0, fmt.Errorf("walking directory: %w", err)
}
return total, nil
}
// ListPrefix returns object metadata for paths under a prefix.
func (fs *Filesystem) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) {
searchRoot, err := fs.fullPath(prefix)
if err != nil {
return nil, err
}
if _, err := os.Stat(searchRoot); err != nil {
if os.IsNotExist(err) {
return []ObjectInfo{}, nil
}
return nil, fmt.Errorf("stat prefix: %w", err)
}
objects := make([]ObjectInfo, 0)
err = filepath.WalkDir(searchRoot, func(path string, entry fsys.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
info, err := entry.Info()
if err != nil {
return err
}
relPath, err := filepath.Rel(fs.root, path)
if err != nil {
return err
}
objects = append(objects, ObjectInfo{
Path: filepath.ToSlash(relPath),
Size: info.Size(),
ModTime: info.ModTime(),
})
return nil
})
if err != nil {
return nil, fmt.Errorf("walking prefix: %w", err)
}
return objects, nil
}
// Root returns the root directory of the storage.
func (fs *Filesystem) Root() string {
return fs.root
}
// FullPath returns the full filesystem path for a storage path.
// Useful for serving files directly or debugging.
// Returns an error if the resulting path would escape the storage root.
func (fs *Filesystem) FullPath(path string) (string, error) {
return fs.fullPath(path)
}
func (fs *Filesystem) URL() string {
return "file://" + filepath.ToSlash(fs.root)
}
func (fs *Filesystem) Close() error {
return nil
}