0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-23 22:27:35 -05:00
zot/pkg/storage/local/driver.go
peusebiu b80deb9927
refactor(storage): refactor storage into a single ImageStore (#1656)
unified both local and s3 ImageStore logic into a single ImageStore
added a new driver interface for common file/dirs manipulations
to be implemented by different storage types

refactor(gc): drop umoci dependency, implemented internal gc

added retentionDelay config option that specifies
the garbage collect delay for images without tags

this will also clean manifests which are part of an index image
(multiarch) that no longer exist.

fix(dedupe): skip blobs under .sync/ directory

if startup dedupe is running while also syncing is running
ignore blobs under sync's temporary storage

fix(storage): do not allow image indexes modifications

when deleting a manifest verify that it is not part of a multiarch image
and throw a MethodNotAllowed error to the client if it is.
we don't want to modify multiarch images

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
2023-09-01 10:54:39 -07:00

481 lines
9.9 KiB
Go

package local
import (
"bufio"
"bytes"
"errors"
"io"
"io/fs"
"os"
"path"
"sort"
"syscall"
"time"
"unicode/utf8"
storagedriver "github.com/docker/distribution/registry/storage/driver"
zerr "zotregistry.io/zot/errors"
storageConstants "zotregistry.io/zot/pkg/storage/constants"
"zotregistry.io/zot/pkg/test/inject"
)
type Driver struct {
commit bool
}
func New(commit bool) *Driver {
return &Driver{commit: commit}
}
func (driver *Driver) Name() string {
return storageConstants.LocalStorageDriverName
}
func (driver *Driver) EnsureDir(path string) error {
err := os.MkdirAll(path, storageConstants.DefaultDirPerms)
return driver.formatErr(err)
}
func (driver *Driver) DirExists(path string) bool {
if !utf8.ValidString(path) {
return false
}
fileInfo, err := os.Stat(path)
if err != nil {
if e, ok := err.(*fs.PathError); ok && errors.Is(e.Err, syscall.ENAMETOOLONG) || //nolint: errorlint
errors.Is(e.Err, syscall.EINVAL) {
return false
}
}
if err != nil && os.IsNotExist(err) {
return false
}
if !fileInfo.IsDir() {
return false
}
return true
}
func (driver *Driver) Reader(path string, offset int64) (io.ReadCloser, error) {
file, err := os.OpenFile(path, os.O_RDONLY, storageConstants.DefaultFilePerms)
if err != nil {
if os.IsNotExist(err) {
return nil, storagedriver.PathNotFoundError{Path: path}
}
return nil, driver.formatErr(err)
}
seekPos, err := file.Seek(offset, io.SeekStart)
if err != nil {
file.Close()
return nil, driver.formatErr(err)
} else if seekPos < offset {
file.Close()
return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset}
}
return file, nil
}
func (driver *Driver) ReadFile(path string) ([]byte, error) {
reader, err := driver.Reader(path, 0)
if err != nil {
return nil, err
}
defer reader.Close()
buf, err := io.ReadAll(reader)
if err != nil {
return nil, driver.formatErr(err)
}
return buf, nil
}
func (driver *Driver) Delete(path string) error {
_, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
return driver.formatErr(err)
} else if err != nil {
return storagedriver.PathNotFoundError{Path: path}
}
return os.RemoveAll(path)
}
func (driver *Driver) Stat(path string) (storagedriver.FileInfo, error) {
fi, err := os.Stat(path) //nolint: varnamelen
if err != nil {
if os.IsNotExist(err) {
return nil, storagedriver.PathNotFoundError{Path: path}
}
return nil, driver.formatErr(err)
}
return fileInfo{
path: path,
FileInfo: fi,
}, nil
}
func (driver *Driver) Writer(filepath string, append bool) (storagedriver.FileWriter, error) { //nolint:predeclared
if append {
_, err := os.Stat(filepath)
if err != nil {
if os.IsNotExist(err) {
return nil, storagedriver.PathNotFoundError{Path: filepath}
}
return nil, driver.formatErr(err)
}
}
parentDir := path.Dir(filepath)
if err := os.MkdirAll(parentDir, storageConstants.DefaultDirPerms); err != nil {
return nil, driver.formatErr(err)
}
file, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, storageConstants.DefaultFilePerms)
if err != nil {
return nil, driver.formatErr(err)
}
var offset int64
if !append {
err := file.Truncate(0)
if err != nil {
file.Close()
return nil, driver.formatErr(err)
}
} else {
n, err := file.Seek(0, io.SeekEnd) //nolint: varnamelen
if err != nil {
file.Close()
return nil, driver.formatErr(err)
}
offset = n
}
return newFileWriter(file, offset, driver.commit), nil
}
func (driver *Driver) WriteFile(filepath string, content []byte) (int, error) {
writer, err := driver.Writer(filepath, false)
if err != nil {
return -1, err
}
nbytes, err := io.Copy(writer, bytes.NewReader(content))
if err != nil {
_ = writer.Cancel()
return -1, driver.formatErr(err)
}
return int(nbytes), writer.Close()
}
func (driver *Driver) Walk(path string, walkFn storagedriver.WalkFn) error {
children, err := driver.List(path)
if err != nil {
return err
}
sort.Stable(sort.StringSlice(children))
for _, child := range children {
// Calling driver.Stat for every entry is quite
// expensive when running against backends with a slow Stat
// implementation, such as s3. This is very likely a serious
// performance bottleneck.
fileInfo, err := driver.Stat(child)
if err != nil {
switch errors.As(err, &storagedriver.PathNotFoundError{}) {
case true:
// repository was removed in between listing and enumeration. Ignore it.
continue
default:
return err
}
}
err = walkFn(fileInfo)
//nolint: gocritic
if err == nil && fileInfo.IsDir() {
if err := driver.Walk(child, walkFn); err != nil {
return err
}
} else if errors.Is(err, storagedriver.ErrSkipDir) {
// Stop iteration if it's a file, otherwise noop if it's a directory
if !fileInfo.IsDir() {
return nil
}
} else if err != nil {
return driver.formatErr(err)
}
}
return nil
}
func (driver *Driver) List(fullpath string) ([]string, error) {
dir, err := os.Open(fullpath)
if err != nil {
if os.IsNotExist(err) {
return nil, storagedriver.PathNotFoundError{Path: fullpath}
}
return nil, driver.formatErr(err)
}
defer dir.Close()
fileNames, err := dir.Readdirnames(0)
if err != nil {
return nil, driver.formatErr(err)
}
keys := make([]string, 0, len(fileNames))
for _, fileName := range fileNames {
keys = append(keys, path.Join(fullpath, fileName))
}
return keys, nil
}
func (driver *Driver) Move(sourcePath string, destPath string) error {
if _, err := os.Stat(sourcePath); os.IsNotExist(err) {
return storagedriver.PathNotFoundError{Path: sourcePath}
}
if err := os.MkdirAll(path.Dir(destPath), storageConstants.DefaultDirPerms); err != nil {
return driver.formatErr(err)
}
return driver.formatErr(os.Rename(sourcePath, destPath))
}
func (driver *Driver) SameFile(path1, path2 string) bool {
file1, err := os.Stat(path1)
if err != nil {
return false
}
file2, err := os.Stat(path2)
if err != nil {
return false
}
return os.SameFile(file1, file2)
}
func (driver *Driver) Link(src, dest string) error {
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
return err
}
return driver.formatErr(os.Link(src, dest))
}
func (driver *Driver) formatErr(err error) error {
switch actual := err.(type) { //nolint: errorlint
case nil:
return nil
case storagedriver.PathNotFoundError:
actual.DriverName = driver.Name()
return actual
case storagedriver.InvalidPathError:
actual.DriverName = driver.Name()
return actual
case storagedriver.InvalidOffsetError:
actual.DriverName = driver.Name()
return actual
default:
storageError := storagedriver.Error{
DriverName: driver.Name(),
Enclosed: err,
}
return storageError
}
}
type fileInfo struct {
os.FileInfo
path string
}
// asserts fileInfo implements storagedriver.FileInfo.
var _ storagedriver.FileInfo = fileInfo{}
// Path provides the full path of the target of this file info.
func (fi fileInfo) Path() string {
return fi.path
}
// Size returns current length in bytes of the file. The return value can
// be used to write to the end of the file at path. The value is
// meaningless if IsDir returns true.
func (fi fileInfo) Size() int64 {
if fi.IsDir() {
return 0
}
return fi.FileInfo.Size()
}
// ModTime returns the modification time for the file. For backends that
// don't have a modification time, the creation time should be returned.
func (fi fileInfo) ModTime() time.Time {
return fi.FileInfo.ModTime()
}
// IsDir returns true if the path is a directory.
func (fi fileInfo) IsDir() bool {
return fi.FileInfo.IsDir()
}
type fileWriter struct {
file *os.File
size int64
bw *bufio.Writer
closed bool
committed bool
cancelled bool
commit bool
}
func newFileWriter(file *os.File, size int64, commit bool) *fileWriter {
return &fileWriter{
file: file,
size: size,
commit: commit,
bw: bufio.NewWriter(file),
}
}
func (fw *fileWriter) Write(buf []byte) (int, error) {
//nolint: gocritic
if fw.closed {
return 0, zerr.ErrFileAlreadyClosed
} else if fw.committed {
return 0, zerr.ErrFileAlreadyCommitted
} else if fw.cancelled {
return 0, zerr.ErrFileAlreadyCancelled
}
n, err := fw.bw.Write(buf)
fw.size += int64(n)
return n, err
}
func (fw *fileWriter) Size() int64 {
return fw.size
}
func (fw *fileWriter) Close() error {
if fw.closed {
return zerr.ErrFileAlreadyClosed
}
if err := fw.bw.Flush(); err != nil {
return err
}
if fw.commit {
if err := inject.Error(fw.file.Sync()); err != nil {
return err
}
}
if err := inject.Error(fw.file.Close()); err != nil {
return err
}
fw.closed = true
return nil
}
func (fw *fileWriter) Cancel() error {
if fw.closed {
return zerr.ErrFileAlreadyClosed
}
fw.cancelled = true
fw.file.Close()
return os.Remove(fw.file.Name())
}
func (fw *fileWriter) Commit() error {
//nolint: gocritic
if fw.closed {
return zerr.ErrFileAlreadyClosed
} else if fw.committed {
return zerr.ErrFileAlreadyCommitted
} else if fw.cancelled {
return zerr.ErrFileAlreadyCancelled
}
if err := fw.bw.Flush(); err != nil {
return err
}
if fw.commit {
if err := fw.file.Sync(); err != nil {
return err
}
}
fw.committed = true
return nil
}
func ValidateHardLink(rootDir string) error {
if err := os.MkdirAll(rootDir, storageConstants.DefaultDirPerms); err != nil {
return err
}
err := os.WriteFile(path.Join(rootDir, "hardlinkcheck.txt"),
[]byte("check whether hardlinks work on filesystem"), storageConstants.DefaultFilePerms)
if err != nil {
return err
}
err = os.Link(path.Join(rootDir, "hardlinkcheck.txt"), path.Join(rootDir, "duphardlinkcheck.txt"))
if err != nil {
// Remove hardlinkcheck.txt if hardlink fails
zerr := os.RemoveAll(path.Join(rootDir, "hardlinkcheck.txt"))
if zerr != nil {
return zerr
}
return err
}
err = os.RemoveAll(path.Join(rootDir, "hardlinkcheck.txt"))
if err != nil {
return err
}
return os.RemoveAll(path.Join(rootDir, "duphardlinkcheck.txt"))
}