@cryptotaxi247 / kubo / commits / 10abb9073

feat(add): add support for naming pinned CIDs (#10877)

* feat(add): add support for naming pinned CID Signed-off-by: kapil <kapilsareen584@gmail.com> * fix(add): no double pinning and simplify pin-name - modify PinRoot to accept name parameter, eliminating double pinning - remove automatic filename fallback logic for cleaner behavior - only create named pins when explicitly requested via --pin-name=value - replace NoPinName constant with idiomatic empty string literals - Update help text and tests to reflect explicit-only behavior * docs: changelog * chore: lint * test: negative case for empty pin-name * chore: gofmt --------- Signed-off-by: kapil <kapilsareen584@gmail.com> Co-authored-by: Marcin Rataj <lidel@lidel.org>

Kapil Sareen committed Aug 6, 2025 at 05:46 UTC 10abb9073d6e7fe4041d463eac9f29597d104f2c
10 files changed +86 -13
cmd/ipfs/kubo/add_migrations.go
+1 -1
@@ -86,7 +86,7 @@ func addMigrationFiles(ctx context.Context, node *core.IpfsNode, paths []string,
86 return err
87 }
88
89 - ipfsPath, err := ufs.Add(ctx, files.NewReaderStatFile(f, fi), options.Unixfs.Pin(pin))
89 + ipfsPath, err := ufs.Add(ctx, files.NewReaderStatFile(f, fi), options.Unixfs.Pin(pin, ""))
90 if err != nil {
91 return err
92 }
config/import.go
-1
@@ -21,7 +21,6 @@ const (
21 // write-batch. The total size of the batch is limited by
22 // BatchMaxnodes and BatchMaxSize.
23 DefaultBatchMaxSize = 100 << 20 // 20MiB
24 -
24 )
25
26 var (
core/commands/add.go
+9 -2
@@ -37,6 +37,7 @@ type AddEvent struct {
37 }
38
39 const (
40 + pinNameOptionName = "pin-name"
41 quietOptionName = "quiet"
42 quieterOptionName = "quieter"
43 silentOptionName = "silent"
@@ -184,6 +185,7 @@ See 'dag export' and 'dag import' for more information.
185 cmds.BoolOption(inlineOptionName, "Inline small blocks into CIDs. (experimental)"),
186 cmds.IntOption(inlineLimitOptionName, "Maximum block size to inline. (experimental)").WithDefault(32),
187 cmds.BoolOption(pinOptionName, "Pin locally to protect added files from garbage collection.").WithDefault(true),
188 + cmds.StringOption(pinNameOptionName, "Name to use for the pin. Requires explicit value (e.g., --pin-name=myname)."),
189 cmds.StringOption(toFilesOptionName, "Add reference to Files API (MFS) at the provided path."),
190 cmds.BoolOption(preserveModeOptionName, "Apply existing POSIX permissions to created UnixFS entries. Disables raw-leaves. (experimental)"),
191 cmds.BoolOption(preserveMtimeOptionName, "Apply existing POSIX modification time to created UnixFS entries. Disables raw-leaves. (experimental)"),
@@ -230,6 +232,7 @@ See 'dag export' and 'dag import' for more information.
232 silent, _ := req.Options[silentOptionName].(bool)
233 chunker, _ := req.Options[chunkerOptionName].(string)
234 dopin, _ := req.Options[pinOptionName].(bool)
235 + pinName, pinNameSet := req.Options[pinNameOptionName].(string)
236 rawblks, rbset := req.Options[rawLeavesOptionName].(bool)
237 maxFileLinks, maxFileLinksSet := req.Options[maxFileLinksOptionName].(int)
238 maxDirectoryLinks, maxDirectoryLinksSet := req.Options[maxDirectoryLinksOptionName].(int)
@@ -260,6 +263,8 @@ See 'dag export' and 'dag import' for more information.
263 cidVer = int(cfg.Import.CidVersion.WithDefault(config.DefaultCidVersion))
264 }
265
266 + // Pin names are only used when explicitly provided via --pin-name=value
267 +
268 if !rbset && cfg.Import.UnixFSRawLeaves != config.Default {
269 rbset = true
270 rawblks = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves)
@@ -296,7 +301,9 @@ See 'dag export' and 'dag import' for more information.
301 if onlyHash && toFilesSet {
302 return fmt.Errorf("%s and %s options are not compatible", onlyHashOptionName, toFilesOptionName)
303 }
299 -
304 + if !dopin && pinNameSet {
305 + return fmt.Errorf("%s option requires %s to be set", pinNameOptionName, pinOptionName)
306 + }
307 if wrap && toFilesSet {
308 return fmt.Errorf("%s and %s options are not compatible", wrapOptionName, toFilesOptionName)
309 }
@@ -326,7 +333,7 @@ See 'dag export' and 'dag import' for more information.
333
334 options.Unixfs.Chunker(chunker),
335
329 - options.Unixfs.Pin(dopin),
336 + options.Unixfs.Pin(dopin, pinName),
337 options.Unixfs.HashOnly(onlyHash),
338 options.Unixfs.FsCache(fscache),
339 options.Unixfs.Nocopy(nocopy),
core/coreapi/test/path_test.go
+1 -1
@@ -39,7 +39,7 @@ func TestPathUnixFSHAMTPartial(t *testing.T) {
39 dir[strconv.Itoa(i)] = files.NewBytesFile([]byte(strconv.Itoa(i)))
40 }
41
42 - r, err := a.Unixfs().Add(ctx, files.NewMapDirectory(dir), options.Unixfs.Pin(false))
42 + r, err := a.Unixfs().Add(ctx, files.NewMapDirectory(dir), options.Unixfs.Pin(false, ""))
43 if err != nil {
44 t.Fatal(err)
45 }
core/coreapi/unixfs.go
+4
@@ -58,6 +58,7 @@ func (api *UnixfsAPI) Add(ctx context.Context, files files.Node, opts ...options
58 attribute.Bool("maxhamtfanoutset", settings.MaxHAMTFanoutSet),
59 attribute.Int("layout", int(settings.Layout)),
60 attribute.Bool("pin", settings.Pin),
61 + attribute.String("pin-name", settings.PinName),
62 attribute.Bool("onlyhash", settings.OnlyHash),
63 attribute.Bool("fscache", settings.FsCache),
64 attribute.Bool("nocopy", settings.NoCopy),
@@ -136,6 +137,9 @@ func (api *UnixfsAPI) Add(ctx context.Context, files files.Node, opts ...options
137 fileAdder.Progress = settings.Progress
138 }
139 fileAdder.Pin = settings.Pin && !settings.OnlyHash
140 + if settings.Pin {
141 + fileAdder.PinName = settings.PinName
142 + }
143 fileAdder.Silent = settings.Silent
144 fileAdder.RawLeaves = settings.RawLeaves
145 if settings.MaxFileLinksSet {
core/coreiface/options/unixfs.go
+6 -1
@@ -39,6 +39,7 @@ type UnixfsAddSettings struct {
39 Layout Layout
40
41 Pin bool
42 + PinName string
43 OnlyHash bool
44 FsCache bool
45 NoCopy bool
@@ -83,6 +84,7 @@ func UnixfsAddOptions(opts ...UnixfsAddOption) (*UnixfsAddSettings, cid.Prefix,
84 Layout: BalancedLayout,
85
86 Pin: false,
87 + PinName: "",
88 OnlyHash: false,
89 FsCache: false,
90 NoCopy: false,
@@ -280,9 +282,12 @@ func (unixfsOpts) Layout(layout Layout) UnixfsAddOption {
282 }
283
284 // Pin tells the adder to pin the file root recursively after adding
283 -func (unixfsOpts) Pin(pin bool) UnixfsAddOption {
285 +func (unixfsOpts) Pin(pin bool, pinName string) UnixfsAddOption {
286 return func(settings *UnixfsAddSettings) error {
287 settings.Pin = pin
288 + if pin {
289 + settings.PinName = pinName
290 + }
291 return nil
292 }
293 }
core/coreiface/tests/unixfs.go
+1 -1
@@ -539,7 +539,7 @@ func (tp *TestSuite) TestAddPinned(t *testing.T) {
539 t.Fatal(err)
540 }
541
542 - _, err = api.Unixfs().Add(ctx, strFile(helloStr)(), options.Unixfs.Pin(true))
542 + _, err = api.Unixfs().Add(ctx, strFile(helloStr)(), options.Unixfs.Pin(true, ""))
543 if err != nil {
544 t.Fatal(err)
545 }
core/coreunix/add.go
+13 -6
@@ -76,6 +76,7 @@ type Adder struct {
76 Out chan<- interface{}
77 Progress bool
78 Pin bool
79 + PinName string
80 Trickle bool
81 RawLeaves bool
82 MaxLinks int
@@ -182,9 +183,10 @@ func (adder *Adder) curRootNode() (ipld.Node, error) {
183 return root, err
184 }
185
185 -// Recursively pins the root node of Adder and
186 -// writes the pin state to the backing datastore.
187 -func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node) error {
186 +// PinRoot recursively pins the root node of Adder with an optional name and
187 +// writes the pin state to the backing datastore. If name is empty, the pin
188 +// will be created without a name.
189 +func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node, name string) error {
190 ctx, span := tracing.Span(ctx, "CoreUnix.Adder", "PinRoot")
191 defer span.End()
192
@@ -207,7 +209,7 @@ func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node) error {
209 adder.tempRoot = rnk
210 }
211
210 - err = adder.pinning.PinWithMode(ctx, rnk, pin.Recursive, "")
212 + err = adder.pinning.PinWithMode(ctx, rnk, pin.Recursive, name)
213 if err != nil {
214 return err
215 }
@@ -369,7 +371,12 @@ func (adder *Adder) AddAllAndPin(ctx context.Context, file files.Node) (ipld.Nod
371 if !adder.Pin {
372 return nd, nil
373 }
372 - return nd, adder.PinRoot(ctx, nd)
374 +
375 + if err := adder.PinRoot(ctx, nd, adder.PinName); err != nil {
376 + return nil, err
377 + }
378 +
379 + return nd, nil
380 }
381
382 func (adder *Adder) addFileNode(ctx context.Context, path string, file files.Node, toplevel bool) error {
@@ -530,7 +537,7 @@ func (adder *Adder) maybePauseForGC(ctx context.Context) error {
537 return err
538 }
539
533 - err = adder.PinRoot(ctx, rn)
540 + err = adder.PinRoot(ctx, rn, "")
541 if err != nil {
542 return err
543 }
docs/changelogs/v0.37.md
+13
@@ -11,6 +11,7 @@ This release was brought to you by the [Interplanetary Shipyard](https://ipship
11 - [Overview](#overview)
12 - [🔦 Highlights](#-highlights)
13 - [Clear provide queue when reprovide strategy changes](#clear-provide-queue-when-reprovide-strategy-changes)
14 + - [Named pins in `ipfs add` command](#-named-pins-in-ipfs-add-command)
15 - [Removed unnecessary dependencies](#removed-unnecessary-dependencies)
16 - [📦️ Important dependency updates](#-important-dependency-updates)
17 - [📝 Changelog](#-changelog)
@@ -31,6 +32,18 @@ A new `ipfs provide clear` command also allows manual queue clearing for debuggi
32 > [!NOTE]
33 > Upgrading to Kubo 0.37 will automatically clear any preexisting provide queue. The next time `Reprovider.Interval` hits, `Reprovider.Strategy` will be executed on a clean slate, ensuring consistent behavior with your current configuration.
34
35 +#### 🧷 Named pins in `ipfs add` command
36 +
37 +Added `--pin-name` flag to `ipfs add` for assigning names to pins.
38 +
39 +```console
40 +$ ipfs add --pin-name=testname cat.jpg
41 +added bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi cat.jpg
42 +
43 +$ ipfs pin ls --names
44 +bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi recursive testname
45 +```
46 +
47 #### Removed unnecessary dependencies
48
49 Kubo has been cleaned up by removing unnecessary dependencies and packages:
test/cli/add_test.go
+38
@@ -108,6 +108,44 @@ func TestAdd(t *testing.T) {
108 require.Equal(t, shortStringCidV1NoRawLeaves, cidStr)
109 })
110
111 + t.Run("ipfs add --pin-name=foo", func(t *testing.T) {
112 + t.Parallel()
113 + node := harness.NewT(t).NewNode().Init().StartDaemon()
114 + defer node.StopDaemon()
115 +
116 + pinName := "test-pin-name"
117 + cidStr := node.IPFSAddStr(shortString, "--pin-name", pinName)
118 + require.Equal(t, shortStringCidV0, cidStr)
119 +
120 + pinList := node.IPFS("pin", "ls", "--names").Stdout.Trimmed()
121 + require.Contains(t, pinList, shortStringCidV0)
122 + require.Contains(t, pinList, pinName)
123 + })
124 +
125 + t.Run("ipfs add --pin=false --pin-name=foo returns an error", func(t *testing.T) {
126 + t.Parallel()
127 +
128 + node := harness.NewT(t).NewNode().Init().StartDaemon()
129 + defer node.StopDaemon()
130 +
131 + // Use RunIPFS to allow for errors without assertion
132 + result := node.RunIPFS("add", "--pin=false", "--pin-name=foo")
133 + require.Error(t, result.Err, "Expected an error due to incompatible --pin and --pin-name")
134 + require.Contains(t, result.Stderr.String(), "pin-name option requires pin to be set")
135 + })
136 +
137 + t.Run("ipfs add --pin-name without value should fail", func(t *testing.T) {
138 + t.Parallel()
139 +
140 + node := harness.NewT(t).NewNode().Init().StartDaemon()
141 + defer node.StopDaemon()
142 +
143 + // When --pin-name is passed without any value, it should fail
144 + result := node.RunIPFS("add", "--pin-name")
145 + require.Error(t, result.Err, "Expected an error when --pin-name has no value")
146 + require.Contains(t, result.Stderr.String(), "missing argument for option \"pin-name\"")
147 + })
148 +
149 t.Run("produced unixfs max file links: command flag --max-file-links overrides configuration in Import.UnixFSFileMaxLinks", func(t *testing.T) {
150 t.Parallel()
151