From 89890b4361f1785c9ddf4ef08379d8f7feda4670 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Thu, 7 Nov 2024 07:57:37 -1000 Subject: [PATCH] wip --- client/rpc/pin.go | 63 +++++-------- client/rpc/unixfs.go | 113 ++++++++++------------- core/commands/ls.go | 17 +++- core/commands/pin/pin.go | 12 ++- core/commands/pin/remotepin.go | 20 ++-- core/coreapi/pin.go | 162 +++++++++++++++------------------ core/coreiface/pin.go | 5 +- core/coreiface/tests/block.go | 13 ++- core/coreiface/tests/pin.go | 34 ++++--- core/coreiface/tests/unixfs.go | 2 +- 10 files changed, 205 insertions(+), 236 deletions(-) diff --git a/client/rpc/pin.go b/client/rpc/pin.go index eb2aa316cf1..c63ca515ff9 100644 --- a/client/rpc/pin.go +++ b/client/rpc/pin.go @@ -62,16 +62,12 @@ type pinLsObject struct { Type string } -func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan iface.Pin, <-chan error) { - pins := make(chan iface.Pin) - errOut := make(chan error, 1) +func (api *PinAPI) Ls(ctx context.Context, pins chan<- iface.Pin, opts ...caopts.PinLsOption) error { + defer close(pins) options, err := caopts.PinLsOptions(opts...) if err != nil { - errOut <- err - close(pins) - close(errOut) - return pins, errOut + return err } res, err := api.core().Request("pin/ls"). @@ -79,42 +75,33 @@ func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan i Option("stream", true). Send(ctx) if err != nil { - errOut <- err - close(pins) - close(errOut) - return pins, errOut + return err } - - go func(ch chan<- iface.Pin, errCh chan<- error) { - defer res.Output.Close() - defer close(ch) - defer close(errCh) - - dec := json.NewDecoder(res.Output) - var out pinLsObject - for { - err := dec.Decode(&out) - if err != nil { - if err != io.EOF { - errCh <- err - } - return + defer res.Output.Close() + + dec := json.NewDecoder(res.Output) + var out pinLsObject + for { + err := dec.Decode(&out) + if err != nil { + if err != io.EOF { + return err } + return nil + } - c, err := cid.Parse(out.Cid) - if err != nil { - errCh <- err - return - } + c, err := cid.Parse(out.Cid) + if err != nil { + return err + } - select { - case ch <- pin{typ: out.Type, name: out.Name, path: path.FromCid(c)}: - case <-ctx.Done(): - return - } + select { + case pins <- pin{typ: out.Type, name: out.Name, path: path.FromCid(c)}: + case <-ctx.Done(): + return ctx.Err() } - }(pins, errOut) - return pins, errOut + } + return nil } // IsPinned returns whether or not the given cid is pinned diff --git a/client/rpc/unixfs.go b/client/rpc/unixfs.go index c913db67a8f..70eb92a3a1d 100644 --- a/client/rpc/unixfs.go +++ b/client/rpc/unixfs.go @@ -144,16 +144,12 @@ type lsOutput struct { Objects []lsObject } -func (api *UnixfsAPI) Ls(ctx context.Context, p path.Path, opts ...caopts.UnixfsLsOption) (<-chan iface.DirEntry, <-chan error) { - out := make(chan iface.DirEntry) - errOut := make(chan error, 1) +func (api *UnixfsAPI) Ls(ctx context.Context, p path.Path, out chan<- iface.DirEntry, opts ...caopts.UnixfsLsOption) error { + defer close(out) options, err := caopts.UnixfsLsOptions(opts...) if err != nil { - errOut <- err - close(out) - close(errOut) - return out, errOut + return err } resp, err := api.core().Request("ls", p.String()). @@ -162,79 +158,66 @@ func (api *UnixfsAPI) Ls(ctx context.Context, p path.Path, opts ...caopts.Unixfs Option("stream", true). Send(ctx) if err != nil { - errOut <- err - close(out) - close(errOut) - return out, errOut + return err } if resp.Error != nil { - errOut <- resp.Error - close(out) - close(errOut) - return out, errOut + return err } + defer resp.Close() dec := json.NewDecoder(resp.Output) - go func() { - defer resp.Close() - defer close(out) - defer close(errOut) - - for { - var link lsOutput - if err := dec.Decode(&link); err != nil { - if err != io.EOF { - errOut <- err - } - return + for { + var link lsOutput + if err = dec.Decode(&link); err != nil { + if err != io.EOF { + return err } + return nil + } - if len(link.Objects) != 1 { - errOut <- errors.New("unexpected Objects len") - return - } + if len(link.Objects) != 1 { + return errors.New("unexpected Objects len") + } - if len(link.Objects[0].Links) != 1 { - errOut <- errors.New("unexpected Links len") - return - } + if len(link.Objects[0].Links) != 1 { + return errors.New("unexpected Links len") + } - l0 := link.Objects[0].Links[0] + l0 := link.Objects[0].Links[0] - c, err := cid.Decode(l0.Hash) - if err != nil { - errOut <- err - return - } + c, err := cid.Decode(l0.Hash) + if err != nil { + return err + } - var ftype iface.FileType - switch l0.Type { - case unixfs.TRaw, unixfs.TFile: - ftype = iface.TFile - case unixfs.THAMTShard, unixfs.TDirectory, unixfs.TMetadata: - ftype = iface.TDirectory - case unixfs.TSymlink: - ftype = iface.TSymlink - } + var ftype iface.FileType + switch l0.Type { + case unixfs.TRaw, unixfs.TFile: + ftype = iface.TFile + case unixfs.THAMTShard, unixfs.TDirectory, unixfs.TMetadata: + ftype = iface.TDirectory + case unixfs.TSymlink: + ftype = iface.TSymlink + } - select { - case out <- iface.DirEntry{ - Name: l0.Name, - Cid: c, - Size: l0.Size, - Type: ftype, - Target: l0.Target, - - Mode: l0.Mode, - ModTime: l0.ModTime, - }: - case <-ctx.Done(): - } + select { + case out <- iface.DirEntry{ + Name: l0.Name, + Cid: c, + Size: l0.Size, + Type: ftype, + Target: l0.Target, + + Mode: l0.Mode, + ModTime: l0.ModTime, + }: + case <-ctx.Done(): + return ctx.Err() } - }() + } - return out, errOut + return nil } func (api *UnixfsAPI) core() *HttpApi { diff --git a/core/commands/ls.go b/core/commands/ls.go index 15db7ff53d7..da1d9ebc530 100644 --- a/core/commands/ls.go +++ b/core/commands/ls.go @@ -133,14 +133,21 @@ The JSON output contains type information. } } + lsCtx, cancel := context.WithCancel(req.Context) + defer cancel() + for i, fpath := range paths { pth, err := cmdutils.PathOrCidPath(fpath) if err != nil { return err } - results, errCh := api.Unixfs().Ls(req.Context, pth, - options.Unixfs.ResolveChildren(resolveSize || resolveType)) + results := make(chan iface.DirEntry) + var lsErr error + go func() { + lsErr = api.Unixfs().Ls(lsCtx, pth, results, + options.Unixfs.ResolveChildren(resolveSize || resolveType)) + }() processLink, dirDone = processDir() for link := range results { @@ -164,12 +171,12 @@ The JSON output contains type information. Mode: link.Mode, ModTime: link.ModTime, } - if err := processLink(paths[i], lsLink); err != nil { + if err = processLink(paths[i], lsLink); err != nil { return err } } - if err = <-errCh; err != nil { - return err + if lsErr != nil { + return lsErr } dirDone(i) } diff --git a/core/commands/pin/pin.go b/core/commands/pin/pin.go index bd9c43a8326..a73e352e268 100644 --- a/core/commands/pin/pin.go +++ b/core/commands/pin/pin.go @@ -557,7 +557,15 @@ func pinLsAll(req *cmds.Request, typeStr string, detailed bool, name string, api panic("unhandled pin type") } - pins, errCh := api.Pin().Ls(req.Context, opt, options.Pin.Ls.Detailed(detailed), options.Pin.Ls.Name(name)) + var lsErr error + pins := make(chan iface.Pin) + lsCtx, cancel := context.WithCancel(req.Context) + defer cancel() + + go func() { + lsErr = api.Pin().Ls(lsCtx, opt, pins, options.Pin.Ls.Detailed(detailed), options.Pin.Ls.Name(name)) + }() + for p := range pins { err = emit(PinLsOutputWrapper{ PinLsObject: PinLsObject{ @@ -570,7 +578,7 @@ func pinLsAll(req *cmds.Request, typeStr string, detailed bool, name string, api return err } } - return <-errCh + return lsErr } const ( diff --git a/core/commands/pin/remotepin.go b/core/commands/pin/remotepin.go index 40cb515c5f3..7319bcd9644 100644 --- a/core/commands/pin/remotepin.go +++ b/core/commands/pin/remotepin.go @@ -313,7 +313,7 @@ Pass '--status=queued,pinning,pinned,failed' to list pins in all states. } // Executes GET /pins/?query-with-filters -func lsRemote(ctx context.Context, req *cmds.Request, c *pinclient.Client) (<-chan pinclient.PinStatusGetter, <-chan error) { +func lsRemote(ctx context.Context, req *cmds.Request, c *pinclient.Client, out chan<- pinclient.PinStatusGetter) error { opts := []pinclient.LsOption{} if name, nameFound := req.Options[pinNameOptionName]; nameFound { nameStr := name.(string) @@ -326,12 +326,8 @@ func lsRemote(ctx context.Context, req *cmds.Request, c *pinclient.Client) (<-ch for _, rawCID := range cidsRawArr { parsedCID, err := cid.Decode(rawCID) if err != nil { - psCh := make(chan pinclient.PinStatusGetter) - errCh := make(chan error, 1) - errCh <- fmt.Errorf("CID %q cannot be parsed: %v", rawCID, err) - close(psCh) - close(errCh) - return psCh, errCh + close(out) + return fmt.Errorf("CID %q cannot be parsed: %v", rawCID, err) } parsedCIDs = append(parsedCIDs, parsedCID) } @@ -343,19 +339,15 @@ func lsRemote(ctx context.Context, req *cmds.Request, c *pinclient.Client) (<-ch for _, rawStatus := range statusRawArr { s := pinclient.Status(rawStatus) if s.String() == string(pinclient.StatusUnknown) { - psCh := make(chan pinclient.PinStatusGetter) - errCh := make(chan error, 1) - errCh <- fmt.Errorf("status %q is not valid", rawStatus) - close(psCh) - close(errCh) - return psCh, errCh + close(out) + return fmt.Errorf("status %q is not valid", rawStatus) } parsedStatuses = append(parsedStatuses, s) } opts = append(opts, pinclient.PinOpts.FilterStatus(parsedStatuses...)) } - return c.Ls(ctx, opts...) + return c.Ls(ctx, out, opts...) } var rmRemotePinCmd = &cmds.Command{ diff --git a/core/coreapi/pin.go b/core/coreapi/pin.go index 838c8a7b554..e2ba714aabf 100644 --- a/core/coreapi/pin.go +++ b/core/coreapi/pin.go @@ -51,18 +51,14 @@ func (api *PinAPI) Add(ctx context.Context, p path.Path, opts ...caopts.PinAddOp return api.pinning.Flush(ctx) } -func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan coreiface.Pin, <-chan error) { +func (api *PinAPI) Ls(ctx context.Context, pins chan<- coreiface.Pin, opts ...caopts.PinLsOption) error { ctx, span := tracing.Span(ctx, "CoreAPI.PinAPI", "Ls") defer span.End() settings, err := caopts.PinLsOptions(opts...) if err != nil { - outCh := make(chan coreiface.Pin) - errCh := make(chan error, 1) - errCh <- err - close(outCh) - close(errCh) - return outCh, errCh + close(pins) + return err } span.SetAttributes(attribute.String("type", settings.Type)) @@ -70,15 +66,11 @@ func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan c switch settings.Type { case "all", "direct", "indirect", "recursive": default: - outCh := make(chan coreiface.Pin) - errCh := make(chan error, 1) - errCh <- fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", settings.Type) - close(outCh) - close(errCh) - return outCh, errCh + close(pins) + return fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", settings.Type) } - return api.pinLsAll(ctx, settings.Type, settings.Detailed, settings.Name) + return api.pinLsAll(ctx, settings.Type, settings.Detailed, settings.Name, pins) } func (api *PinAPI) IsPinned(ctx context.Context, p path.Path, opts ...caopts.PinIsPinnedOption) (string, bool, error) { @@ -283,8 +275,8 @@ func (p *pinInfo) Name() string { // // The caller must keep reading results until the channel is closed to prevent // leaking the goroutine that is fetching pins. -func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string, detailed bool, name string) (<-chan coreiface.Pin, <-chan error) { - out := make(chan coreiface.Pin, 1) +func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string, detailed bool, name string, out chan<- coreiface.Pin) error { + defer close(out) emittedSet := cid.NewSet() AddToResultKeys := func(c cid.Cid, pinName, typeStr string) error { @@ -302,90 +294,82 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string, detailed bool, return nil } - errOut := make(chan error, 1) - - go func() { - defer close(out) - defer close(errOut) - - var rkeys []cid.Cid - var err error - if typeStr == "recursive" || typeStr == "all" { - for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) { - if streamedCid.Err != nil { - errOut <- streamedCid.Err - return - } - if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "recursive"); err != nil { - errOut <- err - return - } - rkeys = append(rkeys, streamedCid.Pin.Key) + var rkeys []cid.Cid + var err error + if typeStr == "recursive" || typeStr == "all" { + for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) { + if streamedCid.Err != nil { + return streamedCid.Err } + if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "recursive"); err != nil { + return err + } + rkeys = append(rkeys, streamedCid.Pin.Key) } - if typeStr == "direct" || typeStr == "all" { - for streamedCid := range api.pinning.DirectKeys(ctx, detailed) { - if streamedCid.Err != nil { - errOut <- streamedCid.Err - return - } - if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "direct"); err != nil { - errOut <- err - return - } + } + if typeStr == "direct" || typeStr == "all" { + for streamedCid := range api.pinning.DirectKeys(ctx, detailed) { + if streamedCid.Err != nil { + return streamedCid.Err + } + if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "direct"); err != nil { + return err } } - if typeStr == "indirect" { - // We need to first visit the direct pins that have priority - // without emitting them - - for streamedCid := range api.pinning.DirectKeys(ctx, detailed) { - if streamedCid.Err != nil { - errOut <- streamedCid.Err - return - } - emittedSet.Add(streamedCid.Pin.Key) + } + if typeStr == "indirect" { + // We need to first visit the direct pins that have priority + // without emitting them + + for streamedCid := range api.pinning.DirectKeys(ctx, detailed) { + if streamedCid.Err != nil { + return streamedCid.Err } + emittedSet.Add(streamedCid.Pin.Key) + } - for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) { - if streamedCid.Err != nil { - errOut <- streamedCid.Err - return - } - emittedSet.Add(streamedCid.Pin.Key) - rkeys = append(rkeys, streamedCid.Pin.Key) + for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) { + if streamedCid.Err != nil { + return streamedCid.Err } + emittedSet.Add(streamedCid.Pin.Key) + rkeys = append(rkeys, streamedCid.Pin.Key) + } + } + if typeStr == "indirect" || typeStr == "all" { + if len(rkeys) == 0 { + return nil } - if typeStr == "indirect" || typeStr == "all" { - walkingSet := cid.NewSet() - for _, k := range rkeys { - err = merkledag.Walk( - ctx, merkledag.GetLinksWithDAG(api.dag), k, - func(c cid.Cid) bool { - if !walkingSet.Visit(c) { - return false - } - if emittedSet.Has(c) { - return true // skipped - } - err := AddToResultKeys(c, "", "indirect") - if err != nil { - errOut <- err - return false - } - return true - }, - merkledag.SkipRoot(), merkledag.Concurrent(), - ) - if err != nil { - errOut <- err - return - } + var addErr error + walkingSet := cid.NewSet() + for _, k := range rkeys { + err = merkledag.Walk( + ctx, merkledag.GetLinksWithDAG(api.dag), k, + func(c cid.Cid) bool { + if !walkingSet.Visit(c) { + return false + } + if emittedSet.Has(c) { + return true // skipped + } + addErr := AddToResultKeys(c, "", "indirect") + if addErr != nil { + return false + } + return true + }, + merkledag.SkipRoot(), merkledag.Concurrent(), + ) + if err != nil { + return err + } + if addErr != nil { + return addErr } } - }() + } - return out, errOut + return nil } func (api *PinAPI) core() coreiface.CoreAPI { diff --git a/core/coreiface/pin.go b/core/coreiface/pin.go index 39cfc5941da..e0fd2fb90ed 100644 --- a/core/coreiface/pin.go +++ b/core/coreiface/pin.go @@ -47,8 +47,9 @@ type PinAPI interface { // tree Add(context.Context, path.Path, ...options.PinAddOption) error - // Ls returns list of pinned objects on this node - Ls(context.Context, ...options.PinLsOption) (<-chan Pin, <-chan error) + // Ls returns this node's pinned objects on the provided channel. The + // channel is closed when there are no more pins and an error is returned. + Ls(context.Context, chan<- Pin, ...options.PinLsOption) error // IsPinned returns whether or not the given cid is pinned // and an explanation of why its pinned diff --git a/core/coreiface/tests/block.go b/core/coreiface/tests/block.go index f59bf1fa871..2b5a68a63b8 100644 --- a/core/coreiface/tests/block.go +++ b/core/coreiface/tests/block.go @@ -323,12 +323,15 @@ func (tp *TestSuite) TestBlockPin(t *testing.T) { t.Fatal(err) } - pinsCh, errCh := api.Pin().Ls(ctx) - _, ok := <-pinsCh - if ok { + pinCh := make(chan coreiface.Pin) + go func() { + err = api.Pin().Ls(ctx, pinCh) + }() + + for range pinCh { t.Fatal("expected 0 pins") } - if err = <-errCh; err != nil { + if err != nil { t.Fatal(err) } @@ -342,7 +345,7 @@ func (tp *TestSuite) TestBlockPin(t *testing.T) { t.Fatal(err) } - pins, err := accPins(api.Pin().Ls(ctx)) + pins, err := accPins(ctx, api) if err != nil { t.Fatal(err) } diff --git a/core/coreiface/tests/pin.go b/core/coreiface/tests/pin.go index a134854931c..4c606323fec 100644 --- a/core/coreiface/tests/pin.go +++ b/core/coreiface/tests/pin.go @@ -67,7 +67,7 @@ func (tp *TestSuite) TestPinSimple(t *testing.T) { t.Fatal(err) } - list, err := accPins(api.Pin().Ls(ctx)) + list, err := accPins(ctx, api) if err != nil { t.Fatal(err) } @@ -91,7 +91,7 @@ func (tp *TestSuite) TestPinSimple(t *testing.T) { t.Fatal(err) } - list, err = accPins(api.Pin().Ls(ctx)) + list, err = accPins(ctx, api) if err != nil { t.Fatal(err) } @@ -143,7 +143,7 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Fatal(err) } - list, err := accPins(api.Pin().Ls(ctx)) + list, err := accPins(ctx, api) if err != nil { t.Fatal(err) } @@ -152,7 +152,7 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Errorf("unexpected pin list len: %d", len(list)) } - list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Direct())) + list, err = accPins(ctx, api, opt.Pin.Ls.Direct()) if err != nil { t.Fatal(err) } @@ -165,7 +165,7 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.FromCid(nd3.Cid()).String()) } - list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Recursive())) + list, err = accPins(ctx, api, opt.Pin.Ls.Recursive()) if err != nil { t.Fatal(err) } @@ -178,7 +178,7 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.FromCid(nd2.Cid()).String()) } - list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Indirect())) + list, err = accPins(ctx, api, opt.Pin.Ls.Indirect()) if err != nil { t.Fatal(err) } @@ -436,21 +436,21 @@ func getThreeChainedNodes(t *testing.T, ctx context.Context, api iface.CoreAPI, func assertPinTypes(t *testing.T, ctx context.Context, api iface.CoreAPI, recusive, direct, indirect []cidContainer) { assertPinLsAllConsistency(t, ctx, api) - list, err := accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Recursive())) + list, err := accPins(ctx, api, opt.Pin.Ls.Recursive()) if err != nil { t.Fatal(err) } assertPinCids(t, list, recusive...) - list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Direct())) + list, err = accPins(ctx, api, opt.Pin.Ls.Direct()) if err != nil { t.Fatal(err) } assertPinCids(t, list, direct...) - list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Indirect())) + list, err = accPins(ctx, api, opt.Pin.Ls.Indirect()) if err != nil { t.Fatal(err) } @@ -500,7 +500,7 @@ func assertPinCids(t *testing.T, pins []iface.Pin, cids ...cidContainer) { // assertPinLsAllConsistency verifies that listing all pins gives the same result as listing the pin types individually func assertPinLsAllConsistency(t *testing.T, ctx context.Context, api iface.CoreAPI) { t.Helper() - allPins, err := accPins(api.Pin().Ls(ctx)) + allPins, err := accPins(ctx, api) if err != nil { t.Fatal(err) } @@ -531,7 +531,7 @@ func assertPinLsAllConsistency(t *testing.T, ctx context.Context, api iface.Core } for typeStr, pinProps := range typeMap { - pins, err := accPins(api.Pin().Ls(ctx, pinProps.PinLsOption)) + pins, err := accPins(ctx, api, pinProps.PinLsOption) if err != nil { t.Fatal(err) } @@ -593,15 +593,19 @@ func assertNotPinned(t *testing.T, ctx context.Context, api iface.CoreAPI, p pat } } -func accPins(pins <-chan iface.Pin, errs <-chan error) ([]iface.Pin, error) { - var results []iface.Pin +func accPins(ctx context.Context, api iface.CoreAPI, opts ...opt.PinLsOption) ([]iface.Pin, error) { + var err error + pins := make(chan iface.Pin) + go func() { + err = api.Pin().Ls(ctx, pins, opts...) + }() + var results []iface.Pin for pin := range pins { results = append(results, pin) } - if err := <-errs; err != nil { + if err != nil { return nil, err } - return results, nil } diff --git a/core/coreiface/tests/unixfs.go b/core/coreiface/tests/unixfs.go index 1ff4f6f2c5a..e806ae9baef 100644 --- a/core/coreiface/tests/unixfs.go +++ b/core/coreiface/tests/unixfs.go @@ -544,7 +544,7 @@ func (tp *TestSuite) TestAddPinned(t *testing.T) { t.Fatal(err) } - pins, err := accPins(api.Pin().Ls(ctx)) + pins, err := accPins(ctx, api) if err != nil { t.Fatal(err) }