Files
distributor/internal/storage/fake/backend.go

308 lines
8.2 KiB
Go

package fake
import (
"bytes"
"context"
"io"
"sort"
"strings"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
)
const backendName = "fake"
type Backend struct {
files map[string][]byte
dirs map[string]struct{}
symlinks map[string]struct{}
}
func New() *Backend {
return &Backend{
files: make(map[string][]byte),
dirs: map[string]struct{}{"": {}},
symlinks: make(map[string]struct{}),
}
}
func (b *Backend) AddDirectory(path string) error {
if err := storage.ValidatePrefix(path); err != nil {
return err
}
b.ensureParents(path)
b.dirs[path] = struct{}{}
return nil
}
func (b *Backend) AddSymlink(path string) error {
if err := storage.ValidatePath(path); err != nil {
return err
}
b.ensureParents(path)
delete(b.files, path)
delete(b.dirs, path)
b.symlinks[path] = struct{}{}
return nil
}
func (b *Backend) ReadFile(ctx context.Context, path string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if err := storage.ValidatePath(path); err != nil {
return nil, err
}
data, ok := b.files[path]
if !ok {
if b.exists(path) {
return nil, storage.NewError(storage.OpReadFile, backendName, path, storage.ErrUnsupported, nil)
}
return nil, storage.NewError(storage.OpReadFile, backendName, path, storage.ErrNotFound, nil)
}
return append([]byte(nil), data...), nil
}
func (b *Backend) OpenReader(ctx context.Context, path string) (io.ReadCloser, error) {
data, err := b.ReadFile(ctx, path)
if err != nil {
return nil, err
}
return io.NopCloser(bytes.NewReader(data)), nil
}
func (b *Backend) WriteFile(ctx context.Context, path string, data []byte, opts storage.WriteOptions) (storage.Entry, error) {
opts.Size = int64(len(data))
opts.SizeKnown = true
return b.WriteFrom(ctx, path, bytes.NewReader(data), opts)
}
func (b *Backend) WriteFrom(ctx context.Context, path string, r io.Reader, opts storage.WriteOptions) (storage.Entry, error) {
if err := ctx.Err(); err != nil {
return storage.Entry{}, err
}
if err := storage.ValidatePath(path); err != nil {
return storage.Entry{}, err
}
if b.exists(path) && !opts.Overwrite {
return storage.Entry{}, storage.NewError(storage.OpWriteFrom, backendName, path, storage.ErrAlreadyExist, nil)
}
if _, ok := b.dirs[path]; ok {
return storage.Entry{}, storage.NewError(storage.OpWriteFrom, backendName, path, storage.ErrConflict, nil)
}
if _, ok := b.symlinks[path]; ok {
return storage.Entry{}, storage.NewError(storage.OpWriteFrom, backendName, path, storage.ErrConflict, nil)
}
data, err := io.ReadAll(r)
if err != nil {
return storage.Entry{}, storage.NewError(storage.OpWriteFrom, backendName, path, storage.ErrUnknown, err)
}
if opts.SizeKnown && int64(len(data)) != opts.Size {
return storage.Entry{}, storage.NewError(storage.OpWriteFrom, backendName, path, storage.ErrConflict, nil)
}
b.ensureParents(path)
b.files[path] = append([]byte(nil), data...)
delete(b.symlinks, path)
return storage.Entry{Path: path, Type: storage.EntryTypeFile, Size: int64(len(data))}, nil
}
func (b *Backend) Stat(ctx context.Context, path string) (storage.Entry, error) {
if err := ctx.Err(); err != nil {
return storage.Entry{}, err
}
if err := storage.ValidatePrefix(path); err != nil {
return storage.Entry{}, err
}
if data, ok := b.files[path]; ok {
return storage.Entry{Path: path, Type: storage.EntryTypeFile, Size: int64(len(data))}, nil
}
if _, ok := b.symlinks[path]; ok {
return storage.Entry{Path: path, Type: storage.EntryTypeSymlink}, nil
}
if _, ok := b.dirs[path]; ok {
return storage.Entry{Path: path, Type: storage.EntryTypeDirectory}, nil
}
return storage.Entry{}, storage.NewError(storage.OpStat, backendName, path, storage.ErrNotFound, nil)
}
func (b *Backend) Walk(ctx context.Context, prefix string, opts storage.WalkOptions, fn storage.WalkFunc) error {
if err := ctx.Err(); err != nil {
return err
}
if err := storage.ValidatePrefix(prefix); err != nil {
return err
}
emitter := storage.NewWalkEmitter(ctx, backendName, opts, fn)
if entry, err := b.Stat(ctx, prefix); err == nil && entry.Type != storage.EntryTypeDirectory {
return storage.FinishWalk(emitter.Emit(entry))
} else if err != nil && !storage.IsNotFound(err) {
return err
}
entries := b.entries()
for _, entry := range entries {
if entry.Path == "" || !entryBelow(prefix, entry.Path) {
continue
}
if !opts.Recursive && !isImmediateChild(prefix, entry.Path) {
continue
}
if err := emitter.Emit(entry); err != nil {
return storage.FinishWalk(err)
}
}
return nil
}
func (b *Backend) HasAny(ctx context.Context, prefix string) (bool, error) {
return storage.HasAny(ctx, b, prefix)
}
func (b *Backend) DeleteManagedBundle(ctx context.Context, bundlePath string, managedOutputPaths []string, opts storage.DeleteOptions) error {
if err := ctx.Err(); err != nil {
return err
}
targets, err := storage.ManagedBundleTargets(bundlePath, managedOutputPaths)
if err != nil {
return err
}
for _, target := range targets {
if _, ok := b.dirs[target]; ok {
return storage.NewError(storage.OpDeleteManagedBundle, backendName, target, storage.ErrUnsupported, nil)
}
if !b.exists(target) {
if opts.IgnoreMissing {
continue
}
return storage.NewError(storage.OpDeleteManagedBundle, backendName, target, storage.ErrNotFound, nil)
}
delete(b.files, target)
delete(b.symlinks, target)
if opts.PruneEmptyDirs {
b.pruneEmptyParents(parentOf(target))
}
}
return nil
}
func (b *Backend) DeletePrefix(ctx context.Context, prefix string, opts storage.DeleteOptions) error {
if err := ctx.Err(); err != nil {
return err
}
if err := storage.ValidatePrefix(prefix); err != nil {
return err
}
if prefix != "" && !b.exists(prefix) && !b.hasChild(prefix) {
if opts.IgnoreMissing {
return nil
}
return storage.NewError(storage.OpDeletePrefix, backendName, prefix, storage.ErrNotFound, nil)
}
for path := range b.files {
if path == prefix || entryBelow(prefix, path) {
delete(b.files, path)
}
}
for path := range b.symlinks {
if path == prefix || entryBelow(prefix, path) {
delete(b.symlinks, path)
}
}
for path := range b.dirs {
if path != "" && (path == prefix || entryBelow(prefix, path)) {
delete(b.dirs, path)
}
}
if opts.PruneEmptyDirs {
b.pruneEmptyParents(parentOf(prefix))
}
b.dirs[""] = struct{}{}
return nil
}
func (b *Backend) ensureParents(path string) {
parent := parentOf(path)
for parent != "" {
b.dirs[parent] = struct{}{}
parent = parentOf(parent)
}
b.dirs[""] = struct{}{}
}
func (b *Backend) pruneEmptyParents(path string) {
for path != "" {
if b.hasChild(path) {
return
}
delete(b.dirs, path)
path = parentOf(path)
}
}
func (b *Backend) hasChild(path string) bool {
for candidate := range b.files {
if entryBelow(path, candidate) {
return true
}
}
for candidate := range b.symlinks {
if entryBelow(path, candidate) {
return true
}
}
for candidate := range b.dirs {
if candidate != path && entryBelow(path, candidate) {
return true
}
}
return false
}
func (b *Backend) exists(path string) bool {
_, file := b.files[path]
_, dir := b.dirs[path]
_, symlink := b.symlinks[path]
return file || dir || symlink
}
func (b *Backend) entries() []storage.Entry {
entries := make([]storage.Entry, 0, len(b.files)+len(b.dirs)+len(b.symlinks))
for path, data := range b.files {
entries = append(entries, storage.Entry{Path: path, Type: storage.EntryTypeFile, Size: int64(len(data))})
}
for path := range b.dirs {
entries = append(entries, storage.Entry{Path: path, Type: storage.EntryTypeDirectory})
}
for path := range b.symlinks {
entries = append(entries, storage.Entry{Path: path, Type: storage.EntryTypeSymlink})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Path < entries[j].Path
})
return entries
}
func entryBelow(prefix, path string) bool {
if prefix == "" {
return path != ""
}
return strings.HasPrefix(path, prefix+"/")
}
func isImmediateChild(prefix, path string) bool {
remainder := path
if prefix != "" {
remainder = strings.TrimPrefix(path, prefix+"/")
}
return !strings.Contains(remainder, "/")
}
func parentOf(path string) string {
index := strings.LastIndex(path, "/")
if index == -1 {
return ""
}
return path[:index]
}