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 } 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 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] }