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.
| Interface | Methods | Enables |
|---|---|---|
WritableRepository | Writer, CanWrite, Delete | storage.Writer, CanWrite, Delete |
AppendableRepository | Appender | storage.Appender |
ListableRepository | CanList, List, CreateListable | storage.List, CanList, CreateListable |
HierarchicalRepository | Parent, Child | storage.Parent, storage.Child |
CopyableRepository | Copy | storage.Copy |
MovableRepository | Move | storage.Move |
DeleteAllRepository | DeleteAll | A faster storage.DeleteAll |
CustomURIRepository | ParseURI | Your own parsing in storage.ParseURI |
WatchableRepository | WatchURI | Change 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
WatchableRepositorycallback can run on any goroutine. Usebellui.Doto 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)
}