forked from TrueCloudLab/distribution
cb0d083d8d
This commit changes storagedriver.Filewriter interface by adding context.Context as an argument to its Commit func. We pass the context appropriately where need be throughout the distribution codebase to all the writers and tests. S3 driver writer unfortunately must maintain the context passed down to it from upstream so it contnues to implement io.Writer and io.Closer interfaces which do not allow accepting the context in any of their funcs. Co-authored-by: Cory Snider <corhere@gmail.com> Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
419 lines
9.7 KiB
Go
419 lines
9.7 KiB
Go
package filesystem
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"time"
|
|
|
|
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
|
|
"github.com/distribution/distribution/v3/registry/storage/driver/base"
|
|
"github.com/distribution/distribution/v3/registry/storage/driver/factory"
|
|
)
|
|
|
|
const (
|
|
driverName = "filesystem"
|
|
defaultRootDirectory = "/var/lib/registry"
|
|
defaultMaxThreads = uint64(100)
|
|
|
|
// minThreads is the minimum value for the maxthreads configuration
|
|
// parameter. If the driver's parameters are less than this we set
|
|
// the parameters to minThreads
|
|
minThreads = uint64(25)
|
|
)
|
|
|
|
// DriverParameters represents all configuration options available for the
|
|
// filesystem driver
|
|
type DriverParameters struct {
|
|
RootDirectory string
|
|
MaxThreads uint64
|
|
}
|
|
|
|
func init() {
|
|
factory.Register(driverName, &filesystemDriverFactory{})
|
|
}
|
|
|
|
// filesystemDriverFactory implements the factory.StorageDriverFactory interface
|
|
type filesystemDriverFactory struct{}
|
|
|
|
func (factory *filesystemDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) {
|
|
return FromParameters(parameters)
|
|
}
|
|
|
|
type driver struct {
|
|
rootDirectory string
|
|
}
|
|
|
|
type baseEmbed struct {
|
|
base.Base
|
|
}
|
|
|
|
// Driver is a storagedriver.StorageDriver implementation backed by a local
|
|
// filesystem. All provided paths will be subpaths of the RootDirectory.
|
|
type Driver struct {
|
|
baseEmbed
|
|
}
|
|
|
|
// FromParameters constructs a new Driver with a given parameters map
|
|
// Optional Parameters:
|
|
// - rootdirectory
|
|
// - maxthreads
|
|
func FromParameters(parameters map[string]interface{}) (*Driver, error) {
|
|
params, err := fromParametersImpl(parameters)
|
|
if err != nil || params == nil {
|
|
return nil, err
|
|
}
|
|
return New(*params), nil
|
|
}
|
|
|
|
func fromParametersImpl(parameters map[string]interface{}) (*DriverParameters, error) {
|
|
var (
|
|
err error
|
|
maxThreads = defaultMaxThreads
|
|
rootDirectory = defaultRootDirectory
|
|
)
|
|
|
|
if parameters != nil {
|
|
if rootDir, ok := parameters["rootdirectory"]; ok {
|
|
rootDirectory = fmt.Sprint(rootDir)
|
|
}
|
|
|
|
maxThreads, err = base.GetLimitFromParameter(parameters["maxthreads"], minThreads, defaultMaxThreads)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("maxthreads config error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
params := &DriverParameters{
|
|
RootDirectory: rootDirectory,
|
|
MaxThreads: maxThreads,
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
// New constructs a new Driver with a given rootDirectory
|
|
func New(params DriverParameters) *Driver {
|
|
fsDriver := &driver{rootDirectory: params.RootDirectory}
|
|
|
|
return &Driver{
|
|
baseEmbed: baseEmbed{
|
|
Base: base.Base{
|
|
StorageDriver: base.NewRegulator(fsDriver, params.MaxThreads),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Implement the storagedriver.StorageDriver interface
|
|
|
|
func (d *driver) Name() string {
|
|
return driverName
|
|
}
|
|
|
|
// GetContent retrieves the content stored at "path" as a []byte.
|
|
func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) {
|
|
rc, err := d.Reader(ctx, path, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
|
|
p, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// PutContent stores the []byte content at a location designated by "path".
|
|
func (d *driver) PutContent(ctx context.Context, subPath string, contents []byte) error {
|
|
writer, err := d.Writer(ctx, subPath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer writer.Close()
|
|
_, err = io.Copy(writer, bytes.NewReader(contents))
|
|
if err != nil {
|
|
writer.Cancel(ctx)
|
|
return err
|
|
}
|
|
return writer.Commit(ctx)
|
|
}
|
|
|
|
// Reader retrieves an io.ReadCloser for the content stored at "path" with a
|
|
// given byte offset.
|
|
func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
|
|
file, err := os.OpenFile(d.fullPath(path), os.O_RDONLY, 0o644)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, storagedriver.PathNotFoundError{Path: path}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
seekPos, err := file.Seek(offset, io.SeekStart)
|
|
if err != nil {
|
|
file.Close()
|
|
return nil, err
|
|
} else if seekPos < offset {
|
|
file.Close()
|
|
return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset}
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
|
|
func (d *driver) Writer(ctx context.Context, subPath string, append bool) (storagedriver.FileWriter, error) {
|
|
fullPath := d.fullPath(subPath)
|
|
parentDir := path.Dir(fullPath)
|
|
if err := os.MkdirAll(parentDir, 0o777); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fp, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE, 0o666)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var offset int64
|
|
|
|
if !append {
|
|
err := fp.Truncate(0)
|
|
if err != nil {
|
|
fp.Close()
|
|
return nil, err
|
|
}
|
|
} else {
|
|
n, err := fp.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
fp.Close()
|
|
return nil, err
|
|
}
|
|
offset = n
|
|
}
|
|
|
|
return newFileWriter(fp, offset), nil
|
|
}
|
|
|
|
// Stat retrieves the FileInfo for the given path, including the current size
|
|
// in bytes and the creation time.
|
|
func (d *driver) Stat(ctx context.Context, subPath string) (storagedriver.FileInfo, error) {
|
|
fullPath := d.fullPath(subPath)
|
|
|
|
fi, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, storagedriver.PathNotFoundError{Path: subPath}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return fileInfo{
|
|
path: subPath,
|
|
FileInfo: fi,
|
|
}, nil
|
|
}
|
|
|
|
// List returns a list of the objects that are direct descendants of the given
|
|
// path.
|
|
func (d *driver) List(ctx context.Context, subPath string) ([]string, error) {
|
|
fullPath := d.fullPath(subPath)
|
|
|
|
dir, err := os.Open(fullPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, storagedriver.PathNotFoundError{Path: subPath}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
defer dir.Close()
|
|
|
|
fileNames, err := dir.Readdirnames(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keys := make([]string, 0, len(fileNames))
|
|
for _, fileName := range fileNames {
|
|
keys = append(keys, path.Join(subPath, fileName))
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
// Move moves an object stored at sourcePath to destPath, removing the original
|
|
// object.
|
|
func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error {
|
|
source := d.fullPath(sourcePath)
|
|
dest := d.fullPath(destPath)
|
|
|
|
if _, err := os.Stat(source); os.IsNotExist(err) {
|
|
return storagedriver.PathNotFoundError{Path: sourcePath}
|
|
}
|
|
|
|
if err := os.MkdirAll(path.Dir(dest), 0o777); err != nil {
|
|
return err
|
|
}
|
|
|
|
err := os.Rename(source, dest)
|
|
return err
|
|
}
|
|
|
|
// Delete recursively deletes all objects stored at "path" and its subpaths.
|
|
func (d *driver) Delete(ctx context.Context, subPath string) error {
|
|
fullPath := d.fullPath(subPath)
|
|
|
|
_, err := os.Stat(fullPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
} else if err != nil {
|
|
return storagedriver.PathNotFoundError{Path: subPath}
|
|
}
|
|
|
|
err = os.RemoveAll(fullPath)
|
|
return err
|
|
}
|
|
|
|
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
|
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
|
func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
|
return "", storagedriver.ErrUnsupportedMethod{}
|
|
}
|
|
|
|
// Walk traverses a filesystem defined within driver, starting
|
|
// from the given path, calling f on each file and directory
|
|
func (d *driver) Walk(ctx context.Context, path string, f storagedriver.WalkFn, options ...func(*storagedriver.WalkOptions)) error {
|
|
return storagedriver.WalkFallback(ctx, d, path, f, options...)
|
|
}
|
|
|
|
// fullPath returns the absolute path of a key within the Driver's storage.
|
|
func (d *driver) fullPath(subPath string) string {
|
|
return path.Join(d.rootDirectory, subPath)
|
|
}
|
|
|
|
type fileInfo struct {
|
|
os.FileInfo
|
|
path string
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func newFileWriter(file *os.File, size int64) *fileWriter {
|
|
return &fileWriter{
|
|
file: file,
|
|
size: size,
|
|
bw: bufio.NewWriter(file),
|
|
}
|
|
}
|
|
|
|
func (fw *fileWriter) Write(p []byte) (int, error) {
|
|
if fw.closed {
|
|
return 0, fmt.Errorf("already closed")
|
|
} else if fw.committed {
|
|
return 0, fmt.Errorf("already committed")
|
|
} else if fw.cancelled {
|
|
return 0, fmt.Errorf("already cancelled")
|
|
}
|
|
n, err := fw.bw.Write(p)
|
|
fw.size += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
func (fw *fileWriter) Size() int64 {
|
|
return fw.size
|
|
}
|
|
|
|
func (fw *fileWriter) Close() error {
|
|
if fw.closed {
|
|
return fmt.Errorf("already closed")
|
|
}
|
|
|
|
if err := fw.bw.Flush(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := fw.file.Sync(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := fw.file.Close(); err != nil {
|
|
return err
|
|
}
|
|
fw.closed = true
|
|
return nil
|
|
}
|
|
|
|
func (fw *fileWriter) Cancel(ctx context.Context) error {
|
|
if fw.closed {
|
|
return fmt.Errorf("already closed")
|
|
}
|
|
|
|
fw.cancelled = true
|
|
fw.file.Close()
|
|
return os.Remove(fw.file.Name())
|
|
}
|
|
|
|
func (fw *fileWriter) Commit(ctx context.Context) error {
|
|
if fw.closed {
|
|
return fmt.Errorf("already closed")
|
|
} else if fw.committed {
|
|
return fmt.Errorf("already committed")
|
|
} else if fw.cancelled {
|
|
return fmt.Errorf("already cancelled")
|
|
}
|
|
|
|
if err := fw.bw.Flush(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := fw.file.Sync(); err != nil {
|
|
return err
|
|
}
|
|
|
|
fw.committed = true
|
|
return nil
|
|
}
|