Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions client/rpc/pin.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ func (api *PinAPI) Add(ctx context.Context, p path.Path, opts ...caopts.PinAddOp
return err
}

return api.core().Request("pin/add", p.String()).
Option("recursive", options.Recursive).Exec(ctx, nil)
req := api.core().Request("pin/add", p.String()).
Option("recursive", options.Recursive)
if options.Name != "" {
req = req.Option("name", options.Name)
}
return req.Exec(ctx, nil)
}

type pinLsObject struct {
Expand Down
71 changes: 37 additions & 34 deletions core/commands/pin/pin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
bserv "github.com/ipfs/boxo/blockservice"
offline "github.com/ipfs/boxo/exchange/offline"
dag "github.com/ipfs/boxo/ipld/merkledag"
pin "github.com/ipfs/boxo/pinning/pinner"
verifcid "github.com/ipfs/boxo/verifcid"
cid "github.com/ipfs/go-cid"
cidenc "github.com/ipfs/go-cidutil/cidenc"
Expand Down Expand Up @@ -370,16 +371,23 @@ Example:
return err
}

n, err := cmdenv.GetNode(env)
if err != nil {
return err
}

if n.Pinning == nil {
return fmt.Errorf("pinning service not available")
}

typeStr, _ := req.Options[pinTypeOptionName].(string)
stream, _ := req.Options[pinStreamOptionName].(bool)
displayNames, _ := req.Options[pinNamesOptionName].(bool)
name, _ := req.Options[pinNameOptionName].(string)

switch typeStr {
case "all", "direct", "indirect", "recursive":
default:
err = fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)
return err
mode, ok := pin.StringToMode(typeStr)
if !ok {
return fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)
}

// For backward compatibility, we accumulate the pins in the same output type as before.
Expand All @@ -397,7 +405,7 @@ Example:
}

if len(req.Arguments) > 0 {
err = pinLsKeys(req, typeStr, api, emit)
err = pinLsKeys(req, mode, displayNames || name != "", n.Pinning, api, emit)
} else {
err = pinLsAll(req, typeStr, displayNames || name != "", name, api, emit)
}
Expand Down Expand Up @@ -482,23 +490,14 @@ type PinLsObject struct {
Type string `json:",omitempty"`
}

func pinLsKeys(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit func(value PinLsOutputWrapper) error) error {
func pinLsKeys(req *cmds.Request, mode pin.Mode, displayNames bool, pinner pin.Pinner, api coreiface.CoreAPI, emit func(value PinLsOutputWrapper) error) error {
enc, err := cmdenv.GetCidEncoder(req)
if err != nil {
return err
}

switch typeStr {
case "all", "direct", "indirect", "recursive":
default:
return fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)
}

opt, err := options.Pin.IsPinned.Type(typeStr)
if err != nil {
panic("unhandled pin type")
}

// Collect CIDs to check
cids := make([]cid.Cid, 0, len(req.Arguments))
for _, p := range req.Arguments {
p, err := cmdutils.PathOrCidPath(p)
if err != nil {
Expand All @@ -510,25 +509,31 @@ func pinLsKeys(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fu
return err
}

pinType, pinned, err := api.Pin().IsPinned(req.Context, rp, opt)
if err != nil {
return err
}
cids = append(cids, rp.RootCid())
}

if !pinned {
return fmt.Errorf("path '%s' is not pinned", p)
// Check pins using the new type-specific method
pinned, err := pinner.CheckIfPinnedWithType(req.Context, mode, displayNames, cids...)
if err != nil {
return err
}

// Process results
for i, p := range pinned {
if !p.Pinned() {
return fmt.Errorf("path '%s' is not pinned", req.Arguments[i])
}

switch pinType {
case "direct", "indirect", "recursive", "internal":
default:
pinType = "indirect through " + pinType
pinType, _ := pin.ModeToString(p.Mode)
if p.Mode == pin.Indirect && p.Via.Defined() {
pinType = "indirect through " + enc.Encode(p.Via)
}

err = emit(PinLsOutputWrapper{
PinLsObject: PinLsObject{
Type: pinType,
Cid: enc.Encode(rp.RootCid()),
Cid: enc.Encode(cids[i]),
Name: p.Name,
},
})
if err != nil {
Expand All @@ -545,11 +550,9 @@ func pinLsAll(req *cmds.Request, typeStr string, detailed bool, name string, api
return err
}

switch typeStr {
case "all", "direct", "indirect", "recursive":
default:
err = fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)
return err
_, ok := pin.StringToMode(typeStr)
if !ok {
return fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)
}

opt, err := options.Pin.Ls.Type(typeStr)
Expand Down
141 changes: 141 additions & 0 deletions core/coreiface/tests/pin.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
ipld "github.com/ipfs/go-ipld-format"
iface "github.com/ipfs/kubo/core/coreiface"
opt "github.com/ipfs/kubo/core/coreiface/options"
"github.com/stretchr/testify/require"
)

func (tp *TestSuite) TestPin(t *testing.T) {
Expand All @@ -28,6 +29,7 @@ func (tp *TestSuite) TestPin(t *testing.T) {
t.Run("TestPinLsIndirect", tp.TestPinLsIndirect)
t.Run("TestPinLsPrecedence", tp.TestPinLsPrecedence)
t.Run("TestPinIsPinned", tp.TestPinIsPinned)
t.Run("TestPinNames", tp.TestPinNames)
}

func (tp *TestSuite) TestPinAdd(t *testing.T) {
Expand Down Expand Up @@ -580,6 +582,145 @@ func assertIsPinned(t *testing.T, ctx context.Context, api iface.CoreAPI, p path
}
}

func (tp *TestSuite) TestPinNames(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
api, err := tp.makeAPI(t, ctx)
require.NoError(t, err)

// Create test content
p1, err := api.Unixfs().Add(ctx, strFile("content1")())
require.NoError(t, err)

p2, err := api.Unixfs().Add(ctx, strFile("content2")())
require.NoError(t, err)

p3, err := api.Unixfs().Add(ctx, strFile("content3")())
require.NoError(t, err)

p4, err := api.Unixfs().Add(ctx, strFile("content4")())
require.NoError(t, err)

// Test 1: Pin with name
err = api.Pin().Add(ctx, p1, opt.Pin.Name("test-pin-1"))
require.NoError(t, err, "failed to add pin with name")

// Test 2: Pin without name
err = api.Pin().Add(ctx, p2)
require.NoError(t, err, "failed to add pin without name")

// Test 3: List pins with detailed option to get names
pins := make(chan iface.Pin)
go func() {
err = api.Pin().Ls(ctx, pins, opt.Pin.Ls.Detailed(true))
}()

pinMap := make(map[string]string)
for pin := range pins {
pinMap[pin.Path().String()] = pin.Name()
}
require.NoError(t, err, "failed to list pins with names")

// Verify pin names
name1, ok := pinMap[p1.String()]
require.True(t, ok, "pin for %s not found", p1)
require.Equal(t, "test-pin-1", name1, "unexpected pin name for %s", p1)

name2, ok := pinMap[p2.String()]
require.True(t, ok, "pin for %s not found", p2)
require.Empty(t, name2, "expected empty pin name for %s, got '%s'", p2, name2)

// Test 4: Pin update preserves name
err = api.Pin().Add(ctx, p3, opt.Pin.Name("updatable-pin"))
require.NoError(t, err, "failed to add pin with name for update test")

err = api.Pin().Update(ctx, p3, p4)
require.NoError(t, err, "failed to update pin")

// Verify name was preserved after update
pins2 := make(chan iface.Pin)
go func() {
err = api.Pin().Ls(ctx, pins2, opt.Pin.Ls.Detailed(true))
}()

updatedPinMap := make(map[string]string)
for pin := range pins2 {
updatedPinMap[pin.Path().String()] = pin.Name()
}
require.NoError(t, err, "failed to list pins after update")

// Old pin should not exist
_, oldExists := updatedPinMap[p3.String()]
require.False(t, oldExists, "old pin %s should not exist after update", p3)

// New pin should have the preserved name
name4, ok := updatedPinMap[p4.String()]
require.True(t, ok, "updated pin for %s not found", p4)
require.Equal(t, "updatable-pin", name4, "pin name not preserved after update from %s to %s", p3, p4)

// Test 5: Re-pinning with different name updates the name
err = api.Pin().Add(ctx, p1, opt.Pin.Name("new-name-for-p1"))
require.NoError(t, err, "failed to re-pin with new name")

// Verify name was updated
pins3 := make(chan iface.Pin)
go func() {
err = api.Pin().Ls(ctx, pins3, opt.Pin.Ls.Detailed(true))
}()

repinMap := make(map[string]string)
for pin := range pins3 {
repinMap[pin.Path().String()] = pin.Name()
}
require.NoError(t, err, "failed to list pins after re-pin")

rePinnedName, ok := repinMap[p1.String()]
require.True(t, ok, "re-pinned content %s not found", p1)
require.Equal(t, "new-name-for-p1", rePinnedName, "pin name not updated after re-pinning %s", p1)

// Test 6: Direct pin with name
p5, err := api.Unixfs().Add(ctx, strFile("direct-content")())
require.NoError(t, err)

err = api.Pin().Add(ctx, p5, opt.Pin.Recursive(false), opt.Pin.Name("direct-pin-name"))
require.NoError(t, err, "failed to add direct pin with name")

// Verify direct pin has name
directPins := make(chan iface.Pin)
typeOpt, err := opt.Pin.Ls.Type("direct")
require.NoError(t, err, "failed to create type option")
go func() {
err = api.Pin().Ls(ctx, directPins, typeOpt, opt.Pin.Ls.Detailed(true))
}()

directPinMap := make(map[string]string)
for pin := range directPins {
directPinMap[pin.Path().String()] = pin.Name()
}
require.NoError(t, err, "failed to list direct pins")

directName, ok := directPinMap[p5.String()]
require.True(t, ok, "direct pin %s not found", p5)
require.Equal(t, "direct-pin-name", directName, "unexpected name for direct pin %s", p5)

// Test 7: List without detailed option doesn't return names
pinsNoDetails := make(chan iface.Pin)
go func() {
err = api.Pin().Ls(ctx, pinsNoDetails)
}()

noDetailsMap := make(map[string]string)
for pin := range pinsNoDetails {
noDetailsMap[pin.Path().String()] = pin.Name()
}
require.NoError(t, err, "failed to list pins without detailed option")

// All names should be empty without detailed option
for path, name := range noDetailsMap {
require.Empty(t, name, "expected empty name for %s without detailed option, got '%s'", path, name)
}
}

func assertNotPinned(t *testing.T, ctx context.Context, api iface.CoreAPI, p path.Path) {
t.Helper()

Expand Down
4 changes: 4 additions & 0 deletions docs/changelogs/v0.38.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ Gateway error pages now provide more actionable information during content retri
> - **Enhanced error details**: Timeout errors now display the retrieval phase where failure occurred (e.g., "connecting to providers", "fetching data") and up to 3 peer IDs that were attempted but couldn't deliver the content, making it easier to diagnose network or provider issues.
> - **Retry button on all error pages**: Every gateway error page now includes a retry button for quick page refresh without manual URL re-entry.

#### 📌 Pin name improvements

`ipfs pin ls <cid> --names` now correctly returns pin names for specific CIDs ([#10649](https://github.com/ipfs/kubo/issues/10649), [boxo#1035](https://github.com/ipfs/boxo/pull/1035)), and RPC no longer incorrectly returns names from other pins ([#10966](https://github.com/ipfs/kubo/pull/10966)).

#### 🛠️ Identity CID size enforcement and `ipfs files write` fixes

**Identity CID size limits are now enforced**
Expand Down
23 changes: 12 additions & 11 deletions docs/examples/kubo-as-a-library/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ go 1.25
replace github.com/ipfs/kubo => ./../../..

require (
github.com/ipfs/boxo v0.34.1-0.20250918150710-6fe835c011c3
github.com/ipfs/boxo v0.34.1-0.20250919000230-031df82b284f
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
github.com/libp2p/go-libp2p v0.43.0
github.com/multiformats/go-multiaddr v0.16.1
Expand Down Expand Up @@ -73,10 +73,10 @@ require (
github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-bitfield v1.1.0 // indirect
github.com/ipfs/go-block-format v0.2.2 // indirect
github.com/ipfs/go-block-format v0.2.3 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/ipfs/go-cidutil v0.1.0 // indirect
github.com/ipfs/go-datastore v0.8.4 // indirect
github.com/ipfs/go-datastore v0.9.0 // indirect
github.com/ipfs/go-ds-badger v0.3.4 // indirect
github.com/ipfs/go-ds-flatfs v0.5.5 // indirect
github.com/ipfs/go-ds-leveldb v0.5.2 // indirect
Expand All @@ -88,14 +88,14 @@ require (
github.com/ipfs/go-ipfs-pq v0.0.3 // indirect
github.com/ipfs/go-ipfs-redirects-file v0.1.2 // indirect
github.com/ipfs/go-ipld-cbor v0.2.1 // indirect
github.com/ipfs/go-ipld-format v0.6.2 // indirect
github.com/ipfs/go-ipld-format v0.6.3 // indirect
github.com/ipfs/go-ipld-git v0.1.1 // indirect
github.com/ipfs/go-ipld-legacy v0.2.2 // indirect
github.com/ipfs/go-log/v2 v2.8.1 // indirect
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
github.com/ipfs/go-peertaskqueue v0.8.2 // indirect
github.com/ipfs/go-test v0.2.3 // indirect
github.com/ipfs/go-unixfsnode v1.10.1 // indirect
github.com/ipfs/go-unixfsnode v1.10.2 // indirect
github.com/ipld/go-car/v2 v2.15.0 // indirect
github.com/ipld/go-codec-dagpb v1.7.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
Expand Down Expand Up @@ -209,15 +209,16 @@ require (
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
Expand Down
Loading
Loading