diff --git a/cmd/ipfs/kubo/add_migrations.go b/cmd/ipfs/kubo/add_migrations.go index 557a8de84..d77d0afdf 100644 --- a/cmd/ipfs/kubo/add_migrations.go +++ b/cmd/ipfs/kubo/add_migrations.go @@ -86,7 +86,7 @@ func addMigrationFiles(ctx context.Context, node *core.IpfsNode, paths []string, return err } - ipfsPath, err := ufs.Add(ctx, files.NewReaderStatFile(f, fi), options.Unixfs.Pin(pin)) + ipfsPath, err := ufs.Add(ctx, files.NewReaderStatFile(f, fi), options.Unixfs.Pin(pin, "")) if err != nil { return err } diff --git a/config/import.go b/config/import.go index 21bf232c1..c51917286 100644 --- a/config/import.go +++ b/config/import.go @@ -21,7 +21,6 @@ const ( // write-batch. The total size of the batch is limited by // BatchMaxnodes and BatchMaxSize. DefaultBatchMaxSize = 100 << 20 // 20MiB - ) var ( diff --git a/core/commands/add.go b/core/commands/add.go index f800e4f42..f9e374b80 100644 --- a/core/commands/add.go +++ b/core/commands/add.go @@ -37,6 +37,7 @@ type AddEvent struct { } const ( + pinNameOptionName = "pin-name" quietOptionName = "quiet" quieterOptionName = "quieter" silentOptionName = "silent" @@ -184,6 +185,7 @@ See 'dag export' and 'dag import' for more information. cmds.BoolOption(inlineOptionName, "Inline small blocks into CIDs. (experimental)"), cmds.IntOption(inlineLimitOptionName, "Maximum block size to inline. (experimental)").WithDefault(32), cmds.BoolOption(pinOptionName, "Pin locally to protect added files from garbage collection.").WithDefault(true), + cmds.StringOption(pinNameOptionName, "Name to use for the pin. Requires explicit value (e.g., --pin-name=myname)."), cmds.StringOption(toFilesOptionName, "Add reference to Files API (MFS) at the provided path."), cmds.BoolOption(preserveModeOptionName, "Apply existing POSIX permissions to created UnixFS entries. Disables raw-leaves. (experimental)"), 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. silent, _ := req.Options[silentOptionName].(bool) chunker, _ := req.Options[chunkerOptionName].(string) dopin, _ := req.Options[pinOptionName].(bool) + pinName, pinNameSet := req.Options[pinNameOptionName].(string) rawblks, rbset := req.Options[rawLeavesOptionName].(bool) maxFileLinks, maxFileLinksSet := req.Options[maxFileLinksOptionName].(int) maxDirectoryLinks, maxDirectoryLinksSet := req.Options[maxDirectoryLinksOptionName].(int) @@ -260,6 +263,8 @@ See 'dag export' and 'dag import' for more information. cidVer = int(cfg.Import.CidVersion.WithDefault(config.DefaultCidVersion)) } + // Pin names are only used when explicitly provided via --pin-name=value + if !rbset && cfg.Import.UnixFSRawLeaves != config.Default { rbset = true rawblks = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves) @@ -296,7 +301,9 @@ See 'dag export' and 'dag import' for more information. if onlyHash && toFilesSet { return fmt.Errorf("%s and %s options are not compatible", onlyHashOptionName, toFilesOptionName) } - + if !dopin && pinNameSet { + return fmt.Errorf("%s option requires %s to be set", pinNameOptionName, pinOptionName) + } if wrap && toFilesSet { return fmt.Errorf("%s and %s options are not compatible", wrapOptionName, toFilesOptionName) } @@ -326,7 +333,7 @@ See 'dag export' and 'dag import' for more information. options.Unixfs.Chunker(chunker), - options.Unixfs.Pin(dopin), + options.Unixfs.Pin(dopin, pinName), options.Unixfs.HashOnly(onlyHash), options.Unixfs.FsCache(fscache), options.Unixfs.Nocopy(nocopy), diff --git a/core/coreapi/test/path_test.go b/core/coreapi/test/path_test.go index 692853a9a..f1337e809 100644 --- a/core/coreapi/test/path_test.go +++ b/core/coreapi/test/path_test.go @@ -39,7 +39,7 @@ func TestPathUnixFSHAMTPartial(t *testing.T) { dir[strconv.Itoa(i)] = files.NewBytesFile([]byte(strconv.Itoa(i))) } - r, err := a.Unixfs().Add(ctx, files.NewMapDirectory(dir), options.Unixfs.Pin(false)) + r, err := a.Unixfs().Add(ctx, files.NewMapDirectory(dir), options.Unixfs.Pin(false, "")) if err != nil { t.Fatal(err) } diff --git a/core/coreapi/unixfs.go b/core/coreapi/unixfs.go index eece797a5..6c78a869a 100644 --- a/core/coreapi/unixfs.go +++ b/core/coreapi/unixfs.go @@ -58,6 +58,7 @@ func (api *UnixfsAPI) Add(ctx context.Context, files files.Node, opts ...options attribute.Bool("maxhamtfanoutset", settings.MaxHAMTFanoutSet), attribute.Int("layout", int(settings.Layout)), attribute.Bool("pin", settings.Pin), + attribute.String("pin-name", settings.PinName), attribute.Bool("onlyhash", settings.OnlyHash), attribute.Bool("fscache", settings.FsCache), attribute.Bool("nocopy", settings.NoCopy), @@ -136,6 +137,9 @@ func (api *UnixfsAPI) Add(ctx context.Context, files files.Node, opts ...options fileAdder.Progress = settings.Progress } fileAdder.Pin = settings.Pin && !settings.OnlyHash + if settings.Pin { + fileAdder.PinName = settings.PinName + } fileAdder.Silent = settings.Silent fileAdder.RawLeaves = settings.RawLeaves if settings.MaxFileLinksSet { diff --git a/core/coreiface/options/unixfs.go b/core/coreiface/options/unixfs.go index 20f18d1e0..45e880ed1 100644 --- a/core/coreiface/options/unixfs.go +++ b/core/coreiface/options/unixfs.go @@ -39,6 +39,7 @@ type UnixfsAddSettings struct { Layout Layout Pin bool + PinName string OnlyHash bool FsCache bool NoCopy bool @@ -83,6 +84,7 @@ func UnixfsAddOptions(opts ...UnixfsAddOption) (*UnixfsAddSettings, cid.Prefix, Layout: BalancedLayout, Pin: false, + PinName: "", OnlyHash: false, FsCache: false, NoCopy: false, @@ -280,9 +282,12 @@ func (unixfsOpts) Layout(layout Layout) UnixfsAddOption { } // Pin tells the adder to pin the file root recursively after adding -func (unixfsOpts) Pin(pin bool) UnixfsAddOption { +func (unixfsOpts) Pin(pin bool, pinName string) UnixfsAddOption { return func(settings *UnixfsAddSettings) error { settings.Pin = pin + if pin { + settings.PinName = pinName + } return nil } } diff --git a/core/coreiface/tests/unixfs.go b/core/coreiface/tests/unixfs.go index 43447990e..c2717216c 100644 --- a/core/coreiface/tests/unixfs.go +++ b/core/coreiface/tests/unixfs.go @@ -539,7 +539,7 @@ func (tp *TestSuite) TestAddPinned(t *testing.T) { t.Fatal(err) } - _, err = api.Unixfs().Add(ctx, strFile(helloStr)(), options.Unixfs.Pin(true)) + _, err = api.Unixfs().Add(ctx, strFile(helloStr)(), options.Unixfs.Pin(true, "")) if err != nil { t.Fatal(err) } diff --git a/core/coreunix/add.go b/core/coreunix/add.go index eb6f25e0f..c693773f9 100644 --- a/core/coreunix/add.go +++ b/core/coreunix/add.go @@ -76,6 +76,7 @@ type Adder struct { Out chan<- interface{} Progress bool Pin bool + PinName string Trickle bool RawLeaves bool MaxLinks int @@ -182,9 +183,10 @@ func (adder *Adder) curRootNode() (ipld.Node, error) { return root, err } -// Recursively pins the root node of Adder and -// writes the pin state to the backing datastore. -func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node) error { +// PinRoot recursively pins the root node of Adder with an optional name and +// writes the pin state to the backing datastore. If name is empty, the pin +// will be created without a name. +func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node, name string) error { ctx, span := tracing.Span(ctx, "CoreUnix.Adder", "PinRoot") defer span.End() @@ -207,7 +209,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, name) if err != nil { return err } @@ -369,7 +371,12 @@ func (adder *Adder) AddAllAndPin(ctx context.Context, file files.Node) (ipld.Nod if !adder.Pin { return nd, nil } - return nd, adder.PinRoot(ctx, nd) + + if err := adder.PinRoot(ctx, nd, adder.PinName); err != nil { + return nil, err + } + + return nd, nil } 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 { return err } - err = adder.PinRoot(ctx, rn) + err = adder.PinRoot(ctx, rn, "") if err != nil { return err } diff --git a/docs/changelogs/v0.37.md b/docs/changelogs/v0.37.md index 72e15d2da..8c264275b 100644 --- a/docs/changelogs/v0.37.md +++ b/docs/changelogs/v0.37.md @@ -11,6 +11,7 @@ This release was brought to you by the [Interplanetary Shipyard](https://ipship - [Overview](#overview) - [๐Ÿ”ฆ Highlights](#-highlights) - [Clear provide queue when reprovide strategy changes](#clear-provide-queue-when-reprovide-strategy-changes) + - [Named pins in `ipfs add` command](#-named-pins-in-ipfs-add-command) - [Removed unnecessary dependencies](#removed-unnecessary-dependencies) - [๐Ÿ“ฆ๏ธ Important dependency updates](#-important-dependency-updates) - [๐Ÿ“ Changelog](#-changelog) @@ -31,6 +32,18 @@ A new `ipfs provide clear` command also allows manual queue clearing for debuggi > [!NOTE] > 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. +#### ๐Ÿงท Named pins in `ipfs add` command + +Added `--pin-name` flag to `ipfs add` for assigning names to pins. + +```console +$ ipfs add --pin-name=testname cat.jpg +added bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi cat.jpg + +$ ipfs pin ls --names +bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi recursive testname +``` + #### Removed unnecessary dependencies Kubo has been cleaned up by removing unnecessary dependencies and packages: diff --git a/test/cli/add_test.go b/test/cli/add_test.go index 775a6063b..e4138b624 100644 --- a/test/cli/add_test.go +++ b/test/cli/add_test.go @@ -108,6 +108,44 @@ func TestAdd(t *testing.T) { require.Equal(t, shortStringCidV1NoRawLeaves, cidStr) }) + t.Run("ipfs add --pin-name=foo", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + pinName := "test-pin-name" + cidStr := node.IPFSAddStr(shortString, "--pin-name", pinName) + require.Equal(t, shortStringCidV0, cidStr) + + pinList := node.IPFS("pin", "ls", "--names").Stdout.Trimmed() + require.Contains(t, pinList, shortStringCidV0) + require.Contains(t, pinList, pinName) + }) + + t.Run("ipfs add --pin=false --pin-name=foo returns an error", func(t *testing.T) { + t.Parallel() + + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + // Use RunIPFS to allow for errors without assertion + result := node.RunIPFS("add", "--pin=false", "--pin-name=foo") + require.Error(t, result.Err, "Expected an error due to incompatible --pin and --pin-name") + require.Contains(t, result.Stderr.String(), "pin-name option requires pin to be set") + }) + + t.Run("ipfs add --pin-name without value should fail", func(t *testing.T) { + t.Parallel() + + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + // When --pin-name is passed without any value, it should fail + result := node.RunIPFS("add", "--pin-name") + require.Error(t, result.Err, "Expected an error when --pin-name has no value") + require.Contains(t, result.Stderr.String(), "missing argument for option \"pin-name\"") + }) + t.Run("produced unixfs max file links: command flag --max-file-links overrides configuration in Import.UnixFSFileMaxLinks", func(t *testing.T) { t.Parallel()