@@ -646,35 +646,9 @@ func (p *pinner) CheckIfPinnedWithType(ctx context.Context, mode ipfspinner.Mode
646
646
647
647
// Check for indirect pins
648
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 {
649
+ if err := p .traverseIndirectPins (ctx , toCheck , & pinned ); err != nil {
673
650
return nil , err
674
651
}
675
- if walkErr != nil {
676
- return nil , walkErr
677
- }
678
652
}
679
653
680
654
// Anything left in toCheck is not pinned
@@ -741,48 +715,93 @@ func (p *pinner) checkPinsInIndex(ctx context.Context, mode ipfspinner.Mode, inc
741
715
return pinned , nil
742
716
}
743
717
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
+
744
761
// checkIndirectPins checks if the given cids are pinned indirectly
745
762
func (p * pinner ) checkIndirectPins (ctx context.Context , cids ... cid.Cid ) ([]ipfspinner.Pinned , error ) {
746
763
pinned := make ([]ipfspinner.Pinned , 0 , len (cids ))
747
764
toCheck := cid .NewSet ()
748
765
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.
751
781
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
752
797
toCheck .Add (c )
753
798
}
754
799
755
800
// Now check for indirect pins by traversing recursive pins
756
801
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 {
781
803
return nil , err
782
804
}
783
- if walkErr != nil {
784
- return nil , walkErr
785
- }
786
805
}
787
806
788
807
// Anything left in toCheck is not pinned
0 commit comments