@cryptotaxi247 / kubo / commits / 3ae88b220

feat(provide): add `ipfs provide once` and support Interval=0 mode (#11321)

* feat(provide): add ipfs provide once for ad-hoc announcements Adds an experimental subcommand that submits provider records for the given CIDs through the provider system right away, without waiting for the next reprovide cycle. Use -r to walk the DAG and announce every reachable block. Designed against the sweep provider (the default since v0.39): StartProviding queues to the burst-provide workers, which publish records to the DHT efficiently. Works with the legacy provider too, though it queues into the slower serial worker pool. CIDs must already exist in the local blockstore. Re-announcement on the regular schedule is governed by Provide.Strategy and Provide.DHT.Interval; this command does not change either. * refactor(routing): deprecate ipfs routing provide Marks `ipfs routing provide` as deprecated and points users at the new `ipfs provide once`. The command keeps its existing Run, Encoders, and flags so existing scripts continue to work; only the status flag and helptext change. * docs(routing): clarify when ipfs routing reprovide applies Tightens the helptext and the sweep-mode error message so the constraint is obvious: this command only triggers a cycle on the legacy provider, and points users at 'ipfs provide stat --all' for monitoring the default sweep schedule. * docs: tighten provide helptext and update routing-provide references Updates docs/config.md and docs/experimental-features.md to reference 'ipfs provide once' instead of 'ipfs routing provide'. Tightens the helptext for 'ipfs provide clear' and the 'ipfs provide stat' overview: drops headings around short paragraphs, prefers active voice, and notes that the sweep provider is the default. * docs: changelog entry for ipfs provide once * docs: use 'provide system' wording consistently * test(provide): cover --recursive and multi-CID paths for provide once Adds two subtests under runProviderSuite (run for both Legacy and Sweep): - --recursive walks the DAG and announces every chunk of a 2 MiB file added with --pin=false under Provide.Strategy=roots, so the auto- provide path stays out of the way. - multiple CIDs in a single invocation succeed and the text encoder reports 'queued 3 CID(s) for immediate provide'. * feat(provide): stream cids and per-cid output for ipfs provide once Each CID flows through the command independently, so stdin can be piped without buffering and consumers see results as they happen. - Run reads CIDs from argv and then from BodyArgs (stdin scanner) one at a time, calling StartProviding per CID. - With -r, the dag.Walk visit callback emits per visited block; the walk cancels its context on the first announce error to stop fetching. - A typed ProvideOnceEvent (one per queued CID) replaces the prior batch result. JSON output streams {"Queued":"<cid>"} per line. - Text output via PostRun: when stderr is a tty, the running count is redrawn on a single line; otherwise a final count is printed. The text encoder still works for HTTP/RPC consumers (one CID per line). - Adds tests for stdin streaming and --enc=json one-event-per-line. * feat(provide): dedupe across all roots and recursive walks Previously the cid set was scoped per root, so a CID shared by two arguments or by two recursive DAG walks was announced twice. Move the set out to the Run scope so each unique CID is announced exactly once per invocation, regardless of how many times it shows up in argv, stdin, or the DAG walks. For -r, hitting an already-seen CID also stops descent into that subtree, avoiding redundant block fetches when DAGs overlap. * style(provide): rename useTTY to isTTY in PostRun * refactor(provide): align ipfs provide once with kubo cmds-lib idioms - Use the existing argumentIterator helper from cid.go to read argv followed by stdin, replacing the inlined two-loop variant. - Document why PostRun forks on encoder type (TTY redraw needs to bypass the encoder; json/xml must keep streaming through it). - Log an ERROR for unexpected response types instead of dropping them silently, mirroring the defensive pattern in cat.go's PostRun. * docs(routing): document streaming limitations of routing provide Spell out what 'ipfs routing provide' does worse than 'ipfs provide once' so users on the deprecation path know why to switch: input buffering, no per-cid output, no dedup across recursive roots, and the sync dht lookup that defeats sweep batching. * docs(changelog): rewrite ipfs provide once entry around user impact Recasts the highlight to lead with what the user can now do, not what the code does internally. Adds a one-line example showing the streaming stdin path that the previous version did not surface, and replaces "namespace" plumbing language with the actual capabilities (running count, json-per-line, single announcement per shared block under -r). * feat(provide): use boxo BloomTracker for cross-input dedup Swaps the cid.Set used by 'ipfs provide once' for the autoscaling boxo BloomTracker, the same dedup mechanism that powers Provide.Strategy=+unique. Run executes on the daemon, not the cli, so this caps daemon memory under hostile or accidental input: a user piping 100M cids previously would have grown the daemon's set to ~7 gb of resident memory; with the bloom chain it plateaus around 700 mb at the default fp rate, and under 100 mb up to 10m unique cids. The trade-off is a small false-positive rate (~1 in 4.75m, the kubo default) that can cause an occasional cid to be silently skipped. For ad-hoc providing this is acceptable; the regular reprovide cycle will pick up anything matched by Provide.Strategy on the next pass. * docs(changelog): use ipfs refs as the provide once example * style(provide): goimports import order * docs(provide): soften dedup wording, comment re.Emit gate, cover Provide.Enabled=false - Change "exactly once per invocation" to acknowledge the bloom false-positive rate now that the dedup is probabilistic. - Add a comment to the text branch of PostRun warning future readers not to call re.Emit there, since the encoder would race with the TTY counter. - Add a runProviderSuite subtest that exercises Provide.Enabled=false through the new code path (the existing routing-provide test only covers the deprecated alias's Run). * docs(changelog): clarify provide once use case and add second example - Note that provide once is also for fine-tuned control over which CIDs get announced when, alongside the regular reprovide schedule. - Add a second example using ipfs pin ls so users see the pattern for replaying their pinset alongside the dag-walk pattern. * feat(provide): error on ipfs provide once with Provide.DHT.Interval=0 When Provide.DHT.Interval=0, kubo wires NoopProvider via OnlineProviders -> OfflineProviders, so StartProviding silently no-ops and the cid never gets announced. provide once was returning success without any DHT publish: a footgun. Add an explicit precondition check that mirrors the routing reprovide error path. Decoupling the wiring so ad-hoc provide works under Interval=0 is tracked separately. * chore(deps): pin go-libp2p-kad-dht to PR #1246 head Pulls in the WithReprovideInterval(0) burst-only mode from https://github.com/libp2p/go-libp2p-kad-dht/pull/1246 so the kubo side of the Provide.DHT.Interval=0 decoupling can be developed against it. * chore(deps): re-pin go-libp2p-kad-dht to PR #1246 head Updates to the latest commit on the upstream branch (817031b) which also relaxes the dual SweepingProvider's reprovide-interval validator to accept 0, on top of the single-provider relaxation in the previous pseudo-version. * feat(provide): decouple Provide.DHT.Interval=0 from the master kill-switch Provide.Enabled is now the only switch that fully turns off the provide system. Provide.DHT.Interval=0 disables only the periodic reprovide schedule; new CIDs still announce via fast-provide-root and 'ipfs provide once'. - groups.go: drop the Interval=0 factor from isProviderEnabled. The real provider (sweep or legacy) is now wired even when Interval=0. - provider.go: skip the keystore sync goroutine in no-schedule mode. The ticker would panic on a zero interval, and with no schedule the keystore has no reader. - cmdenv/env.go: drop the fast-provide-root short-circuit on Interval=0. Provide.Enabled=false is now the only short-circuit. - commands/provide.go: drop the temporary 'cannot provide: Provide.DHT.Interval is 0' error from 'ipfs provide once'. - test/cli: replace the 'Reprovide.Interval=0 disables announcement of new CID too' test (premise is now false) with one asserting that Interval=0 + Enabled=true keeps announcing. Convert the provide-once + Interval=0 test from error path to success path. Tighten the legacy 'Manual Reprovide trigger' test to focus on the error contract. Requires upstream go-libp2p-kad-dht support for WithReprovideInterval(0) (kept under PR #1246). * feat(config): require explicit Provide.Enabled when Provide.DHT.Interval=0 Provide.DHT.Interval=0 used to disable the entire provide system as a side effect. After the decoupling it disables only the periodic reprovide schedule, while new CIDs still announce via fast-provide-root and 'ipfs provide once'. To prevent silent semantic drift on upgrade, the daemon now refuses to start when Interval is explicitly set to 0 unless Provide.Enabled is also set explicitly: - Provide.Enabled=false fully disables providing (the old behaviour). - Provide.Enabled=true keeps ad-hoc providing while skipping the periodic reprovide schedule. The error message names both options so operators can pick the one that matches their intent without reading the changelog. * docs: explain new Provide.DHT.Interval=0 semantic Updates docs/config.md and the v0.42 changelog: Interval=0 now disables only the periodic reprovide schedule, and the daemon refuses to start without an explicit Provide.Enabled in that configuration. Calls out both upgrade paths (Provide.Enabled=false to fully disable, or =true to keep ad-hoc providing). * chore(deps): re-pin go-libp2p-kad-dht to amended PR #1246 head Picks up the timeOffset/timeBetween zero-guards so SweepingProvider.Stats() no longer panics with reprovideInterval=0. Required for 'ipfs provide stat' to work in no-schedule mode. * test(provide): align test expectations with new no-schedule semantic - core/commands/commands_test.go: register /provide/once in the expected command list. - test/cli/provide_stats_test.go: 'ipfs provide stat' with Provide.DHT.Interval=0 now returns valid stats (with the schedule timing fields zeroed) instead of erroring out. Update the assertion to match. * chore(deps): re-pin go-libp2p-kad-dht to amended PR #1246 head Picks up the scheduleEnabled() consistency cleanup so timeOffset and timeBetween match the rest of the upstream gates. * chore(deps): re-pin go-libp2p-kad-dht to PR #1246 merge on master picks up the three follow-up commits guillaumemichel pushed before merging libp2p/go-libp2p-kad-dht#1246: - refactor: simplify StartProvide() - refactor: minimize change diff - fix: don't remove from keystore on StopProviding() * fix(provide): use ProvideOnce in `ipfs provide once` `ipfs provide once` was calling StartProviding, which in sweep mode persists keys to the keystore and adds them to the periodic reprovide schedule. that contradicts the command's name and help text. switch to ProvideOnce so the command publishes once and leaves the schedule untouched. for the legacy provider StartProviding already wraps ProvideOnce, so legacy behaviour is unchanged. also tighten the help text to state plainly that the schedule is not modified. * fix(provider): keep keystore inert when Provide.DHT.Interval=0 In no-schedule mode the keystore has no reader (no reprovide loop) and no writer (kad-dht's burst path skips Put/Delete). Until now we still opened on-disk leveldb/pebble files for it: wasted disk and noise on upgrade/downgrade. Switch the keystore to an in-memory map in no-schedule mode and make destroyDs a no-op. Also purge any pre-existing keystore directory once at startup so users who toggle from schedule to no-schedule reclaim disk. Replace the literal `reprovideInterval == 0` check at the second call site with the named noScheduleMode flag for consistency.

Marcin Rataj committed May 15, 2026 at 01:11 UTC 3ae88b22052d4831a6c3596fdf663a167fa44379
14 files changed +601 -78
config/provide.go
+13
@@ -217,6 +217,19 @@ func ValidateProvideConfig(cfg *Provide) error {
217 if interval < 0 {
218 return fmt.Errorf("Provide.DHT.Interval must be non-negative, got %v", interval)
219 }
220 + // Provide.DHT.Interval=0 used to disable the entire provide system as a
221 + // side effect. It now disables only the periodic reprovide schedule:
222 + // new CIDs still announce via fast-provide-root and 'ipfs provide once'.
223 + // Operators upgrading from earlier kubo versions must opt in to one of
224 + // the two semantics by setting Provide.Enabled explicitly:
225 + // - Provide.Enabled=false fully disables providing (the old behaviour).
226 + // - Provide.Enabled=true keeps ad-hoc providing while disabling the
227 + // periodic reprovide schedule.
228 + if interval == 0 && cfg.Enabled == Default {
229 + return fmt.Errorf("Provide.DHT.Interval=0 no longer disables the provide system on its own; set Provide.Enabled explicitly: " +
230 + "Provide.Enabled=false to fully disable providing, or Provide.Enabled=true to keep ad-hoc 'ipfs provide once' " +
231 + "and fast-provide-root working while skipping the periodic reprovide schedule")
232 + }
233 }
234
235 // Validate MaxWorkers
config/provide_test.go
+11 -7
@@ -155,21 +155,25 @@ func TestValidateProvideConfig_Interval(t *testing.T) {
155 tests := []struct {
156 name string
157 interval time.Duration
158 + enabled Flag
159 wantErr bool
160 errMsg string
161 }{
161 - {"valid default (22h)", 22 * time.Hour, false, ""},
162 - {"valid max (48h)", 48 * time.Hour, false, ""},
163 - {"valid small (1h)", 1 * time.Hour, false, ""},
164 - {"valid zero (disabled)", 0, false, ""},
165 - {"invalid over limit (49h)", 49 * time.Hour, true, "must be less than or equal to DHT provider record validity"},
166 - {"invalid over limit (72h)", 72 * time.Hour, true, "must be less than or equal to DHT provider record validity"},
167 - {"invalid negative", -1 * time.Hour, true, "must be non-negative"},
162 + {"valid default (22h)", 22 * time.Hour, Default, false, ""},
163 + {"valid max (48h)", 48 * time.Hour, Default, false, ""},
164 + {"valid small (1h)", 1 * time.Hour, Default, false, ""},
165 + {"valid zero with explicit Enabled=true", 0, True, false, ""},
166 + {"valid zero with explicit Enabled=false", 0, False, false, ""},
167 + {"invalid zero without explicit Provide.Enabled", 0, Default, true, "set Provide.Enabled explicitly"},
168 + {"invalid over limit (49h)", 49 * time.Hour, Default, true, "must be less than or equal to DHT provider record validity"},
169 + {"invalid over limit (72h)", 72 * time.Hour, Default, true, "must be less than or equal to DHT provider record validity"},
170 + {"invalid negative", -1 * time.Hour, Default, true, "must be non-negative"},
171 }
172
173 for _, tt := range tests {
174 t.Run(tt.name, func(t *testing.T) {
175 cfg := &Provide{
176 + Enabled: tt.enabled,
177 DHT: ProvideDHT{
178 Interval: NewOptionalDuration(tt.interval),
179 },
core/commands/cmdenv/env.go
-3
@@ -148,9 +148,6 @@ func ExecuteFastProvideRoot(
148 case !cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled):
149 log.Debugw("fast-provide-root: skipped", "reason", "Provide.Enabled is false")
150 return nil
151 - case cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) == 0:
152 - log.Debugw("fast-provide-root: skipped", "reason", "Provide.DHT.Interval is 0")
153 - return nil
151 case !ipfsNode.HasActiveDHTClient():
152 log.Debugw("fast-provide-root: skipped", "reason", "DHT not available")
153 return nil
core/commands/commands_test.go
+1
@@ -174,6 +174,7 @@ func TestCommands(t *testing.T) {
174 "/ping",
175 "/provide",
176 "/provide/clear",
177 + "/provide/once",
178 "/provide/stat",
179 "/pubsub",
180 "/pubsub/ls",
core/commands/provide.go
+234 -33
@@ -5,15 +5,19 @@ import (
5 "errors"
6 "fmt"
7 "io"
8 + "os"
9 "strings"
10 "text/tabwriter"
11 "time"
12 "unicode/utf8"
13
14 humanize "github.com/dustin/go-humanize"
15 + "github.com/ipfs/boxo/dag/walker"
16 + dag "github.com/ipfs/boxo/ipld/merkledag"
17 boxoprovider "github.com/ipfs/boxo/provider"
18 cid "github.com/ipfs/go-cid"
19 cmds "github.com/ipfs/go-ipfs-cmds"
20 + "github.com/ipfs/kubo/config"
21 "github.com/ipfs/kubo/core/commands/cmdenv"
22 "github.com/libp2p/go-libp2p-kad-dht/fullrt"
23 "github.com/libp2p/go-libp2p-kad-dht/provider"
@@ -23,6 +27,7 @@ import (
27 routing "github.com/libp2p/go-libp2p/core/routing"
28 "github.com/probe-lab/go-libdht/kad/key"
29 "golang.org/x/exp/constraints"
30 + "golang.org/x/term"
31 )
32
33 const (
@@ -52,10 +57,9 @@ Control providing operations.
57
58 OVERVIEW:
59
55 -The provider system advertises content by publishing provider records,
56 -allowing other nodes to discover which peers have specific content.
57 -Content is reprovided periodically (every Provide.DHT.Interval)
58 -according to Provide.Strategy.
60 +The provide system publishes provider records so other peers can discover
61 +which nodes hold each CID. Content is reprovided periodically (every
62 +Provide.DHT.Interval) according to Provide.Strategy.
63
64 CONFIGURATION:
65
@@ -63,12 +67,13 @@ Learn more: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide
67
68 SEE ALSO:
69
66 -For ad-hoc one-time provide, see 'ipfs routing provide'
70 +For ad-hoc immediate announcements, see 'ipfs provide once'.
71 `,
72 },
73
74 Subcommands: map[string]*cmds.Command{
75 "clear": provideClearCmd,
76 + "once": provideOnceCmd,
77 "stat": provideStatCmd,
78 },
79 }
@@ -78,20 +83,14 @@ var provideClearCmd = &cmds.Command{
83 Helptext: cmds.HelpText{
84 Tagline: "Clear all CIDs from the provide queue.",
85 ShortDescription: `
81 -Clear all CIDs pending to be provided for the first time.
86 +Clears the provide queue: CIDs waiting to be advertised to the DHT for the
87 +first time. Does not affect content that is already being reprovided on
88 +schedule.
89
83 -BEHAVIOR:
90 +Kubo also clears the queue automatically on restart when it detects a
91 +change of Provide.Strategy.
92
85 -This command removes CIDs from the provide queue that are waiting to be
86 -advertised to the DHT for the first time. It does not affect content that
87 -is already being reprovided on schedule.
88 -
89 -AUTOMATIC CLEARING:
90 -
91 -Kubo will automatically clear the queue when it detects a change of
92 -Provide.Strategy upon a restart.
93 -
94 -Learn: https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy
93 +See: https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy
94 `,
95 },
96 Options: []cmds.Option{
@@ -130,6 +129,211 @@ Learn: https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy
129 },
130 }
131
132 +// ProvideOnceEvent is emitted once per CID announced by 'ipfs provide once'.
133 +type ProvideOnceEvent struct {
134 + Queued string
135 +}
136 +
137 +var provideOnceCmd = &cmds.Command{
138 + Status: cmds.Experimental,
139 + Helptext: cmds.HelpText{
140 + Tagline: "Announce CIDs to the routing system on demand.",
141 + ShortDescription: `
142 +Publishes provider records for the given CIDs once. The periodic
143 +reprovide schedule (driven by Provide.Strategy and Provide.DHT.Interval)
144 +is left unchanged: CIDs announced here are NOT added to the schedule.
145 +CIDs can be passed as arguments or streamed from stdin (one per line).
146 +
147 +The default sweep provider (Provide.DHT.SweepEnabled=true) submits the CIDs
148 +to its burst-provide queue and returns as each CID is queued; dedicated
149 +burst workers publish the records to the DHT. Use 'ipfs provide stat' to
150 +monitor progress.
151 +
152 +The legacy provider (Provide.DHT.SweepEnabled=false) queues the CIDs for
153 +its serial worker pool, which publishes one CID at a time and may take
154 +significantly longer to complete.
155 +
156 +Use --recursive to walk the DAG and announce every reachable block. With
157 +the default Provide.Strategy=all, every block is already announced, so -r
158 +is only useful with selective strategies like 'roots' or 'pinned+entities'.
159 +
160 +CIDs must already exist in the local blockstore.
161 +
162 +CIDs are deduplicated across arguments, stdin, and DAG walks. Dedup uses
163 +a bloom filter, so at very large scale a small fraction of CIDs may be
164 +skipped (default rate ~1 in 4.75M).
165 +
166 +OUTPUT:
167 +
168 +Output is streamed as each CID is queued. With --enc=json, one
169 +{"Queued": "<cid>"} object is emitted per line. With the text encoder
170 +(default) on a terminal, a single line shows the running count; on a pipe,
171 +a final count is printed at the end.
172 +`,
173 + },
174 + Arguments: []cmds.Argument{
175 + cmds.StringArg("cid", true, true, "The CID(s) to announce.").EnableStdin(),
176 + },
177 + Options: []cmds.Option{
178 + cmds.BoolOption(recursiveOptionName, "r", "Recursively announce the entire DAG."),
179 + },
180 + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
181 + nd, err := cmdenv.GetNode(env)
182 + if err != nil {
183 + return err
184 + }
185 + if !nd.IsOnline {
186 + return ErrNotOnline
187 + }
188 + cfg, err := nd.Repo.Config()
189 + if err != nil {
190 + return err
191 + }
192 + if !cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled) {
193 + return errors.New("cannot provide: Provide.Enabled is false")
194 + }
195 + if len(nd.PeerHost.Network().Conns()) == 0 && !cfg.HasHTTPProviderConfigured() {
196 + return errors.New("cannot provide: no connected peers")
197 + }
198 +
199 + recursive, _ := req.Options[recursiveOptionName].(bool)
200 +
201 + // seen deduplicates across all roots and recursive walks, so a CID
202 + // shared by multiple roots (or repeated in argv/stdin) is announced
203 + // exactly once per invocation. The bloom autoscales as more CIDs
204 + // arrive, keeping memory bounded for arbitrarily large inputs at
205 + // the cost of a small false-positive rate (default ~1 in 4.75M)
206 + // that may cause an occasional CID to be skipped.
207 + seen, err := walker.NewBloomTracker(walker.MinBloomCapacity, walker.DefaultBloomFPRate)
208 + if err != nil {
209 + return err
210 + }
211 +
212 + // announce queues a single CID into the provide system and emits one
213 + // event for it. Uses ProvideOnce so the CID is published without
214 + // being added to the keystore: the periodic reprovide schedule
215 + // (driven by Provide.Strategy) is unaffected. Errors propagate to
216 + // the caller.
217 + announce := func(c cid.Cid) error {
218 + if err := nd.Provider.ProvideOnce(c.Hash()); err != nil {
219 + return err
220 + }
221 + return res.Emit(&ProvideOnceEvent{Queued: c.String()})
222 + }
223 +
224 + // processRoot validates a root CID against the local blockstore and
225 + // announces either just that CID or every block reachable from it.
226 + processRoot := func(arg string) error {
227 + c, err := cid.Decode(arg)
228 + if err != nil {
229 + return fmt.Errorf("invalid CID %q: %w", arg, err)
230 + }
231 + has, err := nd.Blockstore.Has(req.Context, c)
232 + if err != nil {
233 + return err
234 + }
235 + if !has {
236 + return fmt.Errorf("block %s not found locally, cannot provide", c)
237 + }
238 +
239 + if !recursive {
240 + if !seen.Visit(c) {
241 + return nil
242 + }
243 + return announce(c)
244 + }
245 +
246 + // Stream per-block: visit emits as it walks. Cancel the walk on
247 + // the first announce error so we don't keep fetching DAG nodes
248 + // after we've already failed.
249 + ctx, cancel := context.WithCancel(req.Context)
250 + defer cancel()
251 + var visitErr error
252 + walkErr := dag.Walk(ctx, dag.GetLinksDirect(nd.DAG), c, func(child cid.Cid) bool {
253 + // Skip subtrees we've already walked from a previous root or
254 + // argument: returning false stops descent into this node.
255 + if !seen.Visit(child) {
256 + return false
257 + }
258 + if err := announce(child); err != nil {
259 + visitErr = err
260 + cancel()
261 + return false
262 + }
263 + return true
264 + })
265 + if visitErr != nil {
266 + return visitErr
267 + }
268 + return walkErr
269 + }
270 +
271 + args := argumentIterator{req.Arguments, req.BodyArgs()}
272 + for {
273 + arg, ok := args.next()
274 + if !ok {
275 + break
276 + }
277 + if err := processRoot(arg); err != nil {
278 + return err
279 + }
280 + }
281 + return args.err()
282 + },
283 + PostRun: cmds.PostRunMap{
284 + cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
285 + // In text mode we render the running counter and final summary
286 + // directly to stderr/stdout, bypassing the encoder so the TTY
287 + // redraw works. For other encoders (json, xml) we must let the
288 + // encoder serialize each event, so forward the stream as-is.
289 + if enc, _ := res.Request().Options[cmds.EncLong].(string); enc != "" && enc != cmds.Text {
290 + return cmds.Copy(re, res)
291 + }
292 +
293 + // Text mode: render directly to stderr/stdout below. Do not
294 + // call re.Emit from this branch, or output will race with the
295 + // running counter.
296 + isTTY := term.IsTerminal(int(os.Stderr.Fd()))
297 + var count int
298 + for {
299 + v, err := res.Next()
300 + if err == io.EOF {
301 + break
302 + }
303 + if err != nil {
304 + if isTTY && count > 0 {
305 + fmt.Fprintln(os.Stderr)
306 + }
307 + return err
308 + }
309 + if _, ok := v.(*ProvideOnceEvent); !ok {
310 + log.Errorf("provide once postrun: received unexpected type %T", v)
311 + continue
312 + }
313 + count++
314 + if isTTY {
315 + fmt.Fprintf(os.Stderr, "\rqueued %d CID(s) for immediate provide", count)
316 + }
317 + }
318 + if isTTY && count > 0 {
319 + fmt.Fprintln(os.Stderr)
320 + } else {
321 + fmt.Fprintf(os.Stdout, "queued %d CID(s) for immediate provide\n", count)
322 + }
323 + return nil
324 + },
325 + },
326 + Type: ProvideOnceEvent{},
327 + Encoders: cmds.EncoderMap{
328 + // Used when PostRun is not invoked (HTTP API consumers in text mode).
329 + // One CID per line keeps the stream pipe-friendly.
330 + cmds.Text: cmds.MakeTypedEncoder(func(_ *cmds.Request, w io.Writer, e *ProvideOnceEvent) error {
331 + _, err := fmt.Fprintf(w, "%s\n", e.Queued)
332 + return err
333 + }),
334 + },
335 +}
336 +
337 type provideStats struct {
338 Sweep *stats.Stats
339 Legacy *boxoprovider.ReproviderStats
@@ -159,30 +363,27 @@ func extractSweepingProvider(prov any, useLAN bool) *provider.SweepingProvider {
363 var provideStatCmd = &cmds.Command{
364 Status: cmds.Experimental,
365 Helptext: cmds.HelpText{
162 - Tagline: "Show statistics about the provider system",
366 + Tagline: "Show statistics about the provide system",
367 ShortDescription: `
164 -Returns statistics about the node's provider system.
368 +Returns statistics about the node's provide system.
369
370 OVERVIEW:
371
168 -The provide system advertises content to the DHT by publishing provider
169 -records that map CIDs to your peer ID. These records expire after a fixed
170 -TTL to account for node churn, so content must be reprovided periodically
171 -to stay discoverable.
372 +The provide system publishes provider records mapping CIDs to your peer
373 +ID. Records expire after a fixed TTL, so the system reprovides them on a
374 +schedule to keep content discoverable.
375
376 Two provider types exist:
377
175 -- Sweep provider: Divides the DHT keyspace into regions and systematically
176 - sweeps through them over the reprovide interval. Batches CIDs allocated
378 +- Sweep provider (default): divides the DHT keyspace into regions and
379 + sweeps through them over the reprovide interval. Batches CIDs that map
380 to the same DHT servers, reducing lookups from N (one per CID) to a
178 - small static number based on DHT size (~3k for 10k DHT servers). Spreads
179 - work evenly over time to prevent resource spikes and ensure announcements
180 - happen just before records expire.
181 -
182 -- Legacy provider: Processes each CID individually with separate DHT
183 - lookups. Attempts to reprovide all content as quickly as possible at the
184 - start of each cycle. Works well for small datasets but struggles with
185 - large collections.
381 + small constant based on DHT size (~3k for 10k DHT servers). Spreads work
382 + evenly over time and announces records just before they expire.
383 +
384 +- Legacy provider: announces each CID with a separate DHT lookup. Tries
385 + to reprovide all content as fast as possible at each cycle start. Fine
386 + for small datasets, slow past a few thousand CIDs.
387
388 Learn more:
389 - Config: https://github.com/ipfs/kubo/blob/master/docs/config.md#provide
core/commands/routing.go
+27 -9
@@ -142,9 +142,27 @@ const (
142 )
143
144 var provideRefRoutingCmd = &cmds.Command{
145 - Status: cmds.Experimental,
145 + Status: cmds.Deprecated,
146 Helptext: cmds.HelpText{
147 - Tagline: "Announce to the network that you are providing given values.",
147 + Tagline: "Deprecated, use 'ipfs provide once' instead.",
148 + ShortDescription: `
149 +'ipfs routing provide' has moved to 'ipfs provide once'. This command keeps
150 +its existing behavior so existing scripts continue to work, but will be
151 +removed in a future release.
152 +
153 +Compared to 'ipfs provide once', this command:
154 +
155 +- Buffers all CIDs from arguments and stdin before doing any work,
156 + instead of streaming them as they arrive.
157 +- Emits no per-CID output: there is no JSON event stream and the -v
158 + flag's per-peer events do not actually propagate to the encoder.
159 +- With -r, re-walks subtrees shared between roots and re-announces
160 + shared blocks; 'ipfs provide once' deduplicates across all inputs.
161 +- Issues an extra synchronous DHT lookup per CID on top of the
162 + provider system, which defeats sweep batching.
163 +
164 +Prefer 'ipfs provide once' for new scripts and any large input.
165 +`,
166 },
167
168 Arguments: []cmds.Argument{
@@ -271,14 +289,14 @@ var provideRefRoutingCmd = &cmds.Command{
289 var reprovideRoutingCmd = &cmds.Command{
290 Status: cmds.Deprecated,
291 Helptext: cmds.HelpText{
274 - Tagline: "Trigger reprovider (legacy provider only).",
292 + Tagline: "Trigger a reprovide cycle (legacy provider only).",
293 ShortDescription: `
276 -Trigger reprovider to announce our data to network.
294 +Forces the legacy provider to reprovide all locally stored CIDs that match
295 +Provide.Strategy.
296
278 -Only available with the legacy provider (Provide.DHT.SweepEnabled=false).
279 -Returns an error when Provide.DHT.SweepEnabled=true (the default).
280 -The sweep provider reprovides automatically on schedule.
281 -Use 'ipfs provide stat -a' to monitor reprovide progress.
297 +Only works when Provide.DHT.SweepEnabled=false. With the default sweep
298 +provider, reproviding is continuous and scheduled, so this command returns
299 +an error. Use 'ipfs provide stat --all' to monitor sweep progress.
300 `,
301 },
302 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
@@ -303,7 +321,7 @@ Use 'ipfs provide stat -a' to monitor reprovide progress.
321 }
322 provideSys, ok := nd.Provider.(provider.Reprovider)
323 if !ok {
306 - err := errors.New("invalid configuration: manual reprovide not available with sweep provider (Provide.DHT.SweepEnabled=true), use 'ipfs provide stat -a' to monitor automatic reprovide progress")
324 + err := errors.New("manual reprovide is not available with the sweep provider; set Provide.DHT.SweepEnabled=false to use the legacy provider, or run 'ipfs provide stat --all' to monitor the sweep schedule")
325 log.Error(err)
326 return err
327 }
core/node/groups.go
+5 -3
@@ -349,9 +349,11 @@ func Online(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
349 isBitswapServerEnabled := cfg.Bitswap.ServerEnabled.WithDefault(config.DefaultBitswapServerEnabled)
350 isHTTPRetrievalEnabled := cfg.HTTPRetrieval.Enabled.WithDefault(config.DefaultHTTPRetrievalEnabled)
351
352 - // The Provide system handles both new CID announcements and periodic re-announcements.
353 - // Disabling is controlled by Provide.Enabled=false or setting Interval to 0.
354 - isProviderEnabled := cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled) && cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) != 0
352 + // The Provide system handles both new CID announcements and periodic
353 + // re-announcements. Provide.Enabled=false fully disables it.
354 + // Provide.DHT.Interval=0 disables only the periodic reprovide schedule;
355 + // new CIDs still announce via fast-provide-root and 'ipfs provide once'.
356 + isProviderEnabled := cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled)
357
358 return fx.Options(
359 fx.Provide(BitswapOptions(cfg)),
core/node/provider.go
+29 -3
@@ -600,6 +600,11 @@ func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error
600
601 func SweepingProviderOpt(cfg *config.Config) fx.Option {
602 reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval)
603 + // noScheduleMode is true when the user disabled the periodic reprovide
604 + // schedule (Provide.DHT.Interval=0). In this mode the keystore is
605 + // inert: kad-dht's burst-only path (ProvideOnce, StartProviding) does
606 + // not Put or Delete keys, and no reprovide loop runs to read them.
607 + noScheduleMode := reprovideInterval == 0
608 type providerInput struct {
609 fx.In
610 DHT routing.Routing `name:"dhtc"`
@@ -626,9 +631,9 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
631 if err := validateKeystoreSuffix(suffix); err != nil {
632 return nil, err
633 }
629 - // When no datastore spec is configured (e.g., test/mock repos),
630 - // fall back to an in-memory datastore.
631 - if rootSpec == nil {
634 + // In-memory datastore in no-schedule mode (keystore is inert)
635 + // or when no datastore spec is configured (test/mock repos).
636 + if noScheduleMode || rootSpec == nil {
637 return datastore.NewMapDatastore(), nil
638 }
639 if err := os.MkdirAll(keystoreBasePath, 0o755); err != nil {
@@ -646,10 +651,25 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
651 if err := validateKeystoreSuffix(suffix); err != nil {
652 return err
653 }
654 + if noScheduleMode {
655 + return nil
656 + }
657 providerLog.Infow("provider keystore: removing datastore from disk", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix))
658 return os.RemoveAll(filepath.Join(keystoreBasePath, suffix))
659 }
660
661 + // In no-schedule mode the on-disk keystore is never used. If a
662 + // previous run was in schedule mode it may have left data behind;
663 + // purge it once on startup to free disk.
664 + if noScheduleMode {
665 + if _, statErr := os.Stat(keystoreBasePath); statErr == nil {
666 + providerLog.Infow("provider keystore: purging on-disk data (Provide.DHT.Interval=0)", "path", keystoreBasePath)
667 + if rmErr := os.RemoveAll(keystoreBasePath); rmErr != nil {
668 + providerLog.Warnw("provider keystore: purge failed", "path", keystoreBasePath, "err", rmErr)
669 + }
670 + }
671 + }
672 +
673 // One-time cleanup of stale keystore data left by older Kubo in the
674 // shared repo datastore under /provider/keystore/. New code stores
675 // bulk key data in separate filesystem datastores under
@@ -820,6 +840,12 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
840 if _, ok := in.Provider.(*NoopProvider); ok {
841 return
842 }
843 + // In no-schedule mode no reprovide loop runs, so there is no
844 + // reader for the keystore and no need to sync it. The zero
845 + // interval would also panic the periodic sync ticker.
846 + if noScheduleMode {
847 + return
848 + }
849
850 var (
851 cancel context.CancelFunc
docs/changelogs/v0.42.md
+34
@@ -10,6 +10,8 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
10
11 - [Overview](#overview)
12 - [🔦 Highlights](#-highlights)
13 + - [🎯 Announce CIDs on demand with `ipfs provide once`](#-announce-cids-on-demand-with-ipfs-provide-once)
14 + - [⚙️ `Provide.DHT.Interval=0` no longer disables providing](#%EF%B8%8F-providedhtinterval0-no-longer-disables-providing)
15 - [🐛 Fixed pin operations hanging under pinned reprovide strategies](#-fixed-pin-operations-hanging-under-pinned-reprovide-strategies)
16 - [🐛 Reliable shutdown and container health checks](#-reliable-shutdown-and-container-health-checks)
17 - [🚨 ERROR log for listeners blocked by `Swarm.AddrFilters` or `Addresses.NoAnnounce`](#-error-log-for-listeners-blocked-by-swarmaddrfilters-or-addressesnoannounce)
@@ -22,6 +24,38 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
24
25 ### 🔦 Highlights
26
27 +#### 🎯 Announce CIDs on demand with `ipfs provide once`
28 +
29 +`ipfs provide once <cid>...` announces CIDs to the routing system immediately, without waiting for the next scheduled reprovide. Use it when you want fine-grained control over when specific CIDs are announced.
30 +
31 +CIDs can be streamed in on stdin, so you can pipe arbitrarily large lists without growing daemon memory:
32 +
33 +```sh
34 +# Announce every locally pinned CID.
35 +ipfs pin ls | awk '{print $1}' | ipfs provide once
36 +```
37 +
38 +```sh
39 +# Announce every block reachable from a root (here, ~350 GiB of Wikipedia).
40 +ipfs refs -r bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze | ipfs provide once
41 +```
42 +
43 +In a terminal, the command shows a running count of queued CIDs. With `--enc=json` it emits one `{"Queued":"<cid>"}` line per CID, so downstream scripts can consume events as they arrive.
44 +
45 +`ipfs routing provide` keeps working but is deprecated. See `ipfs provide once --help` for usage and migration notes.
46 +
47 +#### ⚙️ `Provide.DHT.Interval=0` no longer disables providing
48 +
49 +`Provide.DHT.Interval=0` now disables only the periodic reprovide schedule. New CIDs still announce via fast-provide-root and `ipfs provide once`. To fully disable providing, set [`Provide.Enabled=false`](https://github.com/ipfs/kubo/blob/master/docs/config.md#provideenabled).
50 +
51 +> [!IMPORTANT]
52 +> The daemon now refuses to start when `Provide.DHT.Interval=0` is set without an explicit [`Provide.Enabled`](https://github.com/ipfs/kubo/blob/master/docs/config.md#provideenabled). Operators upgrading from an earlier kubo version must opt in to one of the two semantics:
53 +>
54 +> - `Provide.Enabled=false` to fully disable providing (the previous behaviour of `Interval=0`).
55 +> - `Provide.Enabled=true` to keep ad-hoc providing while skipping the periodic reprovide schedule.
56 +>
57 +> The startup error names both options. Pick the one that matches your intent.
58 +
59 #### 🐛 Fixed pin operations hanging under pinned reprovide strategies
60
61 `ipfs pin ls`, `ipfs add`, and other pin-touching operations could block for hours on nodes running with [`Provide.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy) set to `pinned`, `roots`, or `pinned+mfs` (including `+unique` / `+entities` variants). The pin index held a read lock for the entire reprovide cycle, which on large pinsets takes many hours. Any pin operation issued during that window blocked, and further `pin ls` / `ipfs add` calls piled up behind it until the cycle finished.
docs/config.md
+14 -7
@@ -2380,14 +2380,21 @@ interval (common with large datasets), the next cycle is skipped and provider
2380 records may expire.
2381
2382 - If unset, it uses the implicit safe default.
2383 -- If set to the value `"0"` it will disable content reproviding to DHT.
2383 +- If set to `"0"`, the periodic reprovide schedule is disabled. New CIDs are
2384 + still announced immediately via fast-provide-root and `ipfs provide once`.
2385
2386 > [!CAUTION]
2386 -> Disabling this will prevent other nodes from discovering your content via the DHT.
2387 -> Your node will stop announcing data to the DHT, making it
2388 -> inaccessible unless peers connect to you directly. Since provider
2389 -> records expire after `amino.DefaultProvideValidity`, your content will become undiscoverable
2390 -> after this period.
2387 +> `Interval=0` disables only the periodic refresh, not announcements of new
2388 +> content. Once provider records expire after `amino.DefaultProvideValidity`,
2389 +> the affected CIDs become undiscoverable to peers that did not retrieve them
2390 +> within that window. To fully disable providing, set
2391 +> [`Provide.Enabled=false`](#provideenabled) instead.
2392 +
2393 +> [!IMPORTANT]
2394 +> When `Interval=0`, [`Provide.Enabled`](#provideenabled) must be set
2395 +> explicitly. The daemon refuses to start otherwise. This prevents silent
2396 +> behaviour change on upgrade for operators who previously relied on
2397 +> `Interval=0` as a master kill-switch.
2398
2399 Default: `22h`
2400
@@ -2583,7 +2590,7 @@ Number of workers dedicated to burst provides. Only applies when `Provide.DHT.Sw
2590
2591 Burst provides are triggered by:
2592
2586 -- Manual provide commands (`ipfs routing provide`)
2593 +- Manual provide commands (`ipfs provide once`)
2594 - New content matching your `Provide.Strategy` (blocks from `ipfs add`, bitswap, or trustless gateway requests)
2595 - Catch-up reprovides after being disconnected/offline for a while
2596
docs/experimental-features.md
+1 -1
@@ -507,7 +507,7 @@ ones. This heuristic approach can significantly speed up the process, resulting
507 When it is enabled:
508
509 - Amino DHT provide operations should complete much faster than with it disabled
510 -- This can be tested with commands such as `ipfs routing provide`
510 +- This can be tested with commands such as `ipfs provide once`
511
512 **Tradeoffs**
513
go.mod
+1 -1
@@ -92,6 +92,7 @@ require (
92 golang.org/x/mod v0.35.0
93 golang.org/x/sync v0.20.0
94 golang.org/x/sys v0.44.0
95 + golang.org/x/term v0.43.0
96 google.golang.org/protobuf v1.36.11
97 )
98
@@ -259,7 +260,6 @@ require (
260 golang.org/x/net v0.53.0 // indirect
261 golang.org/x/oauth2 v0.36.0 // indirect
262 golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect
262 - golang.org/x/term v0.43.0 // indirect
263 golang.org/x/text v0.36.0 // indirect
264 golang.org/x/time v0.14.0 // indirect
265 golang.org/x/tools v0.44.0 // indirect
test/cli/provide_stats_test.go
+6 -3
@@ -506,7 +506,7 @@ func TestProvideStatDisabledConfig(t *testing.T) {
506 assert.Contains(t, res.Stderr.String(), "stats not available")
507 })
508
509 - t.Run("Provide.Enabled=true with Provide.DHT.Interval=0 returns error stats not available", func(t *testing.T) {
509 + t.Run("Provide.Enabled=true with Provide.DHT.Interval=0 returns stats with zero schedule fields", func(t *testing.T) {
510 t.Parallel()
511
512 h := harness.NewT(t)
@@ -517,8 +517,11 @@ func TestProvideStatDisabledConfig(t *testing.T) {
517 node.StartDaemon()
518 defer node.StopDaemon()
519
520 + // Interval=0 disables only the periodic schedule; the provider
521 + // is still wired and 'provide stat' returns valid stats with
522 + // the schedule-related timing fields zeroed out.
523 res := node.RunIPFS("provide", "stat")
521 - assert.Error(t, res.Err)
522 - assert.Contains(t, res.Stderr.String(), "stats not available")
524 + assert.Equal(t, 0, res.ExitCode())
525 + assert.NotContains(t, res.Stderr.String(), "stats not available")
526 })
527 }
test/cli/provider_test.go
+225 -8
@@ -235,19 +235,241 @@ func runProviderSuite(t *testing.T, sweep bool, apply cfgApplier, awaitReprovide
235 assert.Equal(t, 0, res.ExitCode(), "Should succeed with exit code 0")
236 })
237
238 - // Right now Provide and Reprovide are tied together
239 - t.Run("Reprovide.Interval=0 disables announcement of new CID too", func(t *testing.T) {
238 + t.Run("ipfs provide once works when Provide.DHT.Interval=0", func(t *testing.T) {
239 t.Parallel()
240
241 nodes := initNodes(t, 2, func(n *harness.Node) {
242 + n.SetIPFSConfig("Provide.Enabled", true)
243 + // No periodic reprovide schedule; provide once is the only
244 + // way new content reaches peers in this configuration.
245 n.SetIPFSConfig("Provide.DHT.Interval", "0")
246 + n.SetIPFSConfig("Provide.Strategy", "roots")
247 + })
248 + defer nodes.StopDaemons()
249 +
250 + publisher := nodes[0]
251 + cid := publisher.IPFSAddStr(uniq("interval=0"), "--pin=false")
252 + expectNoProviders(t, cid, nodes[1:]...)
253 +
254 + res := publisher.RunIPFS("provide", "once", cid)
255 + assert.Equal(t, 0, res.ExitCode(), "provide once should succeed with Interval=0")
256 + expectProviders(t, cid, publisher.PeerID().String(), nodes[1:]...)
257 + })
258 +
259 + t.Run("Provide.Enabled=false disables ipfs provide once", func(t *testing.T) {
260 + t.Parallel()
261 +
262 + nodes := initNodes(t, 2, func(n *harness.Node) {
263 + n.SetIPFSConfig("Provide.Enabled", false)
264 })
265 defer nodes.StopDaemons()
266
267 cid := nodes[0].IPFSAddStr(time.Now().String())
268 + res := nodes[0].RunIPFS("provide", "once", cid)
269 + assert.Contains(t, res.Stderr.Trimmed(), "cannot provide: Provide.Enabled is false")
270 + assert.Equal(t, 1, res.ExitCode())
271 +
272 expectNoProviders(t, cid, nodes[1:]...)
273 })
274
275 + t.Run("ipfs provide once announces a CID and finds providers", func(t *testing.T) {
276 + t.Parallel()
277 +
278 + nodes := initNodes(t, 2, func(n *harness.Node) {
279 + n.SetIPFSConfig("Provide.Enabled", true)
280 + // "roots" so add-time providing is skipped and we know the
281 + // announcement comes from `provide once`, not from ipfs add.
282 + n.SetIPFSConfig("Provide.Strategy", "roots")
283 + })
284 + defer nodes.StopDaemons()
285 +
286 + cid := nodes[0].IPFSAddStr(uniq("provide once"), "--pin=false")
287 + expectNoProviders(t, cid, nodes[1:]...)
288 +
289 + res := nodes[0].RunIPFS("provide", "once", cid)
290 + assert.Equal(t, 0, res.ExitCode(), "provide once should succeed")
291 + expectProviders(t, cid, nodes[0].PeerID().String(), nodes[1:]...)
292 + })
293 +
294 + t.Run("ipfs provide once errors when CID is not in local blockstore", func(t *testing.T) {
295 + t.Parallel()
296 +
297 + nodes := initNodes(t, 1, func(n *harness.Node) {
298 + n.SetIPFSConfig("Provide.Enabled", true)
299 + })
300 + defer nodes.StopDaemons()
301 +
302 + // CID for content the node has never seen.
303 + missing := "bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy"
304 + res := nodes[0].RunIPFS("provide", "once", missing)
305 + assert.Contains(t, res.Stderr.Trimmed(), "not found locally, cannot provide")
306 + assert.Equal(t, 1, res.ExitCode())
307 + })
308 +
309 + t.Run("ipfs provide once --recursive announces every block in the DAG", func(t *testing.T) {
310 + t.Parallel()
311 +
312 + nodes := initNodes(t, 2, func(n *harness.Node) {
313 + n.SetIPFSConfig("Provide.Enabled", true)
314 + // Selective strategy + --pin=false below means nothing is
315 + // auto-provided; everything findable comes from `provide once`.
316 + n.SetIPFSConfig("Provide.Strategy", "roots")
317 + // 1 MiB chunks so a 2 MiB file produces multiple leaf blocks.
318 + n.SetIPFSConfig("Import.UnixFSChunker", "size-1048576")
319 + })
320 + defer nodes.StopDaemons()
321 +
322 + publisher := nodes[0]
323 + data := random.Bytes(2 * 1024 * 1024)
324 + cidRoot := publisher.IPFSAdd(bytes.NewReader(data), "-Q", "--pin=false")
325 +
326 + // Discover a chunk CID via the root's DAG links.
327 + dagOut := publisher.IPFS("dag", "get", cidRoot)
328 + var dagNode struct {
329 + Links []struct {
330 + Hash map[string]string `json:"Hash"`
331 + } `json:"Links"`
332 + }
333 + require.NoError(t, json.Unmarshal(dagOut.Stdout.Bytes(), &dagNode))
334 + require.Greater(t, len(dagNode.Links), 1, "2 MiB file with 1 MiB chunker should have multiple chunks")
335 + cidChunk := dagNode.Links[0].Hash["/"]
336 + require.NotEmpty(t, cidChunk)
337 +
338 + // Recursive provide should announce both the root and every chunk.
339 + res := publisher.RunIPFS("provide", "once", "-r", cidRoot)
340 + assert.Equal(t, 0, res.ExitCode(), "provide once -r should succeed")
341 + expectProviders(t, cidRoot, publisher.PeerID().String(), nodes[1:]...)
342 + expectProviders(t, cidChunk, publisher.PeerID().String(), nodes[1:]...)
343 + })
344 +
345 + t.Run("ipfs provide once accepts multiple CIDs and reports count", func(t *testing.T) {
346 + t.Parallel()
347 +
348 + nodes := initNodes(t, 2, func(n *harness.Node) {
349 + n.SetIPFSConfig("Provide.Enabled", true)
350 + n.SetIPFSConfig("Provide.Strategy", "roots")
351 + })
352 + defer nodes.StopDaemons()
353 +
354 + publisher := nodes[0]
355 + c1 := publisher.IPFSAddStr(uniq("multi 1"), "--pin=false")
356 + c2 := publisher.IPFSAddStr(uniq("multi 2"), "--pin=false")
357 + c3 := publisher.IPFSAddStr(uniq("multi 3"), "--pin=false")
358 +
359 + res := publisher.RunIPFS("provide", "once", c1, c2, c3)
360 + assert.Equal(t, 0, res.ExitCode(), "provide once with multiple CIDs should succeed")
361 + assert.Contains(t, res.Stdout.Trimmed(), "queued 3 CID(s) for immediate provide")
362 +
363 + expectProviders(t, c1, publisher.PeerID().String(), nodes[1:]...)
364 + expectProviders(t, c2, publisher.PeerID().String(), nodes[1:]...)
365 + expectProviders(t, c3, publisher.PeerID().String(), nodes[1:]...)
366 + })
367 +
368 + t.Run("ipfs provide once reads CIDs streamed from stdin", func(t *testing.T) {
369 + t.Parallel()
370 +
371 + nodes := initNodes(t, 2, func(n *harness.Node) {
372 + n.SetIPFSConfig("Provide.Enabled", true)
373 + n.SetIPFSConfig("Provide.Strategy", "roots")
374 + })
375 + defer nodes.StopDaemons()
376 +
377 + publisher := nodes[0]
378 + c1 := publisher.IPFSAddStr(uniq("stdin 1"), "--pin=false")
379 + c2 := publisher.IPFSAddStr(uniq("stdin 2"), "--pin=false")
380 + c3 := publisher.IPFSAddStr(uniq("stdin 3"), "--pin=false")
381 +
382 + res := publisher.Runner.Run(harness.RunRequest{
383 + Path: publisher.IPFSBin,
384 + Args: []string{"provide", "once"},
385 + CmdOpts: []harness.CmdOpt{
386 + harness.RunWithStdinStr(c1 + "\n" + c2 + "\n" + c3 + "\n"),
387 + },
388 + })
389 + assert.Equal(t, 0, res.ExitCode(), "provide once with stdin should succeed")
390 + assert.Contains(t, res.Stdout.Trimmed(), "queued 3 CID(s) for immediate provide")
391 +
392 + expectProviders(t, c1, publisher.PeerID().String(), nodes[1:]...)
393 + expectProviders(t, c2, publisher.PeerID().String(), nodes[1:]...)
394 + expectProviders(t, c3, publisher.PeerID().String(), nodes[1:]...)
395 + })
396 +
397 + t.Run("ipfs provide once deduplicates repeated CIDs", func(t *testing.T) {
398 + t.Parallel()
399 +
400 + nodes := initNodes(t, 1, func(n *harness.Node) {
401 + n.SetIPFSConfig("Provide.Enabled", true)
402 + n.SetIPFSConfig("Provide.Strategy", "roots")
403 + })
404 + defer nodes.StopDaemons()
405 +
406 + publisher := nodes[0]
407 + c1 := publisher.IPFSAddStr(uniq("dedup 1"), "--pin=false")
408 + c2 := publisher.IPFSAddStr(uniq("dedup 2"), "--pin=false")
409 +
410 + // 4 args, 2 unique CIDs. The repeated ones should not produce
411 + // extra events on the wire.
412 + res := publisher.RunIPFS("provide", "once", "--enc=json", c1, c2, c1, c2)
413 + assert.Equal(t, 0, res.ExitCode())
414 +
415 + var queued []string
416 + for line := range strings.Lines(res.Stdout.String()) {
417 + line = strings.TrimSpace(line)
418 + if line == "" {
419 + continue
420 + }
421 + var ev struct{ Queued string }
422 + require.NoError(t, json.Unmarshal([]byte(line), &ev))
423 + queued = append(queued, ev.Queued)
424 + }
425 + assert.ElementsMatch(t, []string{c1, c2}, queued, "duplicates should be filtered")
426 + })
427 +
428 + t.Run("ipfs provide once --enc=json streams one event per CID", func(t *testing.T) {
429 + t.Parallel()
430 +
431 + nodes := initNodes(t, 1, func(n *harness.Node) {
432 + n.SetIPFSConfig("Provide.Enabled", true)
433 + n.SetIPFSConfig("Provide.Strategy", "roots")
434 + })
435 + defer nodes.StopDaemons()
436 +
437 + publisher := nodes[0]
438 + c1 := publisher.IPFSAddStr(uniq("json 1"), "--pin=false")
439 + c2 := publisher.IPFSAddStr(uniq("json 2"), "--pin=false")
440 +
441 + res := publisher.RunIPFS("provide", "once", "--enc=json", c1, c2)
442 + assert.Equal(t, 0, res.ExitCode(), "provide once --enc=json should succeed")
443 +
444 + // Parse one JSON object per non-empty line.
445 + var queued []string
446 + for line := range strings.Lines(res.Stdout.String()) {
447 + line = strings.TrimSpace(line)
448 + if line == "" {
449 + continue
450 + }
451 + var ev struct{ Queued string }
452 + require.NoError(t, json.Unmarshal([]byte(line), &ev), "each line must parse as JSON: %q", line)
453 + queued = append(queued, ev.Queued)
454 + }
455 + assert.ElementsMatch(t, []string{c1, c2}, queued)
456 + })
457 +
458 + t.Run("Provide.DHT.Interval=0 keeps announcing new CIDs (fast-provide-root)", func(t *testing.T) {
459 + t.Parallel()
460 +
461 + nodes := initNodes(t, 2, func(n *harness.Node) {
462 + // Required: Interval=0 alone is rejected by the validator
463 + // since the new semantic only disables the schedule.
464 + n.SetIPFSConfig("Provide.Enabled", true)
465 + n.SetIPFSConfig("Provide.DHT.Interval", "0")
466 + })
467 + defer nodes.StopDaemons()
468 +
469 + cid := nodes[0].IPFSAddStr(time.Now().String())
470 + expectProviders(t, cid, nodes[0].PeerID().String(), nodes[1:]...)
471 + })
472 +
473 // `routing reprovide` is only available with the legacy provider.
474 // Sweep provider reprovides automatically on schedule.
475 if !sweep {
@@ -255,19 +477,14 @@ func runProviderSuite(t *testing.T, sweep bool, apply cfgApplier, awaitReprovide
477 t.Parallel()
478
479 nodes := initNodes(t, 2, func(n *harness.Node) {
480 + n.SetIPFSConfig("Provide.Enabled", true)
481 n.SetIPFSConfig("Provide.DHT.Interval", "0")
482 })
483 defer nodes.StopDaemons()
484
262 - cid := nodes[0].IPFSAddStr(time.Now().String())
263 -
264 - expectNoProviders(t, cid, nodes[1:]...)
265 -
485 res := nodes[0].RunIPFS("routing", "reprovide")
486 assert.Contains(t, res.Stderr.Trimmed(), "invalid configuration: Provide.DHT.Interval is set to '0'")
487 assert.Equal(t, 1, res.ExitCode())
269 -
270 - expectNoProviders(t, cid, nodes[1:]...)
488 })
489
490 t.Run("Manual Reprovide trigger does not work when Provide system is disabled", func(t *testing.T) {