From a8a6bbe929b0a821d854a20e3f5ed68f6cfe601c Mon Sep 17 00:00:00 2001
From: Henrique Dias <hacdias@gmail.com>
Date: Thu, 4 Jan 2024 14:25:06 +0100
Subject: [PATCH] feat: support optional pin names (#10261)

---
 client/rpc/pin.go                      |  8 +++-
 cmd/ipfs/kubo/init.go                  |  2 +-
 core/commands/dag/import.go            |  2 +-
 core/commands/pin/pin.go               | 66 ++++++++++++++++++++------
 core/coreapi/block.go                  |  2 +-
 core/coreapi/dag.go                    |  4 +-
 core/coreapi/object.go                 |  2 +-
 core/coreapi/pin.go                    | 40 +++++++++-------
 core/coreiface/options/pin.go          | 21 +++++++-
 core/coreiface/pin.go                  |  3 ++
 core/coreunix/add.go                   |  2 +-
 docs/changelogs/v0.26.md               |  5 ++
 docs/examples/kubo-as-a-library/go.mod |  2 +-
 docs/examples/kubo-as-a-library/go.sum |  4 +-
 fuse/ipns/common.go                    |  2 +-
 gc/gc.go                               | 16 +++----
 gc/gc_test.go                          |  4 +-
 go.mod                                 |  2 +-
 go.sum                                 |  4 +-
 test/cli/pins_test.go                  | 63 ++++++++++++++++++++++++
 test/dependencies/go.mod               |  2 +-
 test/dependencies/go.sum               |  4 +-
 22 files changed, 200 insertions(+), 60 deletions(-)

diff --git a/client/rpc/pin.go b/client/rpc/pin.go
index 632d8f08d..a0469861c 100644
--- a/client/rpc/pin.go
+++ b/client/rpc/pin.go
@@ -26,6 +26,7 @@ type pinRefKeyList struct {
 type pin struct {
 	path path.ImmutablePath
 	typ  string
+	name string
 	err  error
 }
 
@@ -37,6 +38,10 @@ func (p pin) Path() path.ImmutablePath {
 	return p.path
 }
 
+func (p pin) Name() string {
+	return p.name
+}
+
 func (p pin) Type() string {
 	return p.typ
 }
@@ -53,6 +58,7 @@ func (api *PinAPI) Add(ctx context.Context, p path.Path, opts ...caopts.PinAddOp
 
 type pinLsObject struct {
 	Cid  string
+	Name string
 	Type string
 }
 
@@ -102,7 +108,7 @@ func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan i
 			}
 
 			select {
-			case ch <- pin{typ: out.Type, path: path.FromCid(c)}:
+			case ch <- pin{typ: out.Type, name: out.Name, path: path.FromCid(c)}:
 			case <-ctx.Done():
 				return
 			}
diff --git a/cmd/ipfs/kubo/init.go b/cmd/ipfs/kubo/init.go
index 47aee7aeb..986fe90c8 100644
--- a/cmd/ipfs/kubo/init.go
+++ b/cmd/ipfs/kubo/init.go
@@ -252,7 +252,7 @@ func initializeIpnsKeyspace(repoRoot string) error {
 
 	// pin recursively because this might already be pinned
 	// and doing a direct pin would throw an error in that case
-	err = nd.Pinning.Pin(ctx, emptyDir, true)
+	err = nd.Pinning.Pin(ctx, emptyDir, true, "")
 	if err != nil {
 		return err
 	}
diff --git a/core/commands/dag/import.go b/core/commands/dag/import.go
index d95ff7198..5e39393c1 100644
--- a/core/commands/dag/import.go
+++ b/core/commands/dag/import.go
@@ -152,7 +152,7 @@ func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
 				ret.PinErrorMsg = err.Error()
 			} else if nd, err := blockDecoder.DecodeNode(req.Context, block); err != nil {
 				ret.PinErrorMsg = err.Error()
-			} else if err := node.Pinning.Pin(req.Context, nd, true); err != nil {
+			} else if err := node.Pinning.Pin(req.Context, nd, true, ""); err != nil {
 				ret.PinErrorMsg = err.Error()
 			} else if err := node.Pinning.Flush(req.Context); err != nil {
 				ret.PinErrorMsg = err.Error()
diff --git a/core/commands/pin/pin.go b/core/commands/pin/pin.go
index db623a7e6..d75d5d386 100644
--- a/core/commands/pin/pin.go
+++ b/core/commands/pin/pin.go
@@ -57,6 +57,28 @@ var addPinCmd = &cmds.Command{
 	Helptext: cmds.HelpText{
 		Tagline:          "Pin objects to local storage.",
 		ShortDescription: "Stores an IPFS object(s) from a given path locally to disk.",
+		LongDescription: `
+Create a pin for the given object, protecting resolved CID from being garbage
+collected.
+
+An optional name can be provided, and read back via 'ipfs pin ls --names'.
+
+Be mindful of defaults:
+
+Default pin type is 'recursive' (entire DAG).
+Pass -r=false to create a direct pin for a single block.
+Use 'pin ls -t recursive' to only list roots of recursively pinned DAGs
+(significantly faster when many big DAGs are pinned recursively)
+
+Default pin name is empty. Pass '--name' to 'pin add' to set one
+and use 'pin ls --names' to see it.
+Pin add is idempotent: pinning CID which is already pinned won't change
+the name, value passed with '--name' with the original pin is preserved.
+To rename pin, use 'pin rm' and 'pin add --name'.
+
+If daemon is running, any missing blocks will be retrieved from the network.
+It may take some time. Pass '--progress' to track the progress.
+`,
 	},
 
 	Arguments: []cmds.Argument{
@@ -64,6 +86,7 @@ var addPinCmd = &cmds.Command{
 	},
 	Options: []cmds.Option{
 		cmds.BoolOption(pinRecursiveOptionName, "r", "Recursively pin the object linked to by the specified object(s).").WithDefault(true),
+		cmds.StringOption(pinNameOptionName, "n", "An optional name for created pin(s)."),
 		cmds.BoolOption(pinProgressOptionName, "Show progress"),
 	},
 	Type: AddPinOutput{},
@@ -75,6 +98,7 @@ var addPinCmd = &cmds.Command{
 
 		// set recursive flag
 		recursive, _ := req.Options[pinRecursiveOptionName].(bool)
+		name, _ := req.Options[pinNameOptionName].(string)
 		showProgress, _ := req.Options[pinProgressOptionName].(bool)
 
 		if err := req.ParseBodyArgs(); err != nil {
@@ -87,7 +111,7 @@ var addPinCmd = &cmds.Command{
 		}
 
 		if !showProgress {
-			added, err := pinAddMany(req.Context, api, enc, req.Arguments, recursive)
+			added, err := pinAddMany(req.Context, api, enc, req.Arguments, recursive, name)
 			if err != nil {
 				return err
 			}
@@ -105,7 +129,7 @@ var addPinCmd = &cmds.Command{
 
 		ch := make(chan pinResult, 1)
 		go func() {
-			added, err := pinAddMany(ctx, api, enc, req.Arguments, recursive)
+			added, err := pinAddMany(ctx, api, enc, req.Arguments, recursive, name)
 			ch <- pinResult{pins: added, err: err}
 		}()
 
@@ -181,7 +205,7 @@ var addPinCmd = &cmds.Command{
 	},
 }
 
-func pinAddMany(ctx context.Context, api coreiface.CoreAPI, enc cidenc.Encoder, paths []string, recursive bool) ([]string, error) {
+func pinAddMany(ctx context.Context, api coreiface.CoreAPI, enc cidenc.Encoder, paths []string, recursive bool, name string) ([]string, error) {
 	added := make([]string, len(paths))
 	for i, b := range paths {
 		p, err := cmdutils.PathOrCidPath(b)
@@ -194,7 +218,7 @@ func pinAddMany(ctx context.Context, api coreiface.CoreAPI, enc cidenc.Encoder,
 			return nil, err
 		}
 
-		if err := api.Pin().Add(ctx, rp, options.Pin.Recursive(recursive)); err != nil {
+		if err := api.Pin().Add(ctx, rp, options.Pin.Recursive(recursive), options.Pin.Name(name)); err != nil {
 			return nil, err
 		}
 		added[i] = enc.Encode(rp.RootCid())
@@ -281,6 +305,7 @@ const (
 	pinTypeOptionName   = "type"
 	pinQuietOptionName  = "quiet"
 	pinStreamOptionName = "stream"
+	pinNamesOptionName  = "names"
 )
 
 var listPinCmd = &cmds.Command{
@@ -294,6 +319,7 @@ respectively.
 `,
 		LongDescription: `
 Returns a list of objects that are pinned locally.
+
 By default, all pinned objects are returned, but the '--type' flag or
 arguments can restrict that to a specific pin type or to some specific objects
 respectively.
@@ -302,10 +328,13 @@ Use --type=<type> to specify the type of pinned keys to list.
 Valid values are:
     * "direct": pin that specific object.
     * "recursive": pin that specific object, and indirectly pin all its
-    	descendants
+      descendants
     * "indirect": pinned indirectly by an ancestor (like a refcount)
     * "all"
 
+By default, pin names are not included (returned as empty).
+Pass '--names' flag to return pin names (set with '--name' from 'pin add').
+
 With arguments, the command fails if any of the arguments is not a pinned
 object. And if --type=<type> is additionally used, the command will also fail
 if any of the arguments is not of the specified type.
@@ -334,6 +363,7 @@ Example:
 		cmds.StringOption(pinTypeOptionName, "t", "The type of pinned keys to list. Can be \"direct\", \"indirect\", \"recursive\", or \"all\".").WithDefault("all"),
 		cmds.BoolOption(pinQuietOptionName, "q", "Write just hashes of objects."),
 		cmds.BoolOption(pinStreamOptionName, "s", "Enable streaming of pins as they are discovered."),
+		cmds.BoolOption(pinNamesOptionName, "n", "Enable displaying pin names (slower)."),
 	},
 	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
 		api, err := cmdenv.GetApi(env, req)
@@ -343,6 +373,7 @@ Example:
 
 		typeStr, _ := req.Options[pinTypeOptionName].(string)
 		stream, _ := req.Options[pinStreamOptionName].(bool)
+		displayNames, _ := req.Options[pinNamesOptionName].(bool)
 
 		switch typeStr {
 		case "all", "direct", "indirect", "recursive":
@@ -356,7 +387,7 @@ Example:
 		lgcList := map[string]PinLsType{}
 		if !stream {
 			emit = func(v PinLsOutputWrapper) error {
-				lgcList[v.PinLsObject.Cid] = PinLsType{Type: v.PinLsObject.Type}
+				lgcList[v.PinLsObject.Cid] = PinLsType{Type: v.PinLsObject.Type, Name: v.PinLsObject.Name}
 				return nil
 			}
 		} else {
@@ -368,7 +399,7 @@ Example:
 		if len(req.Arguments) > 0 {
 			err = pinLsKeys(req, typeStr, api, emit)
 		} else {
-			err = pinLsAll(req, typeStr, api, emit)
+			err = pinLsAll(req, typeStr, displayNames, api, emit)
 		}
 		if err != nil {
 			return err
@@ -402,8 +433,10 @@ Example:
 			if stream {
 				if quiet {
 					fmt.Fprintf(w, "%s\n", out.PinLsObject.Cid)
-				} else {
+				} else if out.PinLsObject.Name == "" {
 					fmt.Fprintf(w, "%s %s\n", out.PinLsObject.Cid, out.PinLsObject.Type)
+				} else {
+					fmt.Fprintf(w, "%s %s %s\n", out.PinLsObject.Cid, out.PinLsObject.Type, out.PinLsObject.Name)
 				}
 				return nil
 			}
@@ -411,8 +444,10 @@ Example:
 			for k, v := range out.PinLsList.Keys {
 				if quiet {
 					fmt.Fprintf(w, "%s\n", k)
-				} else {
+				} else if v.Name == "" {
 					fmt.Fprintf(w, "%s %s\n", k, v.Type)
+				} else {
+					fmt.Fprintf(w, "%s %s %s\n", k, v.Type, v.Name)
 				}
 			}
 
@@ -437,11 +472,13 @@ type PinLsList struct {
 // PinLsType contains the type of a pin
 type PinLsType struct {
 	Type string
+	Name string
 }
 
 // PinLsObject contains the description of a pin
 type PinLsObject struct {
 	Cid  string `json:",omitempty"`
+	Name string `json:",omitempty"`
 	Type string `json:",omitempty"`
 }
 
@@ -502,7 +539,7 @@ func pinLsKeys(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fu
 	return nil
 }
 
-func pinLsAll(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit func(value PinLsOutputWrapper) error) error {
+func pinLsAll(req *cmds.Request, typeStr string, detailed bool, api coreiface.CoreAPI, emit func(value PinLsOutputWrapper) error) error {
 	enc, err := cmdenv.GetCidEncoder(req)
 	if err != nil {
 		return err
@@ -520,7 +557,7 @@ func pinLsAll(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fun
 		panic("unhandled pin type")
 	}
 
-	pins, err := api.Pin().Ls(req.Context, opt)
+	pins, err := api.Pin().Ls(req.Context, opt, options.Pin.Ls.Detailed(detailed))
 	if err != nil {
 		return err
 	}
@@ -532,6 +569,7 @@ func pinLsAll(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fun
 		err = emit(PinLsOutputWrapper{
 			PinLsObject: PinLsObject{
 				Type: p.Type(),
+				Name: p.Name(),
 				Cid:  enc.Encode(p.Path().RootCid()),
 			},
 		})
@@ -748,15 +786,15 @@ func pinVerify(ctx context.Context, n *core.IpfsNode, opts pinVerifyOpts, enc ci
 	out := make(chan any)
 	go func() {
 		defer close(out)
-		for p := range n.Pinning.RecursiveKeys(ctx) {
+		for p := range n.Pinning.RecursiveKeys(ctx, false) {
 			if p.Err != nil {
 				out <- PinVerifyRes{Err: p.Err.Error()}
 				return
 			}
-			pinStatus := checkPin(p.C)
+			pinStatus := checkPin(p.Pin.Key)
 			if !pinStatus.Ok || opts.includeOk {
 				select {
-				case out <- PinVerifyRes{Cid: enc.Encode(p.C), PinStatus: pinStatus}:
+				case out <- PinVerifyRes{Cid: enc.Encode(p.Pin.Key), PinStatus: pinStatus}:
 				case <-ctx.Done():
 					return
 				}
diff --git a/core/coreapi/block.go b/core/coreapi/block.go
index 0c5597c8d..b386ecd0a 100644
--- a/core/coreapi/block.go
+++ b/core/coreapi/block.go
@@ -60,7 +60,7 @@ func (api *BlockAPI) Put(ctx context.Context, src io.Reader, opts ...caopts.Bloc
 	}
 
 	if settings.Pin {
-		if err = api.pinning.PinWithMode(ctx, b.Cid(), pin.Recursive); err != nil {
+		if err = api.pinning.PinWithMode(ctx, b.Cid(), pin.Recursive, ""); err != nil {
 			return nil, err
 		}
 		if err := api.pinning.Flush(ctx); err != nil {
diff --git a/core/coreapi/dag.go b/core/coreapi/dag.go
index c4d0dc9d2..70686f62e 100644
--- a/core/coreapi/dag.go
+++ b/core/coreapi/dag.go
@@ -30,7 +30,7 @@ func (adder *pinningAdder) Add(ctx context.Context, nd ipld.Node) error {
 		return err
 	}
 
-	if err := adder.pinning.PinWithMode(ctx, nd.Cid(), pin.Recursive); err != nil {
+	if err := adder.pinning.PinWithMode(ctx, nd.Cid(), pin.Recursive, ""); err != nil {
 		return err
 	}
 
@@ -51,7 +51,7 @@ func (adder *pinningAdder) AddMany(ctx context.Context, nds []ipld.Node) error {
 	for _, nd := range nds {
 		c := nd.Cid()
 		if cids.Visit(c) {
-			if err := adder.pinning.PinWithMode(ctx, c, pin.Recursive); err != nil {
+			if err := adder.pinning.PinWithMode(ctx, c, pin.Recursive, ""); err != nil {
 				return err
 			}
 		}
diff --git a/core/coreapi/object.go b/core/coreapi/object.go
index e8f94b1d5..fca98bc5f 100644
--- a/core/coreapi/object.go
+++ b/core/coreapi/object.go
@@ -133,7 +133,7 @@ func (api *ObjectAPI) Put(ctx context.Context, src io.Reader, opts ...caopts.Obj
 	}
 
 	if options.Pin {
-		if err := api.pinning.PinWithMode(ctx, dagnode.Cid(), pin.Recursive); err != nil {
+		if err := api.pinning.PinWithMode(ctx, dagnode.Cid(), pin.Recursive, ""); err != nil {
 			return path.ImmutablePath{}, err
 		}
 
diff --git a/core/coreapi/pin.go b/core/coreapi/pin.go
index 5cb92a819..8db582a4f 100644
--- a/core/coreapi/pin.go
+++ b/core/coreapi/pin.go
@@ -38,7 +38,7 @@ func (api *PinAPI) Add(ctx context.Context, p path.Path, opts ...caopts.PinAddOp
 
 	defer api.blockstore.PinLock(ctx).Unlock(ctx)
 
-	err = api.pinning.Pin(ctx, dagNode, settings.Recursive)
+	err = api.pinning.Pin(ctx, dagNode, settings.Recursive, settings.Name)
 	if err != nil {
 		return fmt.Errorf("pin: %s", err)
 	}
@@ -67,7 +67,7 @@ func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan c
 		return nil, fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", settings.Type)
 	}
 
-	return api.pinLsAll(ctx, settings.Type), nil
+	return api.pinLsAll(ctx, settings.Type, settings.Detailed), nil
 }
 
 func (api *PinAPI) IsPinned(ctx context.Context, p path.Path, opts ...caopts.PinIsPinnedOption) (string, bool, error) {
@@ -231,12 +231,12 @@ func (api *PinAPI) Verify(ctx context.Context) (<-chan coreiface.PinStatus, erro
 	out := make(chan coreiface.PinStatus)
 	go func() {
 		defer close(out)
-		for p := range api.pinning.RecursiveKeys(ctx) {
+		for p := range api.pinning.RecursiveKeys(ctx, false) {
 			var res *pinStatus
 			if p.Err != nil {
 				res = &pinStatus{err: p.Err}
 			} else {
-				res = checkPin(p.C)
+				res = checkPin(p.Pin.Key)
 			}
 			select {
 			case <-ctx.Done():
@@ -252,6 +252,7 @@ func (api *PinAPI) Verify(ctx context.Context) (<-chan coreiface.PinStatus, erro
 type pinInfo struct {
 	pinType string
 	path    path.ImmutablePath
+	name    string
 	err     error
 }
 
@@ -263,6 +264,10 @@ func (p *pinInfo) Type() string {
 	return p.pinType
 }
 
+func (p *pinInfo) Name() string {
+	return p.name
+}
+
 func (p *pinInfo) Err() error {
 	return p.err
 }
@@ -271,16 +276,17 @@ func (p *pinInfo) Err() error {
 //
 // 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) <-chan coreiface.Pin {
+func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string, detailed bool) <-chan coreiface.Pin {
 	out := make(chan coreiface.Pin, 1)
 
 	emittedSet := cid.NewSet()
 
-	AddToResultKeys := func(c cid.Cid, typeStr string) error {
+	AddToResultKeys := func(c cid.Cid, name, typeStr string) error {
 		if emittedSet.Visit(c) {
 			select {
 			case out <- &pinInfo{
 				pinType: typeStr,
+				name:    name,
 				path:    path.FromCid(c),
 			}:
 			case <-ctx.Done():
@@ -296,25 +302,25 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreifac
 		var rkeys []cid.Cid
 		var err error
 		if typeStr == "recursive" || typeStr == "all" {
-			for streamedCid := range api.pinning.RecursiveKeys(ctx) {
+			for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				if err = AddToResultKeys(streamedCid.C, "recursive"); err != nil {
+				if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "recursive"); err != nil {
 					out <- &pinInfo{err: err}
 					return
 				}
-				rkeys = append(rkeys, streamedCid.C)
+				rkeys = append(rkeys, streamedCid.Pin.Key)
 			}
 		}
 		if typeStr == "direct" || typeStr == "all" {
-			for streamedCid := range api.pinning.DirectKeys(ctx) {
+			for streamedCid := range api.pinning.DirectKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				if err = AddToResultKeys(streamedCid.C, "direct"); err != nil {
+				if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "direct"); err != nil {
 					out <- &pinInfo{err: err}
 					return
 				}
@@ -324,21 +330,21 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreifac
 			// We need to first visit the direct pins that have priority
 			// without emitting them
 
-			for streamedCid := range api.pinning.DirectKeys(ctx) {
+			for streamedCid := range api.pinning.DirectKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				emittedSet.Add(streamedCid.C)
+				emittedSet.Add(streamedCid.Pin.Key)
 			}
 
-			for streamedCid := range api.pinning.RecursiveKeys(ctx) {
+			for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				emittedSet.Add(streamedCid.C)
-				rkeys = append(rkeys, streamedCid.C)
+				emittedSet.Add(streamedCid.Pin.Key)
+				rkeys = append(rkeys, streamedCid.Pin.Key)
 			}
 		}
 		if typeStr == "indirect" || typeStr == "all" {
@@ -353,7 +359,7 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreifac
 						if emittedSet.Has(c) {
 							return true // skipped
 						}
-						err := AddToResultKeys(c, "indirect")
+						err := AddToResultKeys(c, "", "indirect")
 						if err != nil {
 							out <- &pinInfo{err: err}
 							return false
diff --git a/core/coreiface/options/pin.go b/core/coreiface/options/pin.go
index 75c2b8a26..0efd853ef 100644
--- a/core/coreiface/options/pin.go
+++ b/core/coreiface/options/pin.go
@@ -5,11 +5,13 @@ import "fmt"
 // PinAddSettings represent the settings for PinAPI.Add
 type PinAddSettings struct {
 	Recursive bool
+	Name      string
 }
 
 // PinLsSettings represent the settings for PinAPI.Ls
 type PinLsSettings struct {
-	Type string
+	Type     string
+	Detailed bool
 }
 
 // PinIsPinnedSettings represent the settings for PinAPI.IsPinned
@@ -194,6 +196,15 @@ func (pinLsOpts) pinType(t string) PinLsOption {
 	}
 }
 
+// Detailed is an option for [Pin.Ls] which sets whether or not to return
+// detailed information, such as pin names and modes.
+func (pinLsOpts) Detailed(detailed bool) PinLsOption {
+	return func(settings *PinLsSettings) error {
+		settings.Detailed = detailed
+		return nil
+	}
+}
+
 type pinIsPinnedOpts struct{}
 
 // All is an option for Pin.IsPinned which will make it search in all type of pins.
@@ -263,6 +274,14 @@ func (pinOpts) Recursive(recursive bool) PinAddOption {
 	}
 }
 
+// Name is an option for Pin.Add which specifies an optional name to add to the pin.
+func (pinOpts) Name(name string) PinAddOption {
+	return func(settings *PinAddSettings) error {
+		settings.Name = name
+		return nil
+	}
+}
+
 // RmRecursive is an option for Pin.Rm which specifies whether to recursively
 // unpin the object linked to by the specified object(s). This does not remove
 // indirect pins referenced by other recursive pins.
diff --git a/core/coreiface/pin.go b/core/coreiface/pin.go
index 25a775965..ed837fc9c 100644
--- a/core/coreiface/pin.go
+++ b/core/coreiface/pin.go
@@ -13,6 +13,9 @@ type Pin interface {
 	// Path to the pinned object
 	Path() path.ImmutablePath
 
+	// Name is the name of the pin.
+	Name() string
+
 	// Type of the pin
 	Type() string
 
diff --git a/core/coreunix/add.go b/core/coreunix/add.go
index 83bec6d03..a8d7e5982 100644
--- a/core/coreunix/add.go
+++ b/core/coreunix/add.go
@@ -186,7 +186,7 @@ func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node) error {
 		adder.tempRoot = rnk
 	}
 
-	err = adder.pinning.PinWithMode(ctx, rnk, pin.Recursive)
+	err = adder.pinning.PinWithMode(ctx, rnk, pin.Recursive, "")
 	if err != nil {
 		return err
 	}
diff --git a/docs/changelogs/v0.26.md b/docs/changelogs/v0.26.md
index 3a9f088ca..9eddcfd85 100644
--- a/docs/changelogs/v0.26.md
+++ b/docs/changelogs/v0.26.md
@@ -7,6 +7,7 @@
 - [Overview](#overview)
 - [๐Ÿ”ฆ Highlights](#-highlights)
   - [Several deprecated commands have been removed](#several-deprecated-commands-have-been-removed)
+  - [Support optional pin names](#support-optional-pin-names)
 - [๐Ÿ“ Changelog](#-changelog)
 - [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors)
 
@@ -30,6 +31,10 @@ Several deprecated commands have been removed:
 - `ipfs dns` deprecated in [April 2022, Kubo 0.13](https://github.com/ipfs/kubo/commit/76ae33a9f3f9abd166d1f6f23d6a8a0511510e3c), use `ipfs resolve /ipns/{name}` instead.
 - `ipfs tar` deprecated [April 2022, Kubo 0.13](https://github.com/ipfs/kubo/pull/8849)
 
+#### Support optional pin names
+
+You can now add a name to a pin when pinning a CID. To do so, use `ipfs pin add --name "Some Name" bafy...`. You can list your pins, including their names, with `ipfs pin ls --names`.
+
 ### ๐Ÿ“ Changelog
 
 - Export a `kubo.Start` function so users can programmatically start Kubo from within a go program.
diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod
index 34b101c9a..b669171fd 100644
--- a/docs/examples/kubo-as-a-library/go.mod
+++ b/docs/examples/kubo-as-a-library/go.mod
@@ -7,7 +7,7 @@ go 1.20
 replace github.com/ipfs/kubo => ./../../..
 
 require (
-	github.com/ipfs/boxo v0.16.0
+	github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d
 	github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
 	github.com/libp2p/go-libp2p v0.32.2
 	github.com/multiformats/go-multiaddr v0.12.0
diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum
index 9d63ebb13..c348ddd60 100644
--- a/docs/examples/kubo-as-a-library/go.sum
+++ b/docs/examples/kubo-as-a-library/go.sum
@@ -303,8 +303,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy
 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI=
 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY=
-github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d h1:7tDzalCLHYr4tzrjNrfqZvIki2MEBSrpLBdc0bssDZk=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
 github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
diff --git a/fuse/ipns/common.go b/fuse/ipns/common.go
index 7306196c8..d22086776 100644
--- a/fuse/ipns/common.go
+++ b/fuse/ipns/common.go
@@ -18,7 +18,7 @@ func InitializeKeyspace(n *core.IpfsNode, key ci.PrivKey) error {
 
 	emptyDir := ft.EmptyDirNode()
 
-	err := n.Pinning.Pin(ctx, emptyDir, false)
+	err := n.Pinning.Pin(ctx, emptyDir, false, "")
 	if err != nil {
 		return err
 	}
diff --git a/gc/gc.go b/gc/gc.go
index c85f9d6bf..51df59e54 100644
--- a/gc/gc.go
+++ b/gc/gc.go
@@ -154,7 +154,7 @@ func GC(ctx context.Context, bs bstore.GCBlockstore, dstor dstore.Datastore, pn
 // Descendants recursively finds all the descendants of the given roots and
 // adds them to the given cid.Set, using the provided dag.GetLinks function
 // to walk the tree.
-func Descendants(ctx context.Context, getLinks dag.GetLinks, set *cid.Set, roots <-chan pin.StreamedCid) error {
+func Descendants(ctx context.Context, getLinks dag.GetLinks, set *cid.Set, roots <-chan pin.StreamedPin) error {
 	verifyGetLinks := func(ctx context.Context, c cid.Cid) ([]*ipld.Link, error) {
 		err := verifcid.ValidateCid(verifcid.DefaultAllowlist, c)
 		if err != nil {
@@ -188,7 +188,7 @@ func Descendants(ctx context.Context, getLinks dag.GetLinks, set *cid.Set, roots
 			}
 
 			// Walk recursively walks the dag and adds the keys to the given set
-			err := dag.Walk(ctx, verifyGetLinks, wrapper.C, func(k cid.Cid) bool {
+			err := dag.Walk(ctx, verifyGetLinks, wrapper.Pin.Key, func(k cid.Cid) bool {
 				return set.Visit(toCidV1(k))
 			}, dag.Concurrent())
 			if err != nil {
@@ -226,7 +226,7 @@ func ColoredSet(ctx context.Context, pn pin.Pinner, ng ipld.NodeGetter, bestEffo
 		}
 		return links, nil
 	}
-	rkeys := pn.RecursiveKeys(ctx)
+	rkeys := pn.RecursiveKeys(ctx, false)
 	err := Descendants(ctx, getLinks, gcs, rkeys)
 	if err != nil {
 		errors = true
@@ -249,14 +249,14 @@ func ColoredSet(ctx context.Context, pn pin.Pinner, ng ipld.NodeGetter, bestEffo
 		}
 		return links, nil
 	}
-	bestEffortRootsChan := make(chan pin.StreamedCid)
+	bestEffortRootsChan := make(chan pin.StreamedPin)
 	go func() {
 		defer close(bestEffortRootsChan)
 		for _, root := range bestEffortRoots {
 			select {
 			case <-ctx.Done():
 				return
-			case bestEffortRootsChan <- pin.StreamedCid{C: root}:
+			case bestEffortRootsChan <- pin.StreamedPin{Pin: pin.Pinned{Key: root}}:
 			}
 		}
 	}()
@@ -270,15 +270,15 @@ func ColoredSet(ctx context.Context, pn pin.Pinner, ng ipld.NodeGetter, bestEffo
 		}
 	}
 
-	dkeys := pn.DirectKeys(ctx)
+	dkeys := pn.DirectKeys(ctx, false)
 	for k := range dkeys {
 		if k.Err != nil {
 			return nil, k.Err
 		}
-		gcs.Add(toCidV1(k.C))
+		gcs.Add(toCidV1(k.Pin.Key))
 	}
 
-	ikeys := pn.InternalPins(ctx)
+	ikeys := pn.InternalPins(ctx, false)
 	err = Descendants(ctx, getLinks, gcs, ikeys)
 	if err != nil {
 		errors = true
diff --git a/gc/gc_test.go b/gc/gc_test.go
index 4fb6dbf09..c5d00714d 100644
--- a/gc/gc_test.go
+++ b/gc/gc_test.go
@@ -38,14 +38,14 @@ func TestGC(t *testing.T) {
 		// direct
 		root, _, err := daggen.MakeDagNode(dserv.Add, 0, 1)
 		require.NoError(t, err)
-		err = pinner.PinWithMode(ctx, root, pin.Direct)
+		err = pinner.PinWithMode(ctx, root, pin.Direct, "")
 		require.NoError(t, err)
 		expectedKept = append(expectedKept, root.Hash())
 
 		// recursive
 		root, allCids, err := daggen.MakeDagNode(dserv.Add, 5, 2)
 		require.NoError(t, err)
-		err = pinner.PinWithMode(ctx, root, pin.Recursive)
+		err = pinner.PinWithMode(ctx, root, pin.Recursive, "")
 		require.NoError(t, err)
 		expectedKept = append(expectedKept, toMHs(allCids)...)
 	}
diff --git a/go.mod b/go.mod
index 74b4cd347..b9fba0309 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/ipfs-shipyard/nopfs v0.0.12-0.20231027223058-cde3b5ba964c
 	github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c
-	github.com/ipfs/boxo v0.16.0
+	github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d
 	github.com/ipfs/go-block-format v0.2.0
 	github.com/ipfs/go-cid v0.4.1
 	github.com/ipfs/go-cidutil v0.1.0
diff --git a/go.sum b/go.sum
index f69fbedd0..8cae85a9f 100644
--- a/go.sum
+++ b/go.sum
@@ -337,8 +337,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy
 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI=
 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY=
-github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d h1:7tDzalCLHYr4tzrjNrfqZvIki2MEBSrpLBdc0bssDZk=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
 github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
diff --git a/test/cli/pins_test.go b/test/cli/pins_test.go
index 8a36d4695..a57f1b89b 100644
--- a/test/cli/pins_test.go
+++ b/test/cli/pins_test.go
@@ -209,4 +209,67 @@ func TestPins(t *testing.T) {
 		testPins(t, testPinsArgs{runDaemon: true, baseArg: "--cid-base=base32"})
 		testPins(t, testPinsArgs{runDaemon: true, lsArg: "--stream", baseArg: "--cid-base=base32"})
 	})
+
+	t.Run("test pinning with names cli text output", func(t *testing.T) {
+		t.Parallel()
+
+		node := harness.NewT(t).NewNode().Init()
+		cidAStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+		cidBStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+
+		_ = node.IPFS("pin", "add", "--name", "testPin", cidAStr)
+
+		outARegular := cidAStr + " recursive"
+		outADetailed := outARegular + " testPin"
+		outBRegular := cidBStr + " recursive"
+		outBDetailed := outBRegular + " testPin"
+
+		pinLs := func(args ...string) []string {
+			return strings.Split(node.IPFS(StrCat("pin", "ls", args)...).Stdout.Trimmed(), "\n")
+		}
+
+		lsOut := pinLs("-t=recursive")
+		require.Contains(t, lsOut, outARegular)
+		require.NotContains(t, lsOut, outADetailed)
+
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outADetailed)
+		require.NotContains(t, lsOut, outARegular)
+
+		_ = node.IPFS("pin", "update", cidAStr, cidBStr)
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outBDetailed)
+		require.NotContains(t, lsOut, outADetailed)
+	})
+
+	// JSON that is also the wire format of /api/v0
+	t.Run("test pinning with names json output", func(t *testing.T) {
+		t.Parallel()
+
+		node := harness.NewT(t).NewNode().Init()
+		cidAStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+		cidBStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+
+		_ = node.IPFS("pin", "add", "--name", "testPinJson", cidAStr)
+
+		outARegular := `"` + cidAStr + `":{"Type":"recursive"`
+		outADetailed := outARegular + `,"Name":"testPinJson"`
+		outBRegular := `"` + cidBStr + `":{"Type":"recursive"`
+		outBDetailed := outBRegular + `,"Name":"testPinJson"`
+
+		pinLs := func(args ...string) string {
+			return node.IPFS(StrCat("pin", "ls", "--enc=json", args)...).Stdout.Trimmed()
+		}
+
+		lsOut := pinLs("-t=recursive")
+		require.Contains(t, lsOut, outARegular)
+		require.NotContains(t, lsOut, outADetailed)
+
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outADetailed)
+
+		_ = node.IPFS("pin", "update", cidAStr, cidBStr)
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outBDetailed)
+	})
 }
diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod
index e55e9f10c..2cb03508a 100644
--- a/test/dependencies/go.mod
+++ b/test/dependencies/go.mod
@@ -103,7 +103,7 @@ require (
 	github.com/hexops/gotextdiff v1.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/ipfs/bbloom v0.0.4 // indirect
-	github.com/ipfs/boxo v0.16.0 // indirect
+	github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d // indirect
 	github.com/ipfs/go-block-format v0.2.0 // indirect
 	github.com/ipfs/go-cid v0.4.1 // indirect
 	github.com/ipfs/go-datastore v0.6.0 // indirect
diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum
index 35f92c1a8..8681e257c 100644
--- a/test/dependencies/go.sum
+++ b/test/dependencies/go.sum
@@ -342,8 +342,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY=
-github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d h1:7tDzalCLHYr4tzrjNrfqZvIki2MEBSrpLBdc0bssDZk=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
 github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
 github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
 github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
-- 
GitLab