Skip to content

Custom repositories

A repository is the code behind one URI scheme. When you call storage.Reader(u), the storage package looks up the repository registered for u.Scheme() and asks it for a reader. By registering your own repository you can make, say, mem:// or s3:// URIs work with every function in the storage package.

What a repository implements

Every repository implements repository.Repository: Exists, Reader, CanRead, and Destroy, which is called when the repository is replaced. Extra abilities come from extra interfaces. The storage functions check for them and return repository.ErrOperationNotSupported when one is missing. storage.DeleteAll is the exception: without DeleteAllRepository it falls back to repository.GenericDeleteAll.

InterfaceMethodsEnables
WritableRepositoryWriter, CanWrite, Deletestorage.Writer, CanWrite, Delete
AppendableRepositoryAppenderstorage.Appender
ListableRepositoryCanList, List, CreateListablestorage.List, CanList, CreateListable
HierarchicalRepositoryParent, Childstorage.Parent, storage.Child
CopyableRepositoryCopystorage.Copy
MovableRepositoryMovestorage.Move
DeleteAllRepositoryDeleteAllA faster storage.DeleteAll
CustomURIRepositoryParseURIYour own parsing in storage.ParseURI
WatchableRepositoryWatchURIChange notifications, for example to refresh a file tree

The package also has helpers such as repository.GenericCopy, GenericMove, GenericParent, GenericChild and GenericDeleteAll, built from the basic operations, for repositories that have no faster way.

An in-memory repository

This repository keeps files in a map. It implements the required methods and WritableRepository:

// memRepo stores file contents in a map, keyed by the URI string.
type memRepo struct {
	mu    sync.Mutex
	files map[string][]byte
}

// The four methods every repository needs.

func (m *memRepo) Exists(u bellui.URI) (bool, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	_, ok := m.files[u.String()]
	return ok, nil
}

func (m *memRepo) Reader(u bellui.URI) (bellui.URIReadCloser, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	data, ok := m.files[u.String()]
	if !ok {
		return nil, errors.New("not found: " + u.String())
	}
	return &memReader{Reader: bytes.NewReader(data), uri: u}, nil
}

func (m *memRepo) CanRead(u bellui.URI) (bool, error) { return m.Exists(u) }

func (m *memRepo) Destroy(string) {}

// Writer, CanWrite and Delete make it a WritableRepository.

func (m *memRepo) Writer(u bellui.URI) (bellui.URIWriteCloser, error) {
	return &memWriter{repo: m, uri: u}, nil
}

func (m *memRepo) CanWrite(bellui.URI) (bool, error) { return true, nil }

func (m *memRepo) Delete(u bellui.URI) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.files, u.String())
	return nil
}

Register it once, before you use the scheme. Without a CustomURIRepository, storage.ParseURI parses the string as a standard URI.

repository.Register("mem", &memRepo{files: map[string][]byte{}})

u, err := storage.ParseURI("mem://notes/hello.txt")
if err != nil {
	panic(err)
}

From then on the ordinary storage functions work with mem:// URIs. Operations the repository does not offer fail cleanly:

wr, _ := storage.Writer(u)
_, _ = wr.Write([]byte("kept in memory"))
_ = wr.Close()

r, _ := storage.Reader(u)
data, _ := io.ReadAll(r)
_ = r.Close()

_, err = storage.List(u) // memRepo is not a ListableRepository
listErr := errors.Is(err, repository.ErrOperationNotSupported)

Things to know

  • Registering a scheme a second time replaces the earlier repository and calls its Destroy.
  • Scheme names are matched in lower case.
  • A storage function calls your repository on the caller’s goroutine. If your app uses storage from background goroutines, guard shared state, as the mutex does here.
  • A WatchableRepository callback can run on any goroutine. Use bellui.Do to touch the UI from it.

The complete example

// A custom storage repository that keeps "mem://" files in memory.
package main

import (
	"bytes"
	"errors"
	"io"
	"sync"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/storage"
	"github.com/belluikit/bellui/storage/repository"
	"github.com/belluikit/bellui/widget"
)

// memRepo stores file contents in a map, keyed by the URI string.
type memRepo struct {
	mu    sync.Mutex
	files map[string][]byte
}

// The four methods every repository needs.

func (m *memRepo) Exists(u bellui.URI) (bool, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	_, ok := m.files[u.String()]
	return ok, nil
}

func (m *memRepo) Reader(u bellui.URI) (bellui.URIReadCloser, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	data, ok := m.files[u.String()]
	if !ok {
		return nil, errors.New("not found: " + u.String())
	}
	return &memReader{Reader: bytes.NewReader(data), uri: u}, nil
}

func (m *memRepo) CanRead(u bellui.URI) (bool, error) { return m.Exists(u) }

func (m *memRepo) Destroy(string) {}

// Writer, CanWrite and Delete make it a WritableRepository.

func (m *memRepo) Writer(u bellui.URI) (bellui.URIWriteCloser, error) {
	return &memWriter{repo: m, uri: u}, nil
}

func (m *memRepo) CanWrite(bellui.URI) (bool, error) { return true, nil }

func (m *memRepo) Delete(u bellui.URI) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.files, u.String())
	return nil
}


type memReader struct {
	*bytes.Reader
	uri bellui.URI
}

func (r *memReader) Close() error     { return nil }
func (r *memReader) URI() bellui.URI { return r.uri }

// memWriter buffers what is written and stores it on Close.
type memWriter struct {
	bytes.Buffer
	repo *memRepo
	uri  bellui.URI
}

func (w *memWriter) Close() error {
	w.repo.mu.Lock()
	defer w.repo.mu.Unlock()
	w.repo.files[w.uri.String()] = w.Bytes()
	return nil
}

func (w *memWriter) URI() bellui.URI { return w.uri }

func main() {
	a := app.New()
	win := a.NewWindow("Custom repository")

	repository.Register("mem", &memRepo{files: map[string][]byte{}})

	u, err := storage.ParseURI("mem://notes/hello.txt")
	if err != nil {
		panic(err)
	}

	wr, _ := storage.Writer(u)
	_, _ = wr.Write([]byte("kept in memory"))
	_ = wr.Close()

	r, _ := storage.Reader(u)
	data, _ := io.ReadAll(r)
	_ = r.Close()

	_, err = storage.List(u) // memRepo is not a ListableRepository
	listErr := errors.Is(err, repository.ErrOperationNotSupported)

	msg := "Read back: " + string(data)
	if listErr {
		msg += "\nList: not supported, as expected"
	}
	win.SetContent(container.NewVBox(widget.NewLabel(msg)))
	bellui.ShowAndRun(win)
}