github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/collection.go (about)

     1  package lib
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/qri-io/qri/base"
    10  	"github.com/qri-io/qri/base/params"
    11  	"github.com/qri-io/qri/dscache/build"
    12  	"github.com/qri-io/qri/dsref"
    13  	qhttp "github.com/qri-io/qri/lib/http"
    14  	"github.com/qri-io/qri/profile"
    15  	reporef "github.com/qri-io/qri/repo/ref"
    16  )
    17  
    18  // CollectionMethods lists a user's datasets. Datasets in a collection consist
    19  // of datasets the user has created and other datasets the user has pulled.
    20  //
    21  // Collections are local. The same user's collection on one qri node will
    22  // often be different from another node, depending on what datasets have been
    23  // created, pushed, or pulled to that node
    24  type CollectionMethods struct {
    25  	d dispatcher
    26  }
    27  
    28  // Name returns the name of this method group
    29  func (m CollectionMethods) Name() string {
    30  	return "collection"
    31  }
    32  
    33  // Attributes defines attributes for each method
    34  func (m CollectionMethods) Attributes() map[string]AttributeSet {
    35  	return map[string]AttributeSet{
    36  		"list":        {Endpoint: qhttp.AEList, HTTPVerb: "POST"},
    37  		"listrawrefs": {Endpoint: qhttp.DenyHTTP},
    38  		"get":         {Endpoint: qhttp.AECollectionGet, HTTPVerb: "POST"},
    39  	}
    40  }
    41  
    42  // ErrListWarning is a warning that can occur while listing
    43  var ErrListWarning = base.ErrUnlistableReferences
    44  
    45  // CollectionListParams defines parameters for listing a user's collection
    46  type CollectionListParams struct {
    47  	params.List
    48  	Username string `json:"username,omitempty"`
    49  	Public   bool   `json:"public,omitempty"`
    50  	Term     string `json:"term,omitempty"`
    51  }
    52  
    53  // SetNonZeroDefaults sets OrderBy to "created" if it's value is empty
    54  func (p *CollectionListParams) SetNonZeroDefaults() {
    55  	if len(p.OrderBy) == 0 {
    56  		p.List = p.List.WithOrderBy("created")
    57  	}
    58  	if p.Offset < 0 {
    59  		p.Offset = 0
    60  	}
    61  	if p.Limit <= 0 {
    62  		p.Limit = params.DefaultListLimit
    63  	}
    64  }
    65  
    66  // List gets the reflist for either the local repo or a peer
    67  func (m CollectionMethods) List(ctx context.Context, p *CollectionListParams) ([]dsref.VersionInfo, Cursor, error) {
    68  	got, cur, err := m.d.Dispatch(ctx, dispatchMethodName(m, "list"), p)
    69  	if res, ok := got.([]dsref.VersionInfo); ok {
    70  		return res, cur, err
    71  	}
    72  	return nil, nil, dispatchReturnError(got, err)
    73  }
    74  
    75  // ListRawRefs gets the list of raw references as string
    76  func (m CollectionMethods) ListRawRefs(ctx context.Context, p *EmptyParams) (string, error) {
    77  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "listrawrefs"), p)
    78  	if res, ok := got.(string); ok {
    79  		return res, err
    80  	}
    81  	return "", dispatchReturnError(got, err)
    82  }
    83  
    84  // CollectionGetParams defines parameters for looking up the head of a dataset from the collection
    85  type CollectionGetParams struct {
    86  	Ref    string `json:"ref"`
    87  	InitID string `json:"initID"`
    88  }
    89  
    90  // Validate returns an error if CollectionGetParams fields are in an invalid state
    91  func (p *CollectionGetParams) Validate() error {
    92  	if p.Ref == "" && p.InitID == "" {
    93  		return fmt.Errorf("either ref or initID are required")
    94  	}
    95  	return nil
    96  }
    97  
    98  // Get gets the head of a dataset as a VersionInfo from the collection
    99  func (m CollectionMethods) Get(ctx context.Context, p *CollectionGetParams) (*dsref.VersionInfo, error) {
   100  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "get"), p)
   101  	if res, ok := got.(*dsref.VersionInfo); ok {
   102  		return res, err
   103  	}
   104  	return nil, dispatchReturnError(got, err)
   105  }
   106  
   107  // collectionImpl holds the method implementations for CollectionMethods
   108  type collectionImpl struct{}
   109  
   110  // List gets the reflist for either the local repo or a peer
   111  func (collectionImpl) List(scope scope, p *CollectionListParams) ([]dsref.VersionInfo, Cursor, error) {
   112  	if s := scope.CollectionSet(); s != nil {
   113  
   114  		id := scope.ActiveProfile().ID
   115  		if p.Username != "" {
   116  			pro, err := getProfile(scope.Context(), scope.Profiles(), "", p.Username)
   117  			if err != nil {
   118  				return nil, nil, err
   119  			}
   120  			id = pro.ID
   121  		}
   122  
   123  		infos, err := s.List(scope.ctx, id, p.List)
   124  		if err != nil {
   125  			return nil, nil, err
   126  		}
   127  
   128  		// Create a cursor that points to the next page of results
   129  		// A cursor is simply the current input params to this method, tweaked such that
   130  		// they get the next page of results
   131  		p.Offset += p.Limit
   132  		cur := scope.MakeCursor(len(infos), p)
   133  		return infos, cur, nil
   134  	}
   135  
   136  	// TODO(dustmop): When List is converted to use scope, get the ProfileID from
   137  	// the scope if the user is authorized to only view their own datasets, as opposed
   138  	// to the full collection that exists in this node's repository.
   139  	restrictPid := ""
   140  
   141  	// ensure valid limit value
   142  	if p.Limit <= 0 {
   143  		p.Limit = 25
   144  	}
   145  	// ensure valid offset value
   146  	if p.Offset < 0 {
   147  		p.Offset = 0
   148  	}
   149  
   150  	reqProfile := scope.Repo().Profiles().Owner(scope.Context())
   151  	listProfile, err := getProfile(scope.Context(), scope.Repo().Profiles(), reqProfile.ID.Encode(), p.Username)
   152  	if err != nil {
   153  		return nil, nil, err
   154  	}
   155  
   156  	// If the list operation leads to a warning, store it in this var
   157  	var listWarning error
   158  
   159  	var infos []dsref.VersionInfo
   160  	if scope.UseDscache() {
   161  		c := scope.Dscache()
   162  		if c.IsEmpty() {
   163  			log.Infof("building dscache from repo's logbook, profile, and dsref")
   164  			built, err := build.DscacheFromRepo(scope.Context(), scope.Repo())
   165  			if err != nil {
   166  				return nil, nil, err
   167  			}
   168  			err = c.Assign(built)
   169  			if err != nil {
   170  				log.Error(err)
   171  			}
   172  		}
   173  		refs, err := c.ListRefs()
   174  		if err != nil {
   175  			return nil, nil, err
   176  		}
   177  		// Filter references so that only with a matching name are returned
   178  		if p.Term != "" {
   179  			matched := make([]reporef.DatasetRef, len(refs))
   180  			count := 0
   181  			for _, ref := range refs {
   182  				if strings.Contains(ref.AliasString(), p.Term) {
   183  					matched[count] = ref
   184  					count++
   185  				}
   186  			}
   187  			refs = matched[:count]
   188  		}
   189  		// Filter references by skipping to the correct offset
   190  		if p.Offset > len(refs) {
   191  			refs = []reporef.DatasetRef{}
   192  		} else {
   193  			refs = refs[p.Offset:]
   194  		}
   195  		// Filter references by limiting how many are returned
   196  		if p.Limit < len(refs) {
   197  			refs = refs[:p.Limit]
   198  		}
   199  		// Convert old style DatasetRef list to VersionInfo list.
   200  		// TODO(dustmop): Remove this and convert lower-level functions to return []VersionInfo.
   201  		infos = make([]dsref.VersionInfo, len(refs))
   202  		for i, r := range refs {
   203  			infos[i] = reporef.ConvertToVersionInfo(&r)
   204  		}
   205  	} else if listProfile.Peername == "" || reqProfile.Peername == listProfile.Peername {
   206  		infos, err = base.ListDatasets(scope.Context(), scope.Repo(), p.Term, restrictPid, p.Offset, p.Limit, p.Public, true)
   207  		if errors.Is(err, ErrListWarning) {
   208  			// This warning can happen when there's conflicts between usernames and
   209  			// profileIDs. This type of conflict should not break listing functionality.
   210  			// Instead, store the warning and treat it as non-fatal.
   211  			listWarning = err
   212  			err = nil
   213  		}
   214  	} else {
   215  		return nil, nil, fmt.Errorf("listing datasets on a peer is not implemented")
   216  	}
   217  	if err != nil {
   218  		return nil, nil, err
   219  	}
   220  
   221  	// Create a cursor that points to the next page of results
   222  	// A cursor is simply the current input params to this method, tweaked such that
   223  	// they get the next page of results
   224  	p.Offset += p.Limit
   225  	cur := scope.MakeCursor(len(infos), p)
   226  
   227  	if listWarning != nil {
   228  		// If there was a warning listing the datasets, we should still return the list
   229  		// itself. The caller should handle this warning by simply printing it, but this
   230  		// shouldn't break the `list` functionality.
   231  		return infos, cur, listWarning
   232  	}
   233  
   234  	return infos, cur, nil
   235  }
   236  
   237  func getProfile(ctx context.Context, pros profile.Store, idStr, peername string) (pro *profile.Profile, err error) {
   238  	if idStr == "" {
   239  		// TODO(b5): we're handling the "me" keyword here, should be handled as part of
   240  		// request scope construction
   241  		if peername == "me" {
   242  			return pros.Owner(ctx), nil
   243  		}
   244  		return profile.ResolveUsername(ctx, pros, peername)
   245  	}
   246  
   247  	id, err := profile.IDB58Decode(idStr)
   248  	if err != nil {
   249  		log.Debugw("decoding profile ID", "err", err)
   250  		return nil, err
   251  	}
   252  	return pros.GetProfile(ctx, id)
   253  }
   254  
   255  // ListRawRefs gets the list of raw references as string
   256  func (collectionImpl) ListRawRefs(scope scope, p *EmptyParams) (string, error) {
   257  	text := ""
   258  	if scope.UseDscache() {
   259  		c := scope.Dscache()
   260  		if c == nil || c.IsEmpty() {
   261  			return "", fmt.Errorf("repo: dscache not found")
   262  		}
   263  		text = c.VerboseString(true)
   264  		return text, nil
   265  	}
   266  	return base.RawDatasetRefs(scope.Context(), scope.ActiveProfile().ID, scope.CollectionSet())
   267  }
   268  
   269  // Get gets the head of a dataset as a VersionInfo from the collection
   270  func (collectionImpl) Get(scope scope, p *CollectionGetParams) (*dsref.VersionInfo, error) {
   271  	s := scope.CollectionSet()
   272  	if s == nil {
   273  		return nil, fmt.Errorf("no collection")
   274  	}
   275  
   276  	var err error
   277  
   278  	ref := dsref.Ref{
   279  		InitID: p.InitID,
   280  	}
   281  
   282  	if ref.InitID != "" {
   283  		_, err = scope.ResolveReference(scope.Context(), &ref)
   284  		if err != nil {
   285  			return nil, err
   286  		}
   287  	} else {
   288  		ref, _, err = scope.ParseAndResolveRef(scope.Context(), p.Ref)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  	}
   293  
   294  	id, err := profile.IDB58Decode(ref.ProfileID)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	return s.Get(scope.Context(), id, ref.InitID)
   299  }