mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
e188f45890
* fix(storage): resolve double-prefixing issue for GCS rootdirectory Preserve double-prefixing for S3 to maintain backward compatibility with existing data. For GCS, always use "/" as rootDir to avoid double-prefixing, as GCS rootdirectory usage is a newer feature without legacy data. Signed-off-by: Sebastian Thees <thees@users.noreply.github.com> * fix(gcs): handle io.EOF correctly in Walk method Ensure io.EOF is returned unwrapped to allow proper error handling with errors.Is() upstream. Signed-off-by: Sebastian Thees <thees@users.noreply.github.com> * fix(storage): set sensible default ("/zot") for GCS when storageDriver.rootdirectory is unset or empty or "/" Signed-off-by: Sebastian Thees <thees@users.noreply.github.com> * fix(imagestore): avoid warning logs for expected cache miss scenarios Refine logging to use debug level for expected cache misses, preventing unnecessary warnings. Signed-off-by: Sebastian Thees <thees@users.noreply.github.com> --------- Signed-off-by: Sebastian Thees <thees@users.noreply.github.com>
230 lines
5.9 KiB
Go
230 lines
5.9 KiB
Go
package gcs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
|
|
// Add gcs support.
|
|
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
|
|
_ "github.com/distribution/distribution/v3/registry/storage/driver/gcs"
|
|
|
|
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
|
|
)
|
|
|
|
type Driver struct {
|
|
store storagedriver.StorageDriver
|
|
}
|
|
|
|
func New(storeDriver storagedriver.StorageDriver) *Driver {
|
|
return &Driver{store: storeDriver}
|
|
}
|
|
|
|
func (driver *Driver) Name() string {
|
|
return storageConstants.GCSStorageDriverName
|
|
}
|
|
|
|
func (driver *Driver) EnsureDir(path string) error {
|
|
return nil
|
|
}
|
|
|
|
func (driver *Driver) DirExists(path string) bool {
|
|
if fi, err := driver.Stat(path); err == nil && fi.IsDir() {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (driver *Driver) Reader(path string, offset int64) (io.ReadCloser, error) {
|
|
reader, err := driver.store.Reader(context.Background(), path, offset)
|
|
if err != nil {
|
|
return nil, driver.formatErr(err, path)
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
func (driver *Driver) ReadFile(path string) ([]byte, error) {
|
|
content, err := driver.store.GetContent(context.Background(), path)
|
|
if err != nil {
|
|
return nil, driver.formatErr(err, path)
|
|
}
|
|
|
|
return content, nil
|
|
}
|
|
|
|
func (driver *Driver) Delete(path string) error {
|
|
err := driver.store.Delete(context.Background(), path)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Format the error first to convert GCS-specific 404 errors to PathNotFoundError
|
|
formattedErr := driver.formatErr(err, path)
|
|
|
|
// Check if the formatted error is PathNotFoundError
|
|
var pathNotFoundErr storagedriver.PathNotFoundError
|
|
if errors.As(formattedErr, &pathNotFoundErr) {
|
|
// For directory deletion, if the path doesn't exist, treat it as success (idempotent delete)
|
|
// In GCS, directories are just prefixes, so if all objects are deleted,
|
|
// the directory may already be gone (especially with eventual consistency in storage-testbench)
|
|
// This makes Delete idempotent: deleting a non-existent path is a no-op
|
|
return nil
|
|
}
|
|
|
|
return formattedErr
|
|
}
|
|
|
|
func (driver *Driver) Stat(path string) (storagedriver.FileInfo, error) {
|
|
fileInfo, err := driver.store.Stat(context.Background(), path)
|
|
if err != nil {
|
|
return nil, driver.formatErr(err, path)
|
|
}
|
|
|
|
return fileInfo, nil
|
|
}
|
|
|
|
func (driver *Driver) Writer(filepath string, append bool) (storagedriver.FileWriter, error) { //nolint:predeclared
|
|
writer, err := driver.store.Writer(context.Background(), filepath, append)
|
|
if err != nil {
|
|
return nil, driver.formatErr(err, filepath)
|
|
}
|
|
|
|
return writer, nil
|
|
}
|
|
|
|
func (driver *Driver) WriteFile(filepath string, content []byte) (int, error) {
|
|
var n int
|
|
|
|
stwr, err := driver.store.Writer(context.Background(), filepath, false)
|
|
if err != nil {
|
|
return -1, driver.formatErr(err, filepath)
|
|
}
|
|
defer stwr.Close()
|
|
|
|
if n, err = stwr.Write(content); err != nil {
|
|
return -1, driver.formatErr(err, filepath)
|
|
}
|
|
|
|
if err := stwr.Commit(context.Background()); err != nil {
|
|
return -1, driver.formatErr(err, filepath)
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func (driver *Driver) Walk(path string, f storagedriver.WalkFn) error {
|
|
err := driver.store.Walk(context.Background(), path, f)
|
|
// io.EOF is used by callers (e.g. GetNextRepository) as a stop signal, not an error, so return directly.
|
|
if isEOF(err) {
|
|
return io.EOF
|
|
}
|
|
|
|
return driver.formatErr(err, path)
|
|
}
|
|
|
|
// isEOF checks whether err is directly io.EOF or if io.EOF is wrapped into storagedriver.Error.Detail.
|
|
func isEOF(err error) bool {
|
|
if errors.Is(err, io.EOF) {
|
|
return true
|
|
}
|
|
|
|
var storageErr storagedriver.Error
|
|
if errors.As(err, &storageErr) {
|
|
return errors.Is(storageErr.Detail, io.EOF)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (driver *Driver) List(fullpath string) ([]string, error) {
|
|
list, err := driver.store.List(context.Background(), fullpath)
|
|
if err != nil {
|
|
return nil, driver.formatErr(err, fullpath)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (driver *Driver) Move(sourcePath string, destPath string) error {
|
|
return driver.formatErr(driver.store.Move(context.Background(), sourcePath, destPath), sourcePath)
|
|
}
|
|
|
|
func (driver *Driver) SameFile(path1, path2 string) bool {
|
|
fi1, _ := driver.store.Stat(context.Background(), path1)
|
|
|
|
fi2, _ := driver.store.Stat(context.Background(), path2)
|
|
|
|
if fi1 != nil && fi2 != nil {
|
|
if fi1.IsDir() == fi2.IsDir() &&
|
|
fi1.ModTime() == fi2.ModTime() &&
|
|
fi1.Path() == fi2.Path() &&
|
|
fi1.Size() == fi2.Size() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Link puts an empty file that will act like a link between the original file and deduped one.
|
|
// Because gcs doesn't support symlinks, wherever the storage will encounter an empty file, it will get the original one
|
|
// from cache.
|
|
func (driver *Driver) Link(src, dest string) error {
|
|
return driver.formatErr(driver.store.PutContent(context.Background(), dest, []byte{}), dest)
|
|
}
|
|
|
|
// formatErr converts GCS-specific 404/not found errors to PathNotFoundError.
|
|
func (driver *Driver) formatErr(err error, path string) error {
|
|
switch actual := err.(type) { //nolint: errorlint
|
|
case nil:
|
|
return nil
|
|
case storagedriver.PathNotFoundError:
|
|
actual.DriverName = driver.Name()
|
|
if actual.Path == "" && path != "" {
|
|
actual.Path = path
|
|
}
|
|
|
|
return actual
|
|
case storagedriver.InvalidPathError:
|
|
actual.DriverName = driver.Name()
|
|
|
|
return actual
|
|
case storagedriver.InvalidOffsetError:
|
|
actual.DriverName = driver.Name()
|
|
|
|
return actual
|
|
default:
|
|
// Check for GCS-specific 404/not found errors by unwrapping the error chain
|
|
errToCheck := err
|
|
for errToCheck != nil {
|
|
errStr := errToCheck.Error()
|
|
isNotFound := strings.Contains(errStr, "object doesn't exist") ||
|
|
strings.Contains(errStr, "Error 404") ||
|
|
strings.Contains(errStr, "does not exist")
|
|
|
|
if isNotFound {
|
|
return storagedriver.PathNotFoundError{
|
|
DriverName: driver.Name(),
|
|
Path: path,
|
|
}
|
|
}
|
|
|
|
if unwrappable, ok := errToCheck.(interface{ Unwrap() error }); ok {
|
|
errToCheck = unwrappable.Unwrap()
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
storageError := storagedriver.Error{
|
|
DriverName: driver.Name(),
|
|
Detail: err,
|
|
}
|
|
|
|
return storageError
|
|
}
|
|
}
|