@@ -646,35 +646,9 @@ func (p *pinner) CheckIfPinnedWithType(ctx context.Context, mode ipfspinner.Mode
646646
647647 // Check for indirect pins
648648 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 {
649+ if err := p .traverseIndirectPins (ctx , toCheck , & pinned ); err != nil {
673650 return nil , err
674651 }
675- if walkErr != nil {
676- return nil , walkErr
677- }
678652 }
679653
680654 // Anything left in toCheck is not pinned
@@ -741,48 +715,93 @@ func (p *pinner) checkPinsInIndex(ctx context.Context, mode ipfspinner.Mode, inc
741715 return pinned , nil
742716}
743717
718+ // traverseIndirectPins is a helper that traverses all recursive pins to find indirect pins.
719+ // It modifies the pinned slice and toCheck set in place.
720+ func (p * pinner ) traverseIndirectPins (ctx context.Context , toCheck * cid.Set , pinned * []ipfspinner.Pinned ) error {
721+ var walkErr error
722+ visited := cid .NewSet ()
723+ err := p .cidRIndex .ForEach (ctx , "" , func (key , value string ) bool {
724+ // Check for context cancellation at the start of each recursive pin
725+ select {
726+ case <- ctx .Done ():
727+ walkErr = ctx .Err ()
728+ return false
729+ default :
730+ }
731+
732+ var rk cid.Cid
733+ rk , walkErr = cid .Cast ([]byte (key ))
734+ if walkErr != nil {
735+ return false
736+ }
737+ walkErr = merkledag .Walk (ctx , merkledag .GetLinksWithDAG (p .dserv ), rk , func (c cid.Cid ) bool {
738+ if toCheck .Len () == 0 || ! visited .Visit (c ) {
739+ return false
740+ }
741+ if toCheck .Has (c ) {
742+ * pinned = append (* pinned , ipfspinner.Pinned {Key : c , Mode : ipfspinner .Indirect , Via : rk })
743+ toCheck .Remove (c )
744+ }
745+ return true
746+ }, merkledag .Concurrent ())
747+ if walkErr != nil {
748+ return false
749+ }
750+ return toCheck .Len () > 0
751+ })
752+ if err != nil {
753+ return err
754+ }
755+ if walkErr != nil {
756+ return walkErr
757+ }
758+ return nil
759+ }
760+
744761// checkIndirectPins checks if the given cids are pinned indirectly
745762func (p * pinner ) checkIndirectPins (ctx context.Context , cids ... cid.Cid ) ([]ipfspinner.Pinned , error ) {
746763 pinned := make ([]ipfspinner.Pinned , 0 , len (cids ))
747764 toCheck := cid .NewSet ()
748765
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
766+ // Filter out CIDs that are recursively pinned at the root level.
767+ // A recursively pinned CID is not considered indirect because recursive pins
768+ // are comprehensive (include all children), making "recursive" take precedence
769+ // over "indirect".
770+ //
771+ // However, we do NOT filter out direct pins here. Direct pins only pin a
772+ // single block, not its children. Therefore, a CID can legitimately be both:
773+ // - Directly pinned (explicitly pinned as a single block)
774+ // - Indirectly pinned (referenced by another pinned object's DAG)
775+ // This is why the asymmetry between recursive and direct pins is intentional.
776+ //
777+ // NOTE: While this behavior may feel arbitrary, we preserve it for compatibility
778+ // as this is how 'ipfs pin ls' has behaved for nearly a decade. The test
779+ // t0081-repo-pinning.sh in Kubo explicitly expects a CID to be both direct
780+ // and indirect, guarding this established behavior.
751781 for _ , c := range cids {
782+ cidKey := c .KeyString ()
783+
784+ // Check if recursively pinned
785+ ids , err := p .cidRIndex .Search (ctx , cidKey )
786+ if err != nil {
787+ return nil , err
788+ }
789+ if len (ids ) > 0 {
790+ // This CID is recursively pinned at root level, not indirect
791+ pinned = append (pinned , ipfspinner.Pinned {Key : c , Mode : ipfspinner .NotPinned })
792+ continue
793+ }
794+
795+ // Still check for indirect even if directly pinned
796+ // A CID can be both direct and indirect
752797 toCheck .Add (c )
753798 }
754799
755800 // Now check for indirect pins by traversing recursive pins
756801 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 {
763- return false
764- }
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
777- }
778- return toCheck .Len () > 0
779- })
780- if err != nil {
802+ if err := p .traverseIndirectPins (ctx , toCheck , & pinned ); err != nil {
781803 return nil , err
782804 }
783- if walkErr != nil {
784- return nil , walkErr
785- }
786805 }
787806
788807 // Anything left in toCheck is not pinned
0 commit comments