mirror of
https://github.com/restic/restic.git
synced 2024-12-26 17:57:50 +00:00
599 lines
19 KiB
Go
599 lines
19 KiB
Go
// Copyright 2017 Google Inc. All Rights Reserved.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package firestore
|
||
|
||
import (
|
||
"errors"
|
||
"reflect"
|
||
"sort"
|
||
|
||
"golang.org/x/net/context"
|
||
"google.golang.org/api/iterator"
|
||
|
||
vkit "cloud.google.com/go/firestore/apiv1beta1"
|
||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||
)
|
||
|
||
var errNilDocRef = errors.New("firestore: nil DocumentRef")
|
||
|
||
// A DocumentRef is a reference to a Firestore document.
|
||
type DocumentRef struct {
|
||
// The CollectionRef that this document is a part of. Never nil.
|
||
Parent *CollectionRef
|
||
|
||
// The full resource path of the document: "projects/P/databases/D/documents..."
|
||
Path string
|
||
|
||
// The ID of the document: the last component of the resource path.
|
||
ID string
|
||
}
|
||
|
||
func newDocRef(parent *CollectionRef, id string) *DocumentRef {
|
||
return &DocumentRef{
|
||
Parent: parent,
|
||
ID: id,
|
||
Path: parent.Path + "/" + id,
|
||
}
|
||
}
|
||
|
||
func (d1 *DocumentRef) equal(d2 *DocumentRef) bool {
|
||
if d1 == nil || d2 == nil {
|
||
return d1 == d2
|
||
}
|
||
return d1.Parent.equal(d2.Parent) && d1.Path == d2.Path && d1.ID == d2.ID
|
||
}
|
||
|
||
// Collection returns a reference to sub-collection of this document.
|
||
func (d *DocumentRef) Collection(id string) *CollectionRef {
|
||
return newCollRefWithParent(d.Parent.c, d, id)
|
||
}
|
||
|
||
// Get retrieves the document. It returns an error if the document does not exist.
|
||
func (d *DocumentRef) Get(ctx context.Context) (*DocumentSnapshot, error) {
|
||
if err := checkTransaction(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if d == nil {
|
||
return nil, errNilDocRef
|
||
}
|
||
doc, err := d.Parent.c.c.GetDocument(withResourceHeader(ctx, d.Parent.c.path()),
|
||
&pb.GetDocumentRequest{Name: d.Path})
|
||
// TODO(jba): verify that GetDocument returns NOT_FOUND.
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return newDocumentSnapshot(d, doc, d.Parent.c)
|
||
}
|
||
|
||
// Create creates the document with the given data.
|
||
// It returns an error if a document with the same ID already exists.
|
||
//
|
||
// The data argument can be a map with string keys, a struct, or a pointer to a
|
||
// struct. The map keys or exported struct fields become the fields of the firestore
|
||
// document.
|
||
// The values of data are converted to Firestore values as follows:
|
||
//
|
||
// - bool converts to Bool.
|
||
// - string converts to String.
|
||
// - int, int8, int16, int32 and int64 convert to Integer.
|
||
// - uint8, uint16 and uint32 convert to Integer. uint64 is disallowed,
|
||
// because it can represent values that cannot be represented in an int64, which
|
||
// is the underlying type of a Integer.
|
||
// - float32 and float64 convert to Double.
|
||
// - []byte converts to Bytes.
|
||
// - time.Time converts to Timestamp.
|
||
// - latlng.LatLng converts to GeoPoint. latlng is the package
|
||
// "google.golang.org/genproto/googleapis/type/latlng".
|
||
// - Slices convert to Array.
|
||
// - Maps and structs convert to Map.
|
||
// - nils of any type convert to Null.
|
||
//
|
||
// Pointers and interface{} are also permitted, and their elements processed
|
||
// recursively.
|
||
//
|
||
// Struct fields can have tags like those used by the encoding/json package. Tags
|
||
// begin with "firestore:" and are followed by "-", meaning "ignore this field," or
|
||
// an alternative name for the field. Following the name, these comma-separated
|
||
// options may be provided:
|
||
//
|
||
// - omitempty: Do not encode this field if it is empty. A value is empty
|
||
// if it is a zero value, or an array, slice or map of length zero.
|
||
// - serverTimestamp: The field must be of type time.Time. When writing, if
|
||
// the field has the zero value, the server will populate the stored document with
|
||
// the time that the request is processed.
|
||
func (d *DocumentRef) Create(ctx context.Context, data interface{}) (*WriteResult, error) {
|
||
ws, err := d.newReplaceWrites(data, nil, Exists(false))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return d.Parent.c.commit(ctx, ws)
|
||
}
|
||
|
||
// Set creates or overwrites the document with the given data. See DocumentRef.Create
|
||
// for the acceptable values of data. Without options, Set overwrites the document
|
||
// completely. Specify one of the Merge options to preserve an existing document's
|
||
// fields.
|
||
func (d *DocumentRef) Set(ctx context.Context, data interface{}, opts ...SetOption) (*WriteResult, error) {
|
||
ws, err := d.newReplaceWrites(data, opts, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return d.Parent.c.commit(ctx, ws)
|
||
}
|
||
|
||
// Delete deletes the document. If the document doesn't exist, it does nothing
|
||
// and returns no error.
|
||
func (d *DocumentRef) Delete(ctx context.Context, preconds ...Precondition) (*WriteResult, error) {
|
||
ws, err := d.newDeleteWrites(preconds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return d.Parent.c.commit(ctx, ws)
|
||
}
|
||
|
||
func (d *DocumentRef) newReplaceWrites(data interface{}, opts []SetOption, p Precondition) ([]*pb.Write, error) {
|
||
if d == nil {
|
||
return nil, errNilDocRef
|
||
}
|
||
origFieldPaths, allPaths, err := processSetOptions(opts)
|
||
isMerge := len(origFieldPaths) > 0 || allPaths // was some Merge option specified?
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
doc, serverTimestampPaths, err := toProtoDocument(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(origFieldPaths) > 0 {
|
||
// Keep only data fields corresponding to the given field paths.
|
||
doc.Fields = applyFieldPaths(doc.Fields, origFieldPaths, nil)
|
||
}
|
||
doc.Name = d.Path
|
||
|
||
var fieldPaths []FieldPath
|
||
if allPaths {
|
||
// MergeAll was passed. Check that the data is a map, and extract its field paths.
|
||
v := reflect.ValueOf(data)
|
||
if v.Kind() != reflect.Map {
|
||
return nil, errors.New("firestore: MergeAll can only be specified with map data")
|
||
}
|
||
fieldPaths = fieldPathsFromMap(v, nil)
|
||
} else if len(origFieldPaths) > 0 {
|
||
// Remove server timestamp paths that are not in the list of paths to merge.
|
||
// Note: this is technically O(n^2), but it is unlikely that there is more
|
||
// than one server timestamp path.
|
||
serverTimestampPaths = removePathsIf(serverTimestampPaths, func(fp FieldPath) bool {
|
||
return !fp.in(origFieldPaths)
|
||
})
|
||
// Remove server timestamp fields from fieldPaths. Those fields were removed
|
||
// from the document by toProtoDocument, so they should not be in the update
|
||
// mask.
|
||
// Note: this is technically O(n^2), but it is unlikely that there is
|
||
// more than one server timestamp path.
|
||
fieldPaths = removePathsIf(origFieldPaths, func(fp FieldPath) bool {
|
||
return fp.in(serverTimestampPaths)
|
||
})
|
||
// Check that all the remaining field paths in the merge option are in the document.
|
||
for _, fp := range fieldPaths {
|
||
if _, err := valueAtPath(fp, doc.Fields); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
var pc *pb.Precondition
|
||
if p != nil {
|
||
pc, err = p.preconditionProto()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
var w *pb.Write
|
||
switch {
|
||
case len(fieldPaths) > 0:
|
||
// There are field paths, so we need an update mask.
|
||
sfps := toServiceFieldPaths(fieldPaths)
|
||
sort.Strings(sfps) // TODO(jba): make tests pass without this
|
||
w = &pb.Write{
|
||
Operation: &pb.Write_Update{doc},
|
||
UpdateMask: &pb.DocumentMask{FieldPaths: sfps},
|
||
CurrentDocument: pc,
|
||
}
|
||
case isMerge && pc != nil:
|
||
// There were field paths, but they all got removed.
|
||
// The write does nothing but enforce the precondition.
|
||
w = &pb.Write{CurrentDocument: pc}
|
||
case !isMerge:
|
||
// Set without merge, so no update mask.
|
||
w = &pb.Write{
|
||
Operation: &pb.Write_Update{doc},
|
||
CurrentDocument: pc,
|
||
}
|
||
}
|
||
return d.writeWithTransform(w, serverTimestampPaths), nil
|
||
}
|
||
|
||
// Create a new map that contains only the field paths in fps.
|
||
func applyFieldPaths(fields map[string]*pb.Value, fps []FieldPath, root FieldPath) map[string]*pb.Value {
|
||
r := map[string]*pb.Value{}
|
||
for k, v := range fields {
|
||
kpath := root.with(k)
|
||
if kpath.in(fps) {
|
||
r[k] = v
|
||
} else if mv := v.GetMapValue(); mv != nil {
|
||
if m2 := applyFieldPaths(mv.Fields, fps, kpath); m2 != nil {
|
||
r[k] = &pb.Value{&pb.Value_MapValue{&pb.MapValue{m2}}}
|
||
}
|
||
}
|
||
}
|
||
if len(r) == 0 {
|
||
return nil
|
||
}
|
||
return r
|
||
}
|
||
|
||
func fieldPathsFromMap(vmap reflect.Value, prefix FieldPath) []FieldPath {
|
||
// vmap is a map and its keys are strings.
|
||
// Each map key denotes a field; no splitting or escaping.
|
||
var fps []FieldPath
|
||
for _, k := range vmap.MapKeys() {
|
||
v := vmap.MapIndex(k)
|
||
fp := prefix.with(k.String())
|
||
if vm := extractMap(v); vm.IsValid() {
|
||
fps = append(fps, fieldPathsFromMap(vm, fp)...)
|
||
} else if v.Interface() != ServerTimestamp {
|
||
// ServerTimestamp fields do not go into the update mask.
|
||
fps = append(fps, fp)
|
||
}
|
||
}
|
||
return fps
|
||
}
|
||
|
||
func extractMap(v reflect.Value) reflect.Value {
|
||
switch v.Kind() {
|
||
case reflect.Map:
|
||
return v
|
||
case reflect.Interface:
|
||
return extractMap(v.Elem())
|
||
default:
|
||
return reflect.Value{}
|
||
}
|
||
}
|
||
|
||
// removePathsIf creates a new slice of FieldPaths that contains
|
||
// exactly those elements of fps for which pred returns false.
|
||
func removePathsIf(fps []FieldPath, pred func(FieldPath) bool) []FieldPath {
|
||
var result []FieldPath
|
||
for _, fp := range fps {
|
||
if !pred(fp) {
|
||
result = append(result, fp)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
func (d *DocumentRef) newDeleteWrites(preconds []Precondition) ([]*pb.Write, error) {
|
||
if d == nil {
|
||
return nil, errNilDocRef
|
||
}
|
||
pc, err := processPreconditionsForDelete(preconds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return []*pb.Write{{
|
||
Operation: &pb.Write_Delete{d.Path},
|
||
CurrentDocument: pc,
|
||
}}, nil
|
||
}
|
||
|
||
func (d *DocumentRef) newUpdateMapWrites(data map[string]interface{}, preconds []Precondition) ([]*pb.Write, error) {
|
||
// Collect all the (top-level) keys map; they will comprise the update mask.
|
||
// Also, translate the map into a sequence of FieldPathUpdates.
|
||
var fps []FieldPath
|
||
var fpus []FieldPathUpdate
|
||
for k, v := range data {
|
||
fp, err := parseDotSeparatedString(k)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
fps = append(fps, fp)
|
||
fpus = append(fpus, FieldPathUpdate{Path: fp, Value: v})
|
||
}
|
||
// Check that there are no duplicate field paths, and that no field
|
||
// path is a prefix of another.
|
||
if err := checkNoDupOrPrefix(fps); err != nil {
|
||
return nil, err
|
||
}
|
||
// Re-create the map from the field paths and their corresponding values. A field path
|
||
// with a Delete value will not appear in the map but it will appear in the
|
||
// update mask, which will cause it to be deleted.
|
||
m := createMapFromFieldPathUpdates(fpus)
|
||
return d.newUpdateWrites(m, fps, preconds)
|
||
}
|
||
|
||
func (d *DocumentRef) newUpdateStructWrites(fieldPaths []string, data interface{}, preconds []Precondition) ([]*pb.Write, error) {
|
||
if !isStructOrStructPtr(data) {
|
||
return nil, errors.New("firestore: data is not struct or struct pointer")
|
||
}
|
||
fps, err := parseDotSeparatedStrings(fieldPaths)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err := checkNoDupOrPrefix(fps); err != nil {
|
||
return nil, err
|
||
}
|
||
return d.newUpdateWrites(data, fps, preconds)
|
||
}
|
||
|
||
func (d *DocumentRef) newUpdatePathWrites(data []FieldPathUpdate, preconds []Precondition) ([]*pb.Write, error) {
|
||
var fps []FieldPath
|
||
for _, fpu := range data {
|
||
if err := fpu.Path.validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
fps = append(fps, fpu.Path)
|
||
}
|
||
if err := checkNoDupOrPrefix(fps); err != nil {
|
||
return nil, err
|
||
}
|
||
m := createMapFromFieldPathUpdates(data)
|
||
return d.newUpdateWrites(m, fps, preconds)
|
||
}
|
||
|
||
// newUpdateWrites creates Write operations for an update.
|
||
func (d *DocumentRef) newUpdateWrites(data interface{}, fieldPaths []FieldPath, preconds []Precondition) ([]*pb.Write, error) {
|
||
if len(fieldPaths) == 0 {
|
||
return nil, errors.New("firestore: no paths to update")
|
||
}
|
||
if d == nil {
|
||
return nil, errNilDocRef
|
||
}
|
||
pc, err := processPreconditionsForUpdate(preconds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
doc, serverTimestampPaths, err := toProtoDocument(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
sfps := toServiceFieldPaths(fieldPaths)
|
||
doc.Name = d.Path
|
||
return d.writeWithTransform(&pb.Write{
|
||
Operation: &pb.Write_Update{doc},
|
||
UpdateMask: &pb.DocumentMask{FieldPaths: sfps},
|
||
CurrentDocument: pc,
|
||
}, serverTimestampPaths), nil
|
||
}
|
||
|
||
var requestTimeTransform = &pb.DocumentTransform_FieldTransform_SetToServerValue{
|
||
pb.DocumentTransform_FieldTransform_REQUEST_TIME,
|
||
}
|
||
|
||
func (d *DocumentRef) writeWithTransform(w *pb.Write, serverTimestampFieldPaths []FieldPath) []*pb.Write {
|
||
var ws []*pb.Write
|
||
if w != nil {
|
||
ws = append(ws, w)
|
||
}
|
||
if len(serverTimestampFieldPaths) > 0 {
|
||
ws = append(ws, d.newTransform(serverTimestampFieldPaths))
|
||
}
|
||
return ws
|
||
}
|
||
|
||
func (d *DocumentRef) newTransform(serverTimestampFieldPaths []FieldPath) *pb.Write {
|
||
sort.Sort(byPath(serverTimestampFieldPaths)) // TODO(jba): make tests pass without this
|
||
var fts []*pb.DocumentTransform_FieldTransform
|
||
for _, p := range serverTimestampFieldPaths {
|
||
fts = append(fts, &pb.DocumentTransform_FieldTransform{
|
||
FieldPath: p.toServiceFieldPath(),
|
||
TransformType: requestTimeTransform,
|
||
})
|
||
}
|
||
return &pb.Write{
|
||
Operation: &pb.Write_Transform{
|
||
&pb.DocumentTransform{
|
||
Document: d.Path,
|
||
FieldTransforms: fts,
|
||
// TODO(jba): should the transform have the same preconditions as the write?
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
var (
|
||
// Delete is used as a value in a call to UpdateMap to indicate that the
|
||
// corresponding key should be deleted.
|
||
Delete = new(int)
|
||
// Not new(struct{}), because addresses of zero-sized values
|
||
// may not be unique.
|
||
|
||
// ServerTimestamp is used as a value in a call to UpdateMap to indicate that the
|
||
// key's value should be set to the time at which the server processed
|
||
// the request.
|
||
ServerTimestamp = new(int)
|
||
)
|
||
|
||
// UpdateMap updates the document using the given data. Map keys replace the stored
|
||
// values, but other fields of the stored document are untouched.
|
||
// See DocumentRef.Create for acceptable map values.
|
||
//
|
||
// If a map key is a multi-element field path, like "a.b", then only key "b" of
|
||
// the map value at "a" is changed; the rest of the map is preserved.
|
||
// For example, if the stored data is
|
||
// {"a": {"b": 1, "c": 2}}
|
||
// then
|
||
// UpdateMap({"a": {"b": 3}}) => {"a": {"b": 3}}
|
||
// while
|
||
// UpdateMap({"a.b": 3}) => {"a": {"b": 3, "c": 2}}
|
||
//
|
||
// To delete a key, specify it in the input with a value of firestore.Delete.
|
||
//
|
||
// Field paths expressed as map keys must not contain any of the runes "˜*/[]".
|
||
// Use UpdatePaths instead for such paths.
|
||
//
|
||
// UpdateMap returns an error if the document does not exist.
|
||
func (d *DocumentRef) UpdateMap(ctx context.Context, data map[string]interface{}, preconds ...Precondition) (*WriteResult, error) {
|
||
ws, err := d.newUpdateMapWrites(data, preconds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return d.Parent.c.commit(ctx, ws)
|
||
}
|
||
|
||
func isStructOrStructPtr(x interface{}) bool {
|
||
v := reflect.ValueOf(x)
|
||
if v.Kind() == reflect.Struct {
|
||
return true
|
||
}
|
||
if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// UpdateStruct updates the given field paths of the stored document from the fields
|
||
// of data, which must be a struct or a pointer to a struct. Other fields of the
|
||
// stored document are untouched.
|
||
// See DocumentRef.Create for the acceptable values of the struct's fields.
|
||
//
|
||
// Each element of fieldPaths is a single field or a dot-separated sequence of
|
||
// fields, none of which contain the runes "˜*/[]".
|
||
//
|
||
// If an element of fieldPaths does not have a corresponding field in the struct,
|
||
// that key is deleted from the stored document.
|
||
//
|
||
// UpdateStruct returns an error if the document does not exist.
|
||
func (d *DocumentRef) UpdateStruct(ctx context.Context, fieldPaths []string, data interface{}, preconds ...Precondition) (*WriteResult, error) {
|
||
ws, err := d.newUpdateStructWrites(fieldPaths, data, preconds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return d.Parent.c.commit(ctx, ws)
|
||
}
|
||
|
||
// A FieldPathUpdate describes an update to a value referred to by a FieldPath.
|
||
// See DocumentRef.Create for acceptable values.
|
||
// To delete a field, specify firestore.Delete as the value.
|
||
type FieldPathUpdate struct {
|
||
Path FieldPath
|
||
Value interface{}
|
||
}
|
||
|
||
// UpdatePaths updates the document using the given data. The values at the given
|
||
// field paths are replaced, but other fields of the stored document are untouched.
|
||
func (d *DocumentRef) UpdatePaths(ctx context.Context, data []FieldPathUpdate, preconds ...Precondition) (*WriteResult, error) {
|
||
ws, err := d.newUpdatePathWrites(data, preconds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return d.Parent.c.commit(ctx, ws)
|
||
}
|
||
|
||
// Collections returns an interator over the immediate sub-collections of the document.
|
||
func (d *DocumentRef) Collections(ctx context.Context) *CollectionIterator {
|
||
client := d.Parent.c
|
||
it := &CollectionIterator{
|
||
err: checkTransaction(ctx),
|
||
client: client,
|
||
parent: d,
|
||
it: client.c.ListCollectionIds(
|
||
withResourceHeader(ctx, client.path()),
|
||
&pb.ListCollectionIdsRequest{Parent: d.Path}),
|
||
}
|
||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||
it.fetch,
|
||
func() int { return len(it.items) },
|
||
func() interface{} { b := it.items; it.items = nil; return b })
|
||
return it
|
||
}
|
||
|
||
// CollectionIterator is an iterator over sub-collections of a document.
|
||
type CollectionIterator struct {
|
||
client *Client
|
||
parent *DocumentRef
|
||
it *vkit.StringIterator
|
||
pageInfo *iterator.PageInfo
|
||
nextFunc func() error
|
||
items []*CollectionRef
|
||
err error
|
||
}
|
||
|
||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||
func (it *CollectionIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
|
||
|
||
// Next returns the next result. Its second return value is iterator.Done if there
|
||
// are no more results. Once Next returns Done, all subsequent calls will return
|
||
// Done.
|
||
func (it *CollectionIterator) Next() (*CollectionRef, error) {
|
||
if err := it.nextFunc(); err != nil {
|
||
return nil, err
|
||
}
|
||
item := it.items[0]
|
||
it.items = it.items[1:]
|
||
return item, nil
|
||
}
|
||
|
||
func (it *CollectionIterator) fetch(pageSize int, pageToken string) (string, error) {
|
||
if it.err != nil {
|
||
return "", it.err
|
||
}
|
||
return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
|
||
id, err := it.it.Next()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var cr *CollectionRef
|
||
if it.parent == nil {
|
||
cr = newTopLevelCollRef(it.client, it.client.path(), id)
|
||
} else {
|
||
cr = newCollRefWithParent(it.client, it.parent, id)
|
||
}
|
||
it.items = append(it.items, cr)
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// GetAll returns all the collections remaining from the iterator.
|
||
func (it *CollectionIterator) GetAll() ([]*CollectionRef, error) {
|
||
var crs []*CollectionRef
|
||
for {
|
||
cr, err := it.Next()
|
||
if err == iterator.Done {
|
||
break
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
crs = append(crs, cr)
|
||
}
|
||
return crs, nil
|
||
}
|
||
|
||
// Common fetch code for iterators that are backed by vkit iterators.
|
||
// TODO(jba): dedup with same function in logging/logadmin.
|
||
func iterFetch(pageSize int, pageToken string, pi *iterator.PageInfo, next func() error) (string, error) {
|
||
pi.MaxSize = pageSize
|
||
pi.Token = pageToken
|
||
// Get one item, which will fill the buffer.
|
||
if err := next(); err != nil {
|
||
return "", err
|
||
}
|
||
// Collect the rest of the buffer.
|
||
for pi.Remaining() > 0 {
|
||
if err := next(); err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
return pi.Token, nil
|
||
}
|