diff --git a/b2/replica_client.go b/b2/replica_client.go new file mode 100644 index 00000000..db1cd0d0 --- /dev/null +++ b/b2/replica_client.go @@ -0,0 +1,575 @@ +package b2 + +import ( + "context" + "fmt" + "io" + "strings" + "sync" + "time" + + "github.com/Backblaze/blazer/b2" + "github.com/benbjohnson/litestream" + "github.com/benbjohnson/litestream/internal" + "golang.org/x/sync/errgroup" +) + +// ReplicaClientType is the client type for this package. +const ReplicaClientType = "b2" + +var _ litestream.ReplicaClient = (*ReplicaClient)(nil) + +// ReplicaClient is a client for writing snapshots & WAL segments to disk. +type ReplicaClient struct { + mu sync.Mutex + BucketClient *b2.Bucket + + // B2 authentication keys. + ApplicationKeyID string + ApplicationKey string + + // B2 bucket information. + Bucket string + Path string + + // Upload options. + ConcurrentUploads int + ChunkSize int + UseFileBuffer bool + FileBufferDir string +} + +// NewReplicaClient returns a new instance of ReplicaClient. +func NewReplicaClient() *ReplicaClient { + return &ReplicaClient{} +} + +// Type returns "s3" as the client type. +func (c *ReplicaClient) Type() string { + return ReplicaClientType +} + +// Init initializes the connection to S3. No-op if already initialized. +func (c *ReplicaClient) Init(ctx context.Context) (err error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.BucketClient != nil { + return nil + } + + client, err := b2.NewClient(ctx, c.ApplicationKeyID, c.ApplicationKey) + if err != nil { + return err + } + + c.BucketClient, err = client.Bucket(ctx, c.Bucket) + return err +} + +// Generations returns a list of available generation names. +func (c *ReplicaClient) Generations(ctx context.Context) ([]string, error) { + if err := c.Init(ctx); err != nil { + return nil, err + } + + prefix := litestream.GenerationsPath(c.Path) + "/" + var generations []string + iter := c.BucketClient.List(ctx, b2.ListPrefix(prefix), b2.ListDelimiter("/")) + for iter.Next() { + name := strings.TrimSuffix(strings.TrimPrefix(iter.Object().Name(), prefix), "/") + if !litestream.IsGenerationName(name) { + continue + } + generations = append(generations, name) + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "LIST").Inc() + + if err := iter.Err(); err != nil { + return nil, err + } + + return generations, nil +} + +// DeleteGeneration deletes all snapshots & WAL segments within a generation. +func (c *ReplicaClient) DeleteGeneration(ctx context.Context, generation string) error { + if err := c.Init(ctx); err != nil { + return err + } + + dir, err := litestream.GenerationPath(c.Path, generation) + if err != nil { + return fmt.Errorf("cannot determine generation path: %w", err) + } + + // Collect all files for the generation. + var objs []*b2.Object + iter := c.BucketClient.List(ctx, b2.ListPrefix(dir+"/"), b2.ListDelimiter("")) + for iter.Next() { + objs = append(objs, iter.Object()) + } + + if err := iter.Err(); err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "LIST").Inc() + + // Delete all files. + for _, obj := range objs { + err := obj.Delete(ctx) + if b2.IsNotExist(err) { + continue + } else if err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc() + } + + return nil +} + +// Snapshots returns an iterator over all available snapshots for a generation. +func (c *ReplicaClient) Snapshots(ctx context.Context, generation string) (litestream.SnapshotIterator, error) { + if err := c.Init(ctx); err != nil { + return nil, err + } + return newSnapshotIterator(ctx, c, generation), nil +} + +// WriteSnapshot writes LZ4 compressed data from rd into a file on disk. +func (c *ReplicaClient) WriteSnapshot(ctx context.Context, generation string, index int, rd io.Reader) (info litestream.SnapshotInfo, err error) { + if err := c.Init(ctx); err != nil { + return info, err + } + + key, err := litestream.SnapshotPath(c.Path, generation, index) + if err != nil { + return info, fmt.Errorf("cannot determine snapshot path: %w", err) + } + startTime := time.Now() + + rc := internal.NewReadCounter(rd) + w := c.BucketClient.Object(key).NewWriter(ctx) + w.ConcurrentUploads = c.ConcurrentUploads + w.ChunkSize = c.ChunkSize + w.UseFileBuffer = c.UseFileBuffer + w.FileBufferDir = c.FileBufferDir + + if _, err := io.Copy(w, rc); err != nil { + w.Close() + return info, err + } + + if err := w.Close(); err != nil { + return info, err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "PUT").Inc() + internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "PUT").Add(float64(rc.N())) + + return litestream.SnapshotInfo{ + Generation: generation, + Index: index, + Size: rc.N(), + CreatedAt: startTime.UTC(), + }, nil +} + +// SnapshotReader returns a reader for snapshot data at the given generation/index. +func (c *ReplicaClient) SnapshotReader(ctx context.Context, generation string, index int) (io.ReadCloser, error) { + if err := c.Init(ctx); err != nil { + return nil, err + } + + key, err := litestream.SnapshotPath(c.Path, generation, index) + if err != nil { + return nil, fmt.Errorf("cannot determine snapshot path: %w", err) + } + + obj := c.BucketClient.Object(key) + r := obj.NewReader(ctx) + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "GET").Inc() + //internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "GET").Add(float64(*out.ContentLength)) + + return r, nil +} + +// DeleteSnapshot deletes a snapshot with the given generation & index. +func (c *ReplicaClient) DeleteSnapshot(ctx context.Context, generation string, index int) error { + if err := c.Init(ctx); err != nil { + return err + } + + key, err := litestream.SnapshotPath(c.Path, generation, index) + if err != nil { + return fmt.Errorf("cannot determine snapshot path: %w", err) + } + + err = c.BucketClient.Object(key).Delete(ctx) + if b2.IsNotExist(err) { + return nil + } else if err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc() + return nil +} + +// WALSegments returns an iterator over all available WAL files for a generation. +func (c *ReplicaClient) WALSegments(ctx context.Context, generation string) (litestream.WALSegmentIterator, error) { + if err := c.Init(ctx); err != nil { + return nil, err + } + return newWALSegmentIterator(ctx, c, generation), nil +} + +// WriteWALSegment writes LZ4 compressed data from rd into a file on disk. +func (c *ReplicaClient) WriteWALSegment(ctx context.Context, pos litestream.Pos, rd io.Reader) (info litestream.WALSegmentInfo, err error) { + if err := c.Init(ctx); err != nil { + return info, err + } + + key, err := litestream.WALSegmentPath(c.Path, pos.Generation, pos.Index, pos.Offset) + if err != nil { + return info, fmt.Errorf("cannot determine wal segment path: %w", err) + } + startTime := time.Now() + + rc := internal.NewReadCounter(rd) + w := c.BucketClient.Object(key).NewWriter(ctx) + w.ConcurrentUploads = c.ConcurrentUploads + w.ChunkSize = c.ChunkSize + w.UseFileBuffer = c.UseFileBuffer + w.FileBufferDir = c.FileBufferDir + + if _, err := io.Copy(w, rc); err != nil { + w.Close() + return info, err + } + + if err := w.Close(); err != nil { + return info, err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "PUT").Inc() + internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "PUT").Add(float64(rc.N())) + + return litestream.WALSegmentInfo{ + Generation: pos.Generation, + Index: pos.Index, + Offset: pos.Offset, + Size: rc.N(), + CreatedAt: startTime.UTC(), + }, nil +} + +// WALSegmentReader returns a reader for a section of WAL data at the given index. +// Returns os.ErrNotExist if no matching index/offset is found. +func (c *ReplicaClient) WALSegmentReader(ctx context.Context, pos litestream.Pos) (io.ReadCloser, error) { + if err := c.Init(ctx); err != nil { + return nil, err + } + + key, err := litestream.WALSegmentPath(c.Path, pos.Generation, pos.Index, pos.Offset) + if err != nil { + return nil, fmt.Errorf("cannot determine wal segment path: %w", err) + } + + obj := c.BucketClient.Object(key) + r := obj.NewReader(ctx) + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "GET").Inc() + //internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "GET").Add(float64(*out.ContentLength)) + + return r, nil +} + +// DeleteWALSegments deletes WAL segments with at the given positions. +func (c *ReplicaClient) DeleteWALSegments(ctx context.Context, a []litestream.Pos) error { + if err := c.Init(ctx); err != nil { + return err + } + + for _, pos := range a { + key, err := litestream.WALSegmentPath(c.Path, pos.Generation, pos.Index, pos.Offset) + if err != nil { + return fmt.Errorf("cannot determine wal segment path: %w", err) + } + + err = c.BucketClient.Object(key).Delete(ctx) + if b2.IsNotExist(err) { + continue + } else if err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc() + } + + return nil +} + +// DeleteAll deletes everything on the remote path. Mainly used for testing. +func (c *ReplicaClient) DeleteAll(ctx context.Context) error { + if err := c.Init(ctx); err != nil { + return err + } + + prefix := c.Path + if prefix != "" { + prefix += "/" + } + + // Collect all files for the generation. + var objs []*b2.Object + iter := c.BucketClient.List(ctx, b2.ListPrefix(prefix), b2.ListDelimiter("")) + for iter.Next() { + objs = append(objs, iter.Object()) + } + + if err := iter.Err(); err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "LIST").Inc() + + // Delete all files. + for _, obj := range objs { + err := obj.Delete(ctx) + if b2.IsNotExist(err) { + continue + } else if err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc() + } + + return nil +} + +type snapshotIterator struct { + client *ReplicaClient + generation string + + ch chan litestream.SnapshotInfo + g errgroup.Group + ctx context.Context + cancel func() + + info litestream.SnapshotInfo + err error +} + +func newSnapshotIterator(ctx context.Context, client *ReplicaClient, generation string) *snapshotIterator { + itr := &snapshotIterator{ + client: client, + generation: generation, + ch: make(chan litestream.SnapshotInfo), + } + + itr.ctx, itr.cancel = context.WithCancel(ctx) + itr.g.Go(itr.fetch) + + return itr +} + +// fetch runs in a separate goroutine to fetch pages of objects and stream them to a channel. +func (itr *snapshotIterator) fetch() error { + defer close(itr.ch) + + dir, err := litestream.SnapshotsPath(itr.client.Path, itr.generation) + if err != nil { + return fmt.Errorf("cannot determine snapshots path: %w", err) + } + + iter := itr.client.BucketClient.List(itr.ctx, b2.ListPrefix(dir+"/"), b2.ListDelimiter("/")) + for iter.Next() { + obj := iter.Object() + name := strings.TrimSuffix(strings.TrimPrefix(iter.Object().Name(), dir+"/"), "/") + + index, err := litestream.ParseSnapshotPath(name) + if err != nil { + continue + } + + attrs, err := obj.Attrs(itr.ctx) + if err != nil { + return err + } + + info := litestream.SnapshotInfo{ + Generation: itr.generation, + Index: index, + Size: attrs.Size, + CreatedAt: attrs.UploadTimestamp.UTC(), + } + + select { + case <-itr.ctx.Done(): + return itr.ctx.Err() + case itr.ch <- info: + } + } + + if err := iter.Err(); err != nil { + return err + } + + internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "LIST").Inc() + return nil +} + +func (itr *snapshotIterator) Close() (err error) { + err = itr.err + + // Cancel context and wait for error group to finish. + itr.cancel() + if e := itr.g.Wait(); e != nil && err == nil { + err = e + } + + return err +} + +func (itr *snapshotIterator) Next() bool { + // Exit if an error has already occurred. + if itr.err != nil { + return false + } + + // Return false if context was canceled or if there are no more snapshots. + // Otherwise fetch the next snapshot and store it on the iterator. + select { + case <-itr.ctx.Done(): + return false + case info, ok := <-itr.ch: + if !ok { + return false + } + itr.info = info + return true + } +} + +func (itr *snapshotIterator) Err() error { return itr.err } + +func (itr *snapshotIterator) Snapshot() litestream.SnapshotInfo { + return itr.info +} + +type walSegmentIterator struct { + client *ReplicaClient + generation string + + ch chan litestream.WALSegmentInfo + g errgroup.Group + ctx context.Context + cancel func() + + info litestream.WALSegmentInfo + err error +} + +func newWALSegmentIterator(ctx context.Context, client *ReplicaClient, generation string) *walSegmentIterator { + itr := &walSegmentIterator{ + client: client, + generation: generation, + ch: make(chan litestream.WALSegmentInfo), + } + + itr.ctx, itr.cancel = context.WithCancel(ctx) + itr.g.Go(itr.fetch) + + return itr +} + +// fetch runs in a separate goroutine to fetch pages of objects and stream them to a channel. +func (itr *walSegmentIterator) fetch() error { + defer close(itr.ch) + + dir, err := litestream.WALPath(itr.client.Path, itr.generation) + if err != nil { + return fmt.Errorf("cannot determine wal path: %w", err) + } + + iter := itr.client.BucketClient.List(itr.ctx, b2.ListPrefix(dir+"/"), b2.ListDelimiter("/")) + for iter.Next() { + obj := iter.Object() + name := strings.TrimSuffix(strings.TrimPrefix(iter.Object().Name(), dir+"/"), "/") + + index, offset, err := litestream.ParseWALSegmentPath(name) + if err != nil { + continue + } + + attrs, err := obj.Attrs(itr.ctx) + if err != nil { + return err + } + + info := litestream.WALSegmentInfo{ + Generation: itr.generation, + Index: index, + Offset: offset, + Size: attrs.Size, + CreatedAt: attrs.UploadTimestamp.UTC(), + } + + select { + case <-itr.ctx.Done(): + return itr.ctx.Err() + case itr.ch <- info: + } + } + + if err := iter.Err(); err != nil { + return err + } + + return nil +} + +func (itr *walSegmentIterator) Close() (err error) { + err = itr.err + + // Cancel context and wait for error group to finish. + itr.cancel() + if e := itr.g.Wait(); e != nil && err == nil { + err = e + } + + return err +} + +func (itr *walSegmentIterator) Next() bool { + // Exit if an error has already occurred. + if itr.err != nil { + return false + } + + // Return false if context was canceled or if there are no more segments. + // Otherwise fetch the next segment and store it on the iterator. + select { + case <-itr.ctx.Done(): + return false + case info, ok := <-itr.ch: + if !ok { + return false + } + itr.info = info + return true + } +} + +func (itr *walSegmentIterator) Err() error { return itr.err } + +func (itr *walSegmentIterator) WALSegment() litestream.WALSegmentInfo { + return itr.info +} diff --git a/cmd/litestream/main.go b/cmd/litestream/main.go index 304fa133..40a0b897 100644 --- a/cmd/litestream/main.go +++ b/cmd/litestream/main.go @@ -19,6 +19,7 @@ import ( "filippo.io/age" "github.com/benbjohnson/litestream" "github.com/benbjohnson/litestream/abs" + "github.com/benbjohnson/litestream/b2" "github.com/benbjohnson/litestream/file" "github.com/benbjohnson/litestream/gcs" "github.com/benbjohnson/litestream/s3" @@ -359,6 +360,10 @@ type ReplicaConfig struct { AccountName string `yaml:"account-name"` AccountKey string `yaml:"account-key"` + // B2 settings + ApplicationKeyID string `yaml:"application-key-id"` + ApplicationKey string `yaml:"application-key"` + // SFTP settings Host string `yaml:"host"` User string `yaml:"user"` @@ -435,6 +440,10 @@ func NewReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *litestream.Re if r.Client, err = newSFTPReplicaClientFromConfig(c, r); err != nil { return nil, err } + case "b2": + if r.Client, err = newB2ReplicaClientFromConfig(c, r); err != nil { + return nil, err + } default: return nil, fmt.Errorf("unknown replica type in config: %q", c.Type) } @@ -668,6 +677,30 @@ func newSFTPReplicaClientFromConfig(c *ReplicaConfig, r *litestream.Replica) (_ return client, nil } +// newB2ReplicaClientFromConfig returns a new instance of abs.ReplicaClient built from config. +func newB2ReplicaClientFromConfig(c *ReplicaConfig, r *litestream.Replica) (_ *b2.ReplicaClient, err error) { + // Ensure URL & constituent parts are not both specified. + if c.URL != "" && c.Path != "" { + return nil, fmt.Errorf("cannot specify url & path for abs replica") + } else if c.URL != "" && c.Bucket != "" { + return nil, fmt.Errorf("cannot specify url & bucket for abs replica") + } + + // Build replica. + client := b2.NewReplicaClient() + client.ApplicationKeyID = c.ApplicationKeyID + client.ApplicationKey = c.ApplicationKey + client.Bucket = c.Bucket + client.Path = c.Path + + // Ensure required settings are set. + if client.Bucket == "" { + return nil, fmt.Errorf("bucket required for B2 replica") + } + + return client, nil +} + // applyLitestreamEnv copies "LITESTREAM" prefixed environment variables to // their AWS counterparts as the "AWS" prefix can be confusing when using a // non-AWS S3-compatible service. diff --git a/cmd/litestream/replicate.go b/cmd/litestream/replicate.go index 7c9b4f30..7015a613 100644 --- a/cmd/litestream/replicate.go +++ b/cmd/litestream/replicate.go @@ -13,6 +13,7 @@ import ( "github.com/benbjohnson/litestream" "github.com/benbjohnson/litestream/abs" + "github.com/benbjohnson/litestream/b2" "github.com/benbjohnson/litestream/file" "github.com/benbjohnson/litestream/gcs" "github.com/benbjohnson/litestream/s3" @@ -121,6 +122,8 @@ func (c *ReplicateCommand) Run() (err error) { slog.Info("replicating to", "bucket", client.Bucket, "path", client.Path, "endpoint", client.Endpoint) case *sftp.ReplicaClient: slog.Info("replicating to", "host", client.Host, "user", client.User, "path", client.Path) + case *b2.ReplicaClient: + slog.Info("replicating to", "bucket", client.Bucket, "path", client.Path) default: slog.Info("replicating to") } diff --git a/go.mod b/go.mod index 35ef41db..74e43545 100644 --- a/go.mod +++ b/go.mod @@ -6,24 +6,25 @@ require ( cloud.google.com/go/storage v1.31.0 filippo.io/age v1.1.1 github.com/Azure/azure-storage-blob-go v0.15.0 + github.com/Backblaze/blazer v0.6.1 github.com/aws/aws-sdk-go v1.44.318 github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-sqlite3 v1.14.17 github.com/pierrec/lz4/v4 v4.1.18 github.com/pkg/sftp v1.13.5 github.com/prometheus/client_golang v1.16.0 - golang.org/x/crypto v0.12.0 + golang.org/x/crypto v0.14.0 golang.org/x/sync v0.3.0 - golang.org/x/sys v0.11.0 + golang.org/x/sys v0.13.0 google.golang.org/api v0.135.0 gopkg.in/yaml.v2 v2.4.0 ) require ( - cloud.google.com/go v0.110.7 // indirect + cloud.google.com/go v0.110.8 // indirect cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.1 // indirect + cloud.google.com/go/iam v1.1.2 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -31,7 +32,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/s2a-go v0.1.4 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -42,14 +43,14 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect - google.golang.org/grpc v1.57.0 // indirect + google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231009173412-8bfb1ae86b6c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/grpc v1.58.2 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index c8747b0f..56f8590c 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= -cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= +cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= -cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4= +cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= cloud.google.com/go/storage v1.31.0 h1:+S3LjjEN2zZ+L5hOwj4+1OkGCsLVe0NzpXKQ1pSdTCI= cloud.google.com/go/storage v1.31.0/go.mod h1:81ams1PrhW16L4kF7qg+4mTq7SRs5HsbDTM0bWvrwJ0= filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= @@ -27,6 +27,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Backblaze/blazer v0.6.1 h1:xC9HyC7OcxRzzmtfRiikIEvq4HZYWjU6caFwX2EXw1s= +github.com/Backblaze/blazer v0.6.1/go.mod h1:7/jrGx4O6OKOto6av+hLwelPR8rwZ+PLxQ5ZOiYAjwY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go v1.44.318 h1:Yl66rpbQHFUbxe9JBKLcvOvRivhVgP6+zH0b9KzARX8= @@ -92,8 +94,8 @@ github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -160,8 +162,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -183,8 +185,8 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= @@ -209,13 +211,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -223,8 +225,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -247,12 +249,12 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 h1:Tyk/35yqszRCvaragTn5NnkY6IiKk/XvHzEWepo71N0= -google.golang.org/genproto v0.0.0-20230807174057-1744710a1577/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577 h1:xv8KoglAClYGkprUSmDTKaILtzfD8XzG9NYVXMprjKo= -google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk= +google.golang.org/genproto/googleapis/api v0.0.0-20231009173412-8bfb1ae86b6c h1:0RtEmmHjemvUXloH7+RuBSIw7n+GEHMOMY1CkGYnWq4= +google.golang.org/genproto/googleapis/api v0.0.0-20231009173412-8bfb1ae86b6c/go.mod h1:Wth13BrWMRN/G+guBLupKa6fslcWZv14R0ZKDRkNfY8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -261,8 +263,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=