From 29d49d464856a285743fd5268ea8ea7af6c65b38 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 26 Jun 2020 12:54:50 -0700 Subject: [PATCH 1/2] Add support for a nested config --- stow.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/stow.go b/stow.go index 2f02e2b5..84174513 100644 --- a/stow.go +++ b/stow.go @@ -149,8 +149,14 @@ type Config interface { // Config gets a string configuration value and a // bool indicating whether the value was present or not. Config(name string) (string, bool) + // NestedConfig gets a key/value configuration and a + // bool indicating whehter the value was present or not. + NestedConfig(name string) (Config, bool) // Set sets the configuration name to specified value Set(name, value string) + // SetNestedConfig sets the configuration name to the + // specified key/value configuration. + SetNestedConfig(name string, value Config) } // Register adds a Location implementation, with two helper functions. @@ -221,12 +227,19 @@ func KindByURL(u *url.URL) (string, error) { // ConfigMap is a map[string]string that implements // the Config method. -type ConfigMap map[string]string +type ConfigMap map[string]interface{} // Config gets a string configuration value and a // bool indicating whether the value was present or not. func (c ConfigMap) Config(name string) (string, bool) { - val, ok := c[name] + val, ok := c[name].(string) + return val, ok +} + +// NestedConfig gets a key/value configuration and a +// bool indicating whehter the value was present or not. +func (c ConfigMap) NestedConfig(name string) (Config, bool) { + val, ok := c[name].(Config) return val, ok } @@ -235,6 +248,12 @@ func (c ConfigMap) Set(name, value string) { c[name] = value } +// SetNestedConfig sets the configuration name to the +// specified key/value configuration. +func (c ConfigMap) SetNestedConfig(name string, value Config) { + c[name] = value +} + // errUnknownKind indicates that a kind is unknown. type errUnknownKind string From 1e05aab1cab151a9e549c1d7351f2d5ab9d78803 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 26 Jun 2020 12:55:21 -0700 Subject: [PATCH 2/2] Add readonly backend --- readonly/config.go | 54 ++++++++++++++++++++++++++++++++ readonly/container.go | 66 ++++++++++++++++++++++++++++++++++++++ readonly/item.go | 61 ++++++++++++++++++++++++++++++++++++ readonly/location.go | 73 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 readonly/config.go create mode 100644 readonly/container.go create mode 100644 readonly/item.go create mode 100644 readonly/location.go diff --git a/readonly/config.go b/readonly/config.go new file mode 100644 index 00000000..fe8fbfba --- /dev/null +++ b/readonly/config.go @@ -0,0 +1,54 @@ +package readonly + +import ( + "errors" + "net/url" + + "github.com/graymeta/stow" +) + +const ( + // ConfigWrappedKind is a key whose value is the type of backend being wrapped. + ConfigWrappedKind = "wrapped_kind" + // ConfigWrapped is the key whose value is the config for the backend being wrapped. + ConfigWrapped = "wrapped" +) + +// Kind is the kid of Location this package provides +const Kind = "readonly" + +func init() { + validatefn := func(config stow.Config) error { + wrappedKind, ok := config.Config(ConfigWrappedKind) + if !ok || wrappedKind == "" { + return errors.New("missing wrapped kind") + } + wrappedConfig, ok := config.NestedConfig(ConfigWrapped) + if !ok { + return errors.New("missing config for wrapped") + } + return stow.Validate(wrappedKind, wrappedConfig) + } + makefn := func(config stow.Config) (stow.Location, error) { + wrappedKind, ok := config.Config(ConfigWrappedKind) + if !ok || wrappedKind == "" { + return nil, errors.New("missing wrapped kind") + } + wrappedConfig, ok := config.NestedConfig(ConfigWrapped) + if !ok { + return nil, errors.New("missing config for wrapped") + } + + w, err := stow.Dial(wrappedKind, wrappedConfig) + if err != nil { + return nil, err + } + + return &location{wrapped: w}, nil + } + kindfn := func(u *url.URL) bool { + return u.Scheme == Kind + } + + stow.Register(Kind, makefn, kindfn, validatefn) +} diff --git a/readonly/container.go b/readonly/container.go new file mode 100644 index 00000000..03278ceb --- /dev/null +++ b/readonly/container.go @@ -0,0 +1,66 @@ +package readonly + +import ( + "errors" + "io" + + "github.com/graymeta/stow" +) + +type container struct { + wrapped stow.Container +} + +// ID gets a unique string describing this Container. +func (c *container) ID() string { + return c.wrapped.ID() +} + +// Name gets a human-readable name describing this Container. +func (c *container) Name() string { + return c.wrapped.Name() +} + +// Item gets an item by its ID. +func (c *container) Item(id string) (stow.Item, error) { + it, err := c.wrapped.Item(id) + if err != nil { + return nil, err + } + + return &item{wrapped: it}, nil +} + +// Items gets a page of items with the specified +// prefix for this Container. +// The specified cursor is a pointer to the start of +// the items to get. It it obtained from a previous +// call to this method, or should be CursorStart for the +// first page. +// count is the number of items to return per page. +// The returned cursor can be checked with IsCursorEnd to +// decide if there are any more items or not. +func (c *container) Items(prefix, cursor string, count int) ([]stow.Item, string, error) { + its, startAfter, err := c.wrapped.Items(prefix, cursor, count) + if err != nil { + return nil, "", err + } + + wrapped := make([]stow.Item, len(its)) + for i, val := range its { + wrapped[i] = &item{wrapped: val} + } + + return wrapped, startAfter, nil +} + +// RemoveItem removes the Item with the specified ID. +func (c *container) RemoveItem(id string) error { + return errors.New("readonly") +} + +// Put creates a new Item with the specified name, and contents +// read from the reader. +func (c *container) Put(name string, r io.Reader, size int64, metadata map[string]interface{}) (stow.Item, error) { + return nil, errors.New("readonly") +} diff --git a/readonly/item.go b/readonly/item.go new file mode 100644 index 00000000..b304c56e --- /dev/null +++ b/readonly/item.go @@ -0,0 +1,61 @@ +package readonly + +import ( + "io" + "net/url" + "time" + + "github.com/graymeta/stow" +) + +type item struct { + wrapped stow.Item +} + +// ID gets a unique string describing this Item. +func (i *item) ID() string { + return i.wrapped.ID() +} + +// Name gets a human-readable name describing this Item. +func (i *item) Name() string { + return i.wrapped.Name() +} + +// URL gets a URL for this item. +// For example: +// local: file:///path/to/something +// azure: azure://host:port/api/something +// s3: s3://host:post/etc +func (i *item) URL() *url.URL { + return i.wrapped.URL() +} + +// Size gets the size of the Item's contents in bytes. +func (i *item) Size() (int64, error) { + return i.wrapped.Size() +} + +// Open opens the Item for reading. +// Calling code must close the io.ReadCloser. +func (i *item) Open() (io.ReadCloser, error) { + return i.wrapped.Open() +} + +// ETag is a string that is different when the Item is +// different, and the same when the item is the same. +// Usually this is the last modified datetime. +func (i *item) ETag() (string, error) { + return i.wrapped.ETag() +} + +// LastMod returns the last modified date of the file. +func (i *item) LastMod() (time.Time, error) { + return i.wrapped.LastMod() +} + +// Metadata gets a map of key/values that belong +// to this Item. +func (i *item) Metadata() (map[string]interface{}, error) { + return i.wrapped.Metadata() +} diff --git a/readonly/location.go b/readonly/location.go new file mode 100644 index 00000000..7d063500 --- /dev/null +++ b/readonly/location.go @@ -0,0 +1,73 @@ +package readonly + +import ( + "errors" + "net/url" + + "github.com/graymeta/stow" +) + +type location struct { + wrapped stow.Location +} + +// Closes the underlying location. +func (l *location) Close() error { + return l.wrapped.Close() +} + +// CreateContainer creates a new Container with the +// specified name. +func (l *location) CreateContainer(name string) (stow.Container, error) { + return nil, errors.New("readonly") +} + +// Containers gets a page of containers +// with the specified prefix from this Location. +// The specified cursor is a pointer to the start of +// the containers to get. It it obtained from a previous +// call to this method, or should be CursorStart for the +// first page. +// count is the number of items to return per page. +// The returned cursor can be checked with IsCursorEnd to +// decide if there are any more items or not. +func (l *location) Containers(prefix string, cursor string, count int) ([]stow.Container, string, error) { + cs, startAfter, err := l.wrapped.Containers(prefix, cursor, count) + if err != nil { + return nil, "", err + } + + wrapped := make([]stow.Container, len(cs)) + for i, val := range cs { + wrapped[i] = &container{wrapped: val} + } + + return wrapped, startAfter, nil +} + +// Container gets the Container with the specified +// identifier. +func (l *location) Container(id string) (stow.Container, error) { + c, err := l.wrapped.Container(id) + if err != nil { + return nil, err + } + + return &container{wrapped: c}, nil +} + +// RemoveContainer removes the container with the specified ID. +func (l *location) RemoveContainer(id string) error { + return errors.New("readonly") +} + +// ItemByURL gets an Item at this location with the +// specified URL. +func (l *location) ItemByURL(url *url.URL) (stow.Item, error) { + i, err := l.wrapped.ItemByURL(url) + if err != nil { + return nil, err + } + + return &item{wrapped: i}, nil +}