317 lines
8.3 KiB
Go
317 lines
8.3 KiB
Go
package fake
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"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
|
|
}
|
|
if entry, err := b.Stat(ctx, prefix); err == nil && entry.Type != storage.EntryTypeDirectory {
|
|
return emit(ctx, entry, opts, fn)
|
|
} else if err != nil && !storage.IsNotFound(err) {
|
|
return err
|
|
}
|
|
|
|
entries := b.entries()
|
|
visited := 0
|
|
for _, entry := range entries {
|
|
if entry.Path == "" || !entryBelow(prefix, entry.Path) {
|
|
continue
|
|
}
|
|
if !opts.Recursive && !isImmediateChild(prefix, entry.Path) {
|
|
continue
|
|
}
|
|
if opts.Limit > 0 && visited >= opts.Limit {
|
|
return nil
|
|
}
|
|
visited++
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := fn(entry); err != nil {
|
|
if errors.Is(err, storage.ErrStopWalk) {
|
|
return nil
|
|
}
|
|
return storage.NewError(storage.OpWalk, backendName, entry.Path, storage.ErrUnknown, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) HasAny(ctx context.Context, prefix string) (bool, error) {
|
|
found := false
|
|
err := b.Walk(ctx, prefix, storage.WalkOptions{Recursive: false, Limit: 1}, func(storage.Entry) error {
|
|
found = true
|
|
return storage.ErrStopWalk
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return found, nil
|
|
}
|
|
|
|
func (b *Backend) DeleteManagedBundle(ctx context.Context, bundlePath string, managedOutputPaths []string, opts storage.DeleteOptions) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := storage.ValidatePrefix(bundlePath); err != nil {
|
|
return err
|
|
}
|
|
targets := make([]string, 0, len(managedOutputPaths)+1)
|
|
for _, outputPath := range managedOutputPaths {
|
|
target, err := storage.Join(bundlePath, outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targets = append(targets, target)
|
|
}
|
|
statePath, err := storage.StatePath(bundlePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targets = append(targets, statePath)
|
|
|
|
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) 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 emit(ctx context.Context, entry storage.Entry, opts storage.WalkOptions, fn storage.WalkFunc) error {
|
|
if opts.Limit > 0 && opts.Limit < 1 {
|
|
return nil
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := fn(entry); err != nil && !errors.Is(err, storage.ErrStopWalk) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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]
|
|
}
|