Skip to content

Commit 0cd6062

Browse files
authored
feat(pinner): add CheckIfPinnedWithType for efficient checks with names (#1035)
* feat: add CheckIfPinnedWithType for efficient pin checks adds CheckIfPinnedWithType method to Pinner interface that allows checking specific pin types with optional name loading. this enables efficient pin operations like 'ipfs pin ls <cid> --names' without loading all pins. - CheckIfPinned now delegates to CheckIfPinnedWithType for consistency necessary for ipfs/js-kubo-rpc-client#343 * docs: add changelog entry for CheckIfPinnedWithType * test: add tests for CheckIfPinnedWithType tests cover: - type-specific pin checking (direct, recursive, indirect) - pin name loading with includeNames flag - multiple CIDs in single call - edge cases (not pinned, internal mode, invalid mode) - backward compatibility with CheckIfPinned
1 parent cf5a62e commit 0cd6062

File tree

4 files changed

+364
-43
lines changed

4 files changed

+364
-43
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ The following emojis are used to highlight certain changes:
1616

1717
### Added
1818

19+
- `pinning/pinner`: Added `CheckIfPinnedWithType` method to `Pinner` interface for efficient type-specific pin checks with optional name loading ([#1035](https://github.com/ipfs/boxo/pull/1035))
20+
- Enables checking specific pin types (recursive, direct, indirect) without loading all pins
21+
- Optional `includeNames` parameter controls whether pin names are loaded from datastore
22+
- `CheckIfPinned` now delegates to `CheckIfPinnedWithType` for consistency
1923
- `gateway`: Enhanced error handling and UX for timeouts:
2024
- Added retrieval state tracking for timeout diagnostics. When retrieval timeouts occur, the error messages now include detailed information about which phase failed (path resolution, provider discovery, connecting, or data retrieval) and provider statistics including failed peer IDs [#1015](https://github.com/ipfs/boxo/pull/1015) [#1023](https://github.com/ipfs/boxo/pull/1023)
2125
- Added `Config.DiagnosticServiceURL` to configure a CID retrievability diagnostic service. When set, 504 Gateway Timeout errors show a "Check CID retrievability" button linking to the service with `?cid=<failed-cid>` [#1023](https://github.com/ipfs/boxo/pull/1023)

pinning/pinner/dspinner/pin.go

Lines changed: 181 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -585,64 +585,204 @@ func (p *pinner) isPinnedWithType(ctx context.Context, c cid.Cid, mode ipfspinne
585585
//
586586
// TODO: If a CID is pinned by multiple pins, should they all be reported?
587587
func (p *pinner) CheckIfPinned(ctx context.Context, cids ...cid.Cid) ([]ipfspinner.Pinned, error) {
588-
pinned := make([]ipfspinner.Pinned, 0, len(cids))
589-
toCheck := cid.NewSet()
588+
// Simply delegate to CheckIfPinnedWithType with Any mode and no names
589+
return p.CheckIfPinnedWithType(ctx, ipfspinner.Any, false, cids...)
590+
}
590591

592+
// loadPinName attempts to load the pin name if includeNames is true.
593+
// It logs errors but doesn't fail the operation if name loading fails.
594+
func (p *pinner) loadPinName(ctx context.Context, pin *ipfspinner.Pinned, pinID string, includeNames bool) {
595+
if !includeNames {
596+
return
597+
}
598+
pinData, err := p.loadPin(ctx, pinID)
599+
if err != nil {
600+
log.Errorf("failed to load pin %s: %v", pinID, err)
601+
return
602+
}
603+
pin.Name = pinData.Name
604+
}
605+
606+
// CheckIfPinnedWithType implements the Pinner interface, checking specific pin types.
607+
// This method is optimized to only check the requested pin type(s).
608+
func (p *pinner) CheckIfPinnedWithType(ctx context.Context, mode ipfspinner.Mode, includeNames bool, cids ...cid.Cid) ([]ipfspinner.Pinned, error) {
591609
p.lock.RLock()
592610
defer p.lock.RUnlock()
593611

594-
// First check for non-Indirect pins directly
595-
for _, c := range cids {
596-
cidKey := c.KeyString()
597-
has, err := p.cidRIndex.HasAny(ctx, cidKey)
598-
if err != nil {
599-
return nil, err
600-
}
601-
if has {
602-
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Recursive})
603-
} else {
604-
has, err = p.cidDIndex.HasAny(ctx, cidKey)
612+
switch mode {
613+
case ipfspinner.Any:
614+
// Check all pin types
615+
pinned := make([]ipfspinner.Pinned, 0, len(cids))
616+
toCheck := cid.NewSet()
617+
618+
// First check for non-Indirect pins directly
619+
for _, c := range cids {
620+
cidKey := c.KeyString()
621+
622+
// Check recursive pins
623+
ids, err := p.cidRIndex.Search(ctx, cidKey)
605624
if err != nil {
606625
return nil, err
607626
}
608-
if has {
609-
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Direct})
627+
if len(ids) > 0 {
628+
pin := ipfspinner.Pinned{Key: c, Mode: ipfspinner.Recursive}
629+
p.loadPinName(ctx, &pin, ids[0], includeNames)
630+
pinned = append(pinned, pin)
610631
} else {
611-
toCheck.Add(c)
632+
// Check direct pins
633+
ids, err = p.cidDIndex.Search(ctx, cidKey)
634+
if err != nil {
635+
return nil, err
636+
}
637+
if len(ids) > 0 {
638+
pin := ipfspinner.Pinned{Key: c, Mode: ipfspinner.Direct}
639+
p.loadPinName(ctx, &pin, ids[0], includeNames)
640+
pinned = append(pinned, pin)
641+
} else {
642+
toCheck.Add(c)
643+
}
644+
}
645+
}
646+
647+
// Check for indirect pins
648+
if toCheck.Len() > 0 {
649+
var walkErr error
650+
visited := cid.NewSet()
651+
err := p.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
652+
var rk cid.Cid
653+
rk, walkErr = cid.Cast([]byte(key))
654+
if walkErr != nil {
655+
return false
656+
}
657+
walkErr = merkledag.Walk(ctx, merkledag.GetLinksWithDAG(p.dserv), rk, func(c cid.Cid) bool {
658+
if toCheck.Len() == 0 || !visited.Visit(c) {
659+
return false
660+
}
661+
if toCheck.Has(c) {
662+
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Indirect, Via: rk})
663+
toCheck.Remove(c)
664+
}
665+
return true
666+
}, merkledag.Concurrent())
667+
if walkErr != nil {
668+
return false
669+
}
670+
return toCheck.Len() > 0
671+
})
672+
if err != nil {
673+
return nil, err
612674
}
675+
if walkErr != nil {
676+
return nil, walkErr
677+
}
678+
}
679+
680+
// Anything left in toCheck is not pinned
681+
for _, k := range toCheck.Keys() {
682+
pinned = append(pinned, ipfspinner.Pinned{Key: k, Mode: ipfspinner.NotPinned})
683+
}
684+
return pinned, nil
685+
686+
case ipfspinner.Recursive, ipfspinner.Direct:
687+
// Check only the specific index
688+
return p.checkPinsInIndex(ctx, mode, includeNames, cids...)
689+
690+
case ipfspinner.Indirect:
691+
// Only check for indirect pins (expensive - requires traversal of all recursive pins' graphs)
692+
return p.checkIndirectPins(ctx, cids...)
693+
694+
case ipfspinner.Internal:
695+
// Internal pins are not exposed to users, return NotPinned
696+
// Note: this is legacy behavior kept for backward-compatibility
697+
pinned := make([]ipfspinner.Pinned, 0, len(cids))
698+
for _, c := range cids {
699+
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.NotPinned})
613700
}
701+
return pinned, nil
702+
703+
default:
704+
// For unknown modes, return an error to maintain backward compatibility
705+
return nil, fmt.Errorf(
706+
"invalid Pin Mode '%d', must be one of {%d, %d, %d, %d, %d}",
707+
mode, ipfspinner.Direct, ipfspinner.Indirect, ipfspinner.Recursive,
708+
ipfspinner.Internal, ipfspinner.Any)
614709
}
710+
}
615711

616-
var e error
617-
visited := cid.NewSet()
618-
err := p.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
619-
var rk cid.Cid
620-
rk, e = cid.Cast([]byte(key))
621-
if e != nil {
622-
return false
712+
// checkPinsInIndex is a helper that checks for pins in a specific index based on mode (pin type).
713+
// It selects either the recursive or direct index depending on the mode parameter.
714+
func (p *pinner) checkPinsInIndex(ctx context.Context, mode ipfspinner.Mode, includeNames bool, cids ...cid.Cid) ([]ipfspinner.Pinned, error) {
715+
pinned := make([]ipfspinner.Pinned, 0, len(cids))
716+
717+
// Select the appropriate index based on mode (pin type)
718+
var index dsindex.Indexer
719+
if mode == ipfspinner.Recursive {
720+
index = p.cidRIndex
721+
} else {
722+
index = p.cidDIndex
723+
}
724+
725+
for _, c := range cids {
726+
cidKey := c.KeyString()
727+
ids, err := index.Search(ctx, cidKey)
728+
if err != nil {
729+
return nil, err
730+
}
731+
732+
if len(ids) > 0 {
733+
pin := ipfspinner.Pinned{Key: c, Mode: mode}
734+
p.loadPinName(ctx, &pin, ids[0], includeNames)
735+
pinned = append(pinned, pin)
736+
} else {
737+
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.NotPinned})
623738
}
624-
e = merkledag.Walk(ctx, merkledag.GetLinksWithDAG(p.dserv), rk, func(c cid.Cid) bool {
625-
if toCheck.Len() == 0 || !visited.Visit(c) {
739+
}
740+
741+
return pinned, nil
742+
}
743+
744+
// checkIndirectPins checks if the given cids are pinned indirectly
745+
func (p *pinner) checkIndirectPins(ctx context.Context, cids ...cid.Cid) ([]ipfspinner.Pinned, error) {
746+
pinned := make([]ipfspinner.Pinned, 0, len(cids))
747+
toCheck := cid.NewSet()
748+
749+
// Check all CIDs for indirect pins, regardless of their direct pin status
750+
// A CID can be both directly pinned AND indirectly pinned through a parent
751+
for _, c := range cids {
752+
toCheck.Add(c)
753+
}
754+
755+
// Now check for indirect pins by traversing recursive pins
756+
if toCheck.Len() > 0 {
757+
var walkErr error
758+
visited := cid.NewSet()
759+
err := p.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
760+
var rk cid.Cid
761+
rk, walkErr = cid.Cast([]byte(key))
762+
if walkErr != nil {
626763
return false
627764
}
628-
629-
if toCheck.Has(c) {
630-
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Indirect, Via: rk})
631-
toCheck.Remove(c)
765+
walkErr = merkledag.Walk(ctx, merkledag.GetLinksWithDAG(p.dserv), rk, func(c cid.Cid) bool {
766+
if toCheck.Len() == 0 || !visited.Visit(c) {
767+
return false
768+
}
769+
if toCheck.Has(c) {
770+
pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Indirect, Via: rk})
771+
toCheck.Remove(c)
772+
}
773+
return true
774+
}, merkledag.Concurrent())
775+
if walkErr != nil {
776+
return false
632777
}
633-
634-
return true
635-
}, merkledag.Concurrent())
636-
if e != nil {
637-
return false
778+
return toCheck.Len() > 0
779+
})
780+
if err != nil {
781+
return nil, err
782+
}
783+
if walkErr != nil {
784+
return nil, walkErr
638785
}
639-
return toCheck.Len() > 0
640-
})
641-
if err != nil {
642-
return nil, err
643-
}
644-
if e != nil {
645-
return nil, e
646786
}
647787

648788
// Anything left in toCheck is not pinned

0 commit comments

Comments
 (0)