2023-09-01 12:54:39 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-22 13:51:20 -05:00
|
|
|
if err := os.Link(src, dest); err != nil {
|
|
|
|
return driver.formatErr(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
/* also update the modtime, so that gc won't remove recently linked blobs
|
|
|
|
otherwise ifBlobOlderThan(gcDelay) will return the modtime of the inode */
|
2023-11-01 11:16:18 -05:00
|
|
|
currentTime := time.Now() //nolint: gosmopolitan
|
2023-09-22 13:51:20 -05:00
|
|
|
if err := os.Chtimes(dest, currentTime, currentTime); err != nil {
|
|
|
|
return driver.formatErr(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2023-09-01 12:54:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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"))
|
|
|
|
}
|