Skip to content

Commit b598dce

Browse files
authored
Merge pull request #63 from fpetkovski/iter-object-attributes
Add support for object attributes in Iter call
2 parents 8648e8e + 2c4ff97 commit b598dce

20 files changed

+534
-59
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re
2525
- [#79](https://github.com/thanos-io/objstore/pull/79) Metrics: Fix `objstore_bucket_operation_duration_seconds` for `iter` operations.
2626

2727
### Added
28+
- [#63](https://github.com/thanos-io/objstore/pull/63) Implement a `IterWithAttributes` method on the bucket client.
2829
- [#15](https://github.com/thanos-io/objstore/pull/15) Add Oracle Cloud Infrastructure Object Storage Bucket support.
2930
- [#25](https://github.com/thanos-io/objstore/pull/25) S3: Support specifying S3 storage class.
3031
- [#32](https://github.com/thanos-io/objstore/pull/32) Swift: Support authentication using application credentials.

README.md

+19-4
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ See [MAINTAINERS.md](https://github.com/thanos-io/thanos/blob/main/MAINTAINERS.m
4848

4949
The core this module is the [`Bucket` interface](objstore.go):
5050

51-
```go mdox-exec="sed -n '37,50p' objstore.go"
51+
```go mdox-exec="sed -n '39,55p' objstore.go"
5252
// Bucket provides read and write access to an object storage bucket.
5353
// NOTE: We assume strong consistency for write-read flow.
5454
type Bucket interface {
@@ -63,18 +63,31 @@ type Bucket interface {
6363
// If object does not exist in the moment of deletion, Delete should throw error.
6464
Delete(ctx context.Context, name string) error
6565

66+
// Name returns the bucket name for the provider.
67+
Name() string
68+
}
6669
```
6770

6871
All [provider implementations](providers) have to implement `Bucket` interface that allows common read and write operations that all supported by all object providers. If you want to limit the code that will do bucket operation to only read access (smart idea, allowing to limit access permissions), you can use the [`BucketReader` interface](objstore.go):
6972

70-
```go mdox-exec="sed -n '68,93p' objstore.go"
71-
73+
```go mdox-exec="sed -n '71,106p' objstore.go"
7274
// BucketReader provides read access to an object storage bucket.
7375
type BucketReader interface {
7476
// Iter calls f for each entry in the given directory (not recursive.). The argument to f is the full
7577
// object name including the prefix of the inspected directory.
78+
7679
// Entries are passed to function in sorted order.
77-
Iter(ctx context.Context, dir string, f func(string) error, options ...IterOption) error
80+
Iter(ctx context.Context, dir string, f func(name string) error, options ...IterOption) error
81+
82+
// IterWithAttributes calls f for each entry in the given directory similar to Iter.
83+
// In addition to Name, it also includes requested object attributes in the argument to f.
84+
//
85+
// Attributes can be requested using IterOption.
86+
// Not all IterOptions are supported by all providers, requesting for an unsupported option will fail with ErrOptionNotSupported.
87+
IterWithAttributes(ctx context.Context, dir string, f func(attrs IterObjectAttributes) error, options ...IterOption) error
88+
89+
// SupportedIterOptions returns a list of supported IterOptions by the underlying provider.
90+
SupportedIterOptions() []IterOptionType
7891

7992
// Get returns a reader for the given object name.
8093
Get(ctx context.Context, name string) (io.ReadCloser, error)
@@ -374,6 +387,7 @@ config:
374387
server_name: ""
375388
insecure_skip_verify: false
376389
disable_compression: false
390+
chunk_size_bytes: 0
377391
prefix: ""
378392
```
379393

@@ -447,6 +461,7 @@ config:
447461
storage_account: ""
448462
storage_account_key: ""
449463
storage_connection_string: ""
464+
storage_create_container: false
450465
container: ""
451466
endpoint: ""
452467
user_assigned_id: ""

inmem.go

+14
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ func (b *InMemBucket) Iter(_ context.Context, dir string, f func(string) error,
106106
return nil
107107
}
108108

109+
func (i *InMemBucket) SupportedIterOptions() []IterOptionType {
110+
return []IterOptionType{Recursive}
111+
}
112+
113+
func (b *InMemBucket) IterWithAttributes(ctx context.Context, dir string, f func(attrs IterObjectAttributes) error, options ...IterOption) error {
114+
if err := ValidateIterOptions(b.SupportedIterOptions(), options...); err != nil {
115+
return err
116+
}
117+
118+
return b.Iter(ctx, dir, func(name string) error {
119+
return f(IterObjectAttributes{Name: name})
120+
}, options...)
121+
}
122+
109123
// Get returns a reader for the given object name.
110124
func (b *InMemBucket) Get(_ context.Context, name string) (io.ReadCloser, error) {
111125
if name == "" {

objstore.go

+100-9
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ package objstore
66
import (
77
"bytes"
88
"context"
9+
"fmt"
910
"io"
1011
"io/fs"
1112
"os"
1213
"path"
1314
"path/filepath"
15+
"slices"
1416
"strings"
1517
"sync"
1618
"time"
@@ -70,8 +72,19 @@ type InstrumentedBucket interface {
7072
type BucketReader interface {
7173
// Iter calls f for each entry in the given directory (not recursive.). The argument to f is the full
7274
// object name including the prefix of the inspected directory.
75+
7376
// Entries are passed to function in sorted order.
74-
Iter(ctx context.Context, dir string, f func(string) error, options ...IterOption) error
77+
Iter(ctx context.Context, dir string, f func(name string) error, options ...IterOption) error
78+
79+
// IterWithAttributes calls f for each entry in the given directory similar to Iter.
80+
// In addition to Name, it also includes requested object attributes in the argument to f.
81+
//
82+
// Attributes can be requested using IterOption.
83+
// Not all IterOptions are supported by all providers, requesting for an unsupported option will fail with ErrOptionNotSupported.
84+
IterWithAttributes(ctx context.Context, dir string, f func(attrs IterObjectAttributes) error, options ...IterOption) error
85+
86+
// SupportedIterOptions returns a list of supported IterOptions by the underlying provider.
87+
SupportedIterOptions() []IterOptionType
7588

7689
// Get returns a reader for the given object name.
7790
Get(ctx context.Context, name string) (io.ReadCloser, error)
@@ -101,24 +114,66 @@ type InstrumentedBucketReader interface {
101114
ReaderWithExpectedErrs(IsOpFailureExpectedFunc) BucketReader
102115
}
103116

117+
var ErrOptionNotSupported = errors.New("iter option is not supported")
118+
119+
// IterOptionType is used for type-safe option support checking.
120+
type IterOptionType int
121+
122+
const (
123+
Recursive IterOptionType = iota
124+
UpdatedAt
125+
)
126+
104127
// IterOption configures the provided params.
105-
type IterOption func(params *IterParams)
128+
type IterOption struct {
129+
Type IterOptionType
130+
Apply func(params *IterParams)
131+
}
106132

107133
// WithRecursiveIter is an option that can be applied to Iter() to recursively list objects
108134
// in the bucket.
109-
func WithRecursiveIter(params *IterParams) {
110-
params.Recursive = true
135+
func WithRecursiveIter() IterOption {
136+
return IterOption{
137+
Type: Recursive,
138+
Apply: func(params *IterParams) {
139+
params.Recursive = true
140+
},
141+
}
142+
}
143+
144+
// WithUpdatedAt is an option that can be applied to Iter() to
145+
// include the last modified time in the attributes.
146+
// NB: Prefixes may not report last modified time.
147+
// This option is currently supported for the azure, s3, bos, gcs and filesystem providers.
148+
func WithUpdatedAt() IterOption {
149+
return IterOption{
150+
Type: UpdatedAt,
151+
Apply: func(params *IterParams) {
152+
params.LastModified = true
153+
},
154+
}
111155
}
112156

113157
// IterParams holds the Iter() parameters and is used by objstore clients implementations.
114158
type IterParams struct {
115-
Recursive bool
159+
Recursive bool
160+
LastModified bool
161+
}
162+
163+
func ValidateIterOptions(supportedOptions []IterOptionType, options ...IterOption) error {
164+
for _, opt := range options {
165+
if !slices.Contains(supportedOptions, opt.Type) {
166+
return fmt.Errorf("%w: %v", ErrOptionNotSupported, opt.Type)
167+
}
168+
}
169+
170+
return nil
116171
}
117172

118173
func ApplyIterOptions(options ...IterOption) IterParams {
119174
out := IterParams{}
120175
for _, opt := range options {
121-
opt(&out)
176+
opt.Apply(&out)
122177
}
123178
return out
124179
}
@@ -189,6 +244,20 @@ type ObjectAttributes struct {
189244
LastModified time.Time `json:"last_modified"`
190245
}
191246

247+
type IterObjectAttributes struct {
248+
Name string
249+
lastModified time.Time
250+
}
251+
252+
func (i *IterObjectAttributes) SetLastModified(t time.Time) {
253+
i.lastModified = t
254+
}
255+
256+
// LastModified returns the timestamp the object was last modified. Returns false if the timestamp is not available.
257+
func (i *IterObjectAttributes) LastModified() (time.Time, bool) {
258+
return i.lastModified, !i.lastModified.IsZero()
259+
}
260+
192261
// TryToGetSize tries to get upfront size from reader.
193262
// Some implementations may return only size of unread data in the reader, so it's best to call this method before
194263
// doing any reading.
@@ -533,21 +602,43 @@ func (b *metricBucket) ReaderWithExpectedErrs(fn IsOpFailureExpectedFunc) Bucket
533602
return b.WithExpectedErrs(fn)
534603
}
535604

536-
func (b *metricBucket) Iter(ctx context.Context, dir string, f func(name string) error, options ...IterOption) error {
605+
func (b *metricBucket) Iter(ctx context.Context, dir string, f func(string) error, options ...IterOption) error {
537606
const op = OpIter
538607
b.metrics.ops.WithLabelValues(op).Inc()
539608

540-
start := time.Now()
609+
timer := prometheus.NewTimer(b.metrics.opsDuration.WithLabelValues(op))
610+
defer timer.ObserveDuration()
611+
541612
err := b.bkt.Iter(ctx, dir, f, options...)
542613
if err != nil {
543614
if !b.metrics.isOpFailureExpected(err) && ctx.Err() != context.Canceled {
544615
b.metrics.opsFailures.WithLabelValues(op).Inc()
545616
}
546617
}
547-
b.metrics.opsDuration.WithLabelValues(op).Observe(time.Since(start).Seconds())
548618
return err
549619
}
550620

621+
func (b *metricBucket) IterWithAttributes(ctx context.Context, dir string, f func(IterObjectAttributes) error, options ...IterOption) error {
622+
const op = OpIter
623+
b.metrics.ops.WithLabelValues(op).Inc()
624+
625+
timer := prometheus.NewTimer(b.metrics.opsDuration.WithLabelValues(op))
626+
defer timer.ObserveDuration()
627+
628+
err := b.bkt.IterWithAttributes(ctx, dir, f, options...)
629+
if err != nil {
630+
if !b.metrics.isOpFailureExpected(err) && ctx.Err() != context.Canceled {
631+
b.metrics.opsFailures.WithLabelValues(op).Inc()
632+
}
633+
}
634+
635+
return err
636+
}
637+
638+
func (b *metricBucket) SupportedIterOptions() []IterOptionType {
639+
return b.bkt.SupportedIterOptions()
640+
}
641+
551642
func (b *metricBucket) Attributes(ctx context.Context, name string) (ObjectAttributes, error) {
552643
const op = OpAttributes
553644
b.metrics.ops.WithLabelValues(op).Inc()

prefixed_bucket.go

+13
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ func (p *PrefixedBucket) Iter(ctx context.Context, dir string, f func(string) er
5454
}, options...)
5555
}
5656

57+
func (p *PrefixedBucket) IterWithAttributes(ctx context.Context, dir string, f func(IterObjectAttributes) error, options ...IterOption) error {
58+
pdir := withPrefix(p.prefix, dir)
59+
60+
return p.bkt.IterWithAttributes(ctx, pdir, func(attrs IterObjectAttributes) error {
61+
attrs.Name = strings.TrimPrefix(attrs.Name, p.prefix+DirDelim)
62+
return f(attrs)
63+
}, options...)
64+
}
65+
66+
func (p *PrefixedBucket) SupportedIterOptions() []IterOptionType {
67+
return p.bkt.SupportedIterOptions()
68+
}
69+
5770
// Get returns a reader for the given object name.
5871
func (p *PrefixedBucket) Get(ctx context.Context, name string) (io.ReadCloser, error) {
5972
return p.bkt.Get(ctx, conditionalPrefix(p.prefix, name))

prefixed_bucket_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func UsesPrefixTest(t *testing.T, bkt Bucket, prefix string) {
7474
testutil.Ok(t, pBkt.Iter(context.Background(), "", func(fn string) error {
7575
seen = append(seen, fn)
7676
return nil
77-
}, WithRecursiveIter))
77+
}, WithRecursiveIter()))
7878
expected := []string{"dir/file1.jpg", "file1.jpg"}
7979
sort.Strings(expected)
8080
sort.Strings(seen)

providers/azure/azure.go

+41-6
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,15 @@ func NewBucketWithConfig(logger log.Logger, conf Config, component string, wrapR
193193
return bkt, nil
194194
}
195195

196-
// Iter calls f for each entry in the given directory. The argument to f is the full
197-
// object name including the prefix of the inspected directory.
198-
func (b *Bucket) Iter(ctx context.Context, dir string, f func(string) error, options ...objstore.IterOption) error {
196+
func (b *Bucket) SupportedIterOptions() []objstore.IterOptionType {
197+
return []objstore.IterOptionType{objstore.Recursive, objstore.UpdatedAt}
198+
}
199+
200+
func (b *Bucket) IterWithAttributes(ctx context.Context, dir string, f func(attrs objstore.IterObjectAttributes) error, options ...objstore.IterOption) error {
201+
if err := objstore.ValidateIterOptions(b.SupportedIterOptions(), options...); err != nil {
202+
return err
203+
}
204+
199205
prefix := dir
200206
if prefix != "" && !strings.HasSuffix(prefix, DirDelim) {
201207
prefix += DirDelim
@@ -211,7 +217,13 @@ func (b *Bucket) Iter(ctx context.Context, dir string, f func(string) error, opt
211217
return err
212218
}
213219
for _, blob := range resp.Segment.BlobItems {
214-
if err := f(*blob.Name); err != nil {
220+
attrs := objstore.IterObjectAttributes{
221+
Name: *blob.Name,
222+
}
223+
if params.LastModified {
224+
attrs.SetLastModified(*blob.Properties.LastModified)
225+
}
226+
if err := f(attrs); err != nil {
215227
return err
216228
}
217229
}
@@ -227,19 +239,42 @@ func (b *Bucket) Iter(ctx context.Context, dir string, f func(string) error, opt
227239
return err
228240
}
229241
for _, blobItem := range resp.Segment.BlobItems {
230-
if err := f(*blobItem.Name); err != nil {
242+
attrs := objstore.IterObjectAttributes{
243+
Name: *blobItem.Name,
244+
}
245+
if params.LastModified {
246+
attrs.SetLastModified(*blobItem.Properties.LastModified)
247+
}
248+
if err := f(attrs); err != nil {
231249
return err
232250
}
233251
}
234252
for _, blobPrefix := range resp.Segment.BlobPrefixes {
235-
if err := f(*blobPrefix.Name); err != nil {
253+
if err := f(objstore.IterObjectAttributes{Name: *blobPrefix.Name}); err != nil {
236254
return err
237255
}
238256
}
239257
}
240258
return nil
241259
}
242260

261+
// Iter calls f for each entry in the given directory. The argument to f is the full
262+
// object name including the prefix of the inspected directory.
263+
func (b *Bucket) Iter(ctx context.Context, dir string, f func(string) error, opts ...objstore.IterOption) error {
264+
// Only include recursive option since attributes are not used in this method.
265+
var filteredOpts []objstore.IterOption
266+
for _, opt := range opts {
267+
if opt.Type == objstore.Recursive {
268+
filteredOpts = append(filteredOpts, opt)
269+
break
270+
}
271+
}
272+
273+
return b.IterWithAttributes(ctx, dir, func(attrs objstore.IterObjectAttributes) error {
274+
return f(attrs.Name)
275+
}, filteredOpts...)
276+
}
277+
243278
// IsObjNotFoundErr returns true if error means that object is not found. Relevant to Get operations.
244279
func (b *Bucket) IsObjNotFoundErr(err error) bool {
245280
if err == nil {

0 commit comments

Comments
 (0)