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