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 } }