| 1 | package name |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "time" |
| 8 | |
| 9 | cmdenv "github.com/ipfs/kubo/core/commands/cmdenv" |
| 10 | "github.com/ipfs/kubo/core/commands/cmdutils" |
| 11 | |
| 12 | ipns "github.com/ipfs/boxo/ipns" |
| 13 | cmds "github.com/ipfs/go-ipfs-cmds" |
| 14 | ke "github.com/ipfs/kubo/core/commands/keyencode" |
| 15 | iface "github.com/ipfs/kubo/core/coreiface" |
| 16 | options "github.com/ipfs/kubo/core/coreiface/options" |
| 17 | ) |
| 18 | |
| 19 | var errAllowOffline = errors.New("can't publish while offline: pass `--allow-offline` to override or `--allow-delegated` if Ipns.DelegatedPublishers are set up") |
| 20 | |
| 21 | const ( |
| 22 | ipfsPathOptionName = "ipfs-path" |
| 23 | resolveOptionName = "resolve" |
| 24 | allowOfflineOptionName = "allow-offline" |
| 25 | allowDelegatedOptionName = "allow-delegated" |
| 26 | lifeTimeOptionName = "lifetime" |
| 27 | ttlOptionName = "ttl" |
| 28 | keyOptionName = "key" |
| 29 | quieterOptionName = "quieter" |
| 30 | v1compatOptionName = "v1compat" |
| 31 | sequenceOptionName = "sequence" |
| 32 | ) |
| 33 | |
| 34 | var PublishCmd = &cmds.Command{ |
| 35 | Helptext: cmds.HelpText{ |
| 36 | Tagline: "Publish IPNS names.", |
| 37 | ShortDescription: ` |
| 38 | IPNS is a PKI namespace, where names are the hashes of public keys, and |
| 39 | the private key enables publishing new (signed) values. In both publish |
| 40 | and resolve, the default name used is the node's own PeerID, |
| 41 | which is the hash of its public key. |
| 42 | `, |
| 43 | LongDescription: ` |
| 44 | IPNS is a PKI namespace, where names are the hashes of public keys, and |
| 45 | the private key enables publishing new (signed) values. In both publish |
| 46 | and resolve, the default name used is the node's own PeerID, |
| 47 | which is the hash of its public key. |
| 48 | |
| 49 | You can use the 'ipfs key' commands to list and generate more names and their |
| 50 | respective keys. |
| 51 | |
| 52 | Publishing Modes: |
| 53 | |
| 54 | By default, IPNS records are published to both the DHT and any configured |
| 55 | HTTP delegated publishers. You can control this behavior with the following flags: |
| 56 | |
| 57 | --allow-offline Allow publishing when offline (publishes to local datastore, network operations are optional) |
| 58 | --allow-delegated Allow publishing without DHT connectivity (local + HTTP delegated publishers only) |
| 59 | |
| 60 | Examples: |
| 61 | |
| 62 | Publish an <ipfs-path> with your default name: |
| 63 | |
| 64 | > ipfs name publish /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy |
| 65 | Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy |
| 66 | |
| 67 | Publish without DHT (HTTP delegated publishers only): |
| 68 | |
| 69 | > ipfs name publish --allow-delegated /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy |
| 70 | Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy |
| 71 | |
| 72 | Publish when offline (local publish, network optional): |
| 73 | |
| 74 | > ipfs name publish --allow-offline /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy |
| 75 | Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy |
| 76 | |
| 77 | Notes: |
| 78 | |
| 79 | The --ttl option specifies the time duration for caching IPNS records. |
| 80 | Lower values like '1m' enable faster updates but increase network load, |
| 81 | while the default of 1 hour reduces traffic but may delay propagation. |
| 82 | Gateway operators may override this with Ipns.MaxCacheTTL configuration. |
| 83 | |
| 84 | The --sequence option sets a custom sequence number for the IPNS record. |
| 85 | The sequence number must be monotonically increasing (greater than the |
| 86 | current record's sequence). This is useful for manually coordinating |
| 87 | updates across multiple writers. If not specified, the sequence number |
| 88 | increments automatically. |
| 89 | |
| 90 | For faster IPNS updates, consider: |
| 91 | - Using a lower --ttl value (e.g., '1m' for quick updates) |
| 92 | - Enabling PubSub via Ipns.UsePubsub in the config |
| 93 | |
| 94 | `, |
| 95 | }, |
| 96 | |
| 97 | Arguments: []cmds.Argument{ |
| 98 | cmds.StringArg(ipfsPathOptionName, true, false, "ipfs path of the object to be published.").EnableStdin(), |
| 99 | }, |
| 100 | Options: []cmds.Option{ |
| 101 | cmds.StringOption(keyOptionName, "k", "Name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'.").WithDefault("self"), |
| 102 | cmds.BoolOption(resolveOptionName, "Check if the given path can be resolved before publishing.").WithDefault(true), |
| 103 | cmds.StringOption(lifeTimeOptionName, "t", `Time duration the signed record will be valid for. Accepts durations such as "300s", "1.5h" or "7d2h45m"`).WithDefault(ipns.DefaultRecordLifetime.String()), |
| 104 | cmds.StringOption(ttlOptionName, "Time duration hint, akin to --lifetime, indicating how long to cache this record before checking for updates.").WithDefault(ipns.DefaultRecordTTL.String()), |
| 105 | cmds.BoolOption(quieterOptionName, "Q", "Write only final IPNS Name encoded as CIDv1 (for use in /ipns content paths)."), |
| 106 | cmds.BoolOption(v1compatOptionName, "Produce a backward-compatible IPNS Record by including fields for both V1 and V2 signatures.").WithDefault(true), |
| 107 | cmds.BoolOption(allowOfflineOptionName, "Allow publishing when offline - publishes to local datastore without requiring network connectivity."), |
| 108 | cmds.BoolOption(allowDelegatedOptionName, "Allow publishing without DHT connectivity - uses local datastore and HTTP delegated publishers only."), |
| 109 | cmds.Uint64Option(sequenceOptionName, "Set a custom sequence number for the IPNS record (must be higher than current)."), |
| 110 | ke.OptionIPNSBase, |
| 111 | }, |
| 112 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 113 | api, err := cmdenv.GetApi(env, req) |
| 114 | if err != nil { |
| 115 | return err |
| 116 | } |
| 117 | |
| 118 | allowOffline, _ := req.Options[allowOfflineOptionName].(bool) |
| 119 | allowDelegated, _ := req.Options[allowDelegatedOptionName].(bool) |
| 120 | compatibleWithV1, _ := req.Options[v1compatOptionName].(bool) |
| 121 | kname, _ := req.Options[keyOptionName].(string) |
| 122 | |
| 123 | // Validate flag combinations |
| 124 | if allowOffline && allowDelegated { |
| 125 | return errors.New("cannot use both --allow-offline and --allow-delegated flags") |
| 126 | } |
| 127 | |
| 128 | validTimeOpt, _ := req.Options[lifeTimeOptionName].(string) |
| 129 | validTime, err := time.ParseDuration(validTimeOpt) |
| 130 | if err != nil { |
| 131 | return fmt.Errorf("error parsing lifetime option: %s", err) |
| 132 | } |
| 133 | |
| 134 | opts := []options.NamePublishOption{ |
| 135 | options.Name.AllowOffline(allowOffline), |
| 136 | options.Name.AllowDelegated(allowDelegated), |
| 137 | options.Name.Key(kname), |
| 138 | options.Name.ValidTime(validTime), |
| 139 | options.Name.CompatibleWithV1(compatibleWithV1), |
| 140 | } |
| 141 | |
| 142 | if ttl, found := req.Options[ttlOptionName].(string); found { |
| 143 | d, err := time.ParseDuration(ttl) |
| 144 | if err != nil { |
| 145 | return err |
| 146 | } |
| 147 | |
| 148 | opts = append(opts, options.Name.TTL(d)) |
| 149 | } |
| 150 | |
| 151 | if sequence, found := req.Options[sequenceOptionName].(uint64); found { |
| 152 | opts = append(opts, options.Name.Sequence(sequence)) |
| 153 | } |
| 154 | |
| 155 | p, err := cmdutils.PathOrCidPath(req.Arguments[0]) |
| 156 | if err != nil { |
| 157 | return err |
| 158 | } |
| 159 | |
| 160 | if verifyExists, _ := req.Options[resolveOptionName].(bool); verifyExists { |
| 161 | _, err := api.ResolveNode(req.Context, p) |
| 162 | if err != nil { |
| 163 | return err |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | name, err := api.Name().Publish(req.Context, p, opts...) |
| 168 | if err != nil { |
| 169 | if err == iface.ErrOffline { |
| 170 | err = errAllowOffline |
| 171 | } |
| 172 | return err |
| 173 | } |
| 174 | |
| 175 | return cmds.EmitOnce(res, &IpnsEntry{ |
| 176 | Name: name.String(), |
| 177 | Value: p.String(), |
| 178 | }) |
| 179 | }, |
| 180 | Encoders: cmds.EncoderMap{ |
| 181 | cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, ie *IpnsEntry) error { |
| 182 | var err error |
| 183 | quieter, _ := req.Options[quieterOptionName].(bool) |
| 184 | if quieter { |
| 185 | _, err = fmt.Fprintln(w, cmdenv.EscNonPrint(ie.Name)) |
| 186 | } else { |
| 187 | _, err = fmt.Fprintf(w, "Published to %s: %s\n", cmdenv.EscNonPrint(ie.Name), cmdenv.EscNonPrint(ie.Value)) |
| 188 | } |
| 189 | return err |
| 190 | }), |
| 191 | }, |
| 192 | Type: IpnsEntry{}, |
| 193 | } |