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 }