| 1 | package name |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/hex" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "strings" |
| 10 | "text/tabwriter" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/ipfs/boxo/ipns" |
| 14 | ipns_pb "github.com/ipfs/boxo/ipns/pb" |
| 15 | cmds "github.com/ipfs/go-ipfs-cmds" |
| 16 | "github.com/ipfs/kubo/core/commands/cmdenv" |
| 17 | "github.com/ipfs/kubo/core/coreiface/options" |
| 18 | "google.golang.org/protobuf/proto" |
| 19 | ) |
| 20 | |
| 21 | type IpnsEntry struct { |
| 22 | Name string |
| 23 | Value string |
| 24 | } |
| 25 | |
| 26 | var NameCmd = &cmds.Command{ |
| 27 | Helptext: cmds.HelpText{ |
| 28 | Tagline: "Publish and resolve IPNS names.", |
| 29 | ShortDescription: ` |
| 30 | IPNS is a PKI namespace, where names are the hashes of public keys, and |
| 31 | the private key enables publishing new (signed) values. In both publish |
| 32 | and resolve, the default name used is the node's own PeerID, |
| 33 | which is the hash of its public key. |
| 34 | `, |
| 35 | LongDescription: ` |
| 36 | IPNS is a PKI namespace, where names are the hashes of public keys, and |
| 37 | the private key enables publishing new (signed) values. In both publish |
| 38 | and resolve, the default name used is the node's own PeerID, |
| 39 | which is the hash of its public key. |
| 40 | |
| 41 | You can use the 'ipfs key' commands to list and generate more names and their |
| 42 | respective keys. |
| 43 | |
| 44 | Examples: |
| 45 | |
| 46 | Publish an <ipfs-path> with your default name: |
| 47 | |
| 48 | > ipfs name publish /ipfs/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4 |
| 49 | Published to k51qzi5uqu5dgklc20hksmmzhoy5lfrn5xcnryq6xp4r50b5yc0vnivpywfu9p: /ipfs/bafk... |
| 50 | |
| 51 | Publish an <ipfs-path> with another name, added by an 'ipfs key' command: |
| 52 | |
| 53 | > ipfs key gen --type=ed25519 mykey |
| 54 | k51qzi5uqu5dlz49qkb657myg6f1buu6rauv8c6b489a9i1e4dkt7a3yo9j2wr |
| 55 | > ipfs name publish --key=mykey /ipfs/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4 |
| 56 | Published to k51qzi5uqu5dlz49qkb657myg6f1buu6rauv8c6b489a9i1e4dkt7a3yo9j2wr: /ipfs/bafk... |
| 57 | |
| 58 | Resolve the value of your name: |
| 59 | |
| 60 | > ipfs name resolve |
| 61 | /ipfs/bafk... |
| 62 | |
| 63 | Resolve the value of another name: |
| 64 | |
| 65 | > ipfs name resolve k51qzi5uqu5dlz49qkb657myg6f1buu6rauv8c6b489a9i1e4dkt7a3yo9j2wr |
| 66 | /ipfs/bafk... |
| 67 | |
| 68 | Resolve the value of a dnslink: |
| 69 | |
| 70 | > ipfs name resolve specs.ipfs.tech |
| 71 | /ipfs/bafy... |
| 72 | |
| 73 | `, |
| 74 | }, |
| 75 | |
| 76 | Subcommands: map[string]*cmds.Command{ |
| 77 | "publish": PublishCmd, |
| 78 | "resolve": IpnsCmd, |
| 79 | "pubsub": IpnsPubsubCmd, |
| 80 | "inspect": IpnsInspectCmd, |
| 81 | "get": IpnsGetCmd, |
| 82 | "put": IpnsPutCmd, |
| 83 | }, |
| 84 | } |
| 85 | |
| 86 | type IpnsInspectValidation struct { |
| 87 | Valid bool |
| 88 | Reason string |
| 89 | Name string |
| 90 | } |
| 91 | |
| 92 | // IpnsInspectEntry contains the deserialized values from an IPNS Entry: |
| 93 | // https://github.com/ipfs/specs/blob/main/ipns/IPNS.md#record-serialization-format |
| 94 | type IpnsInspectEntry struct { |
| 95 | Value string |
| 96 | ValidityType *ipns.ValidityType |
| 97 | Validity *time.Time |
| 98 | Sequence *uint64 |
| 99 | TTL *time.Duration |
| 100 | } |
| 101 | |
| 102 | type IpnsInspectResult struct { |
| 103 | Entry IpnsInspectEntry |
| 104 | PbSize int |
| 105 | SignatureType string |
| 106 | HexDump string |
| 107 | Validation *IpnsInspectValidation |
| 108 | } |
| 109 | |
| 110 | var IpnsInspectCmd = &cmds.Command{ |
| 111 | Status: cmds.Experimental, |
| 112 | Helptext: cmds.HelpText{ |
| 113 | Tagline: "Inspects an IPNS Record", |
| 114 | ShortDescription: ` |
| 115 | Prints values inside of IPNS Record protobuf and its DAG-CBOR Data field. |
| 116 | Passing --verify will verify signature against provided public key. |
| 117 | `, |
| 118 | LongDescription: ` |
| 119 | Prints values inside of IPNS Record protobuf and its DAG-CBOR Data field. |
| 120 | |
| 121 | The input can be a file or STDIN, the output can be JSON: |
| 122 | |
| 123 | $ ipfs routing get "/ipns/$PEERID" > ipns_record |
| 124 | $ ipfs name inspect --enc=json < ipns_record |
| 125 | |
| 126 | Values in PublicKey, SignatureV1 and SignatureV2 fields are raw bytes encoded |
| 127 | in Multibase. The Data field is DAG-CBOR represented as DAG-JSON. |
| 128 | |
| 129 | Passing --verify will verify signature against provided public key. |
| 130 | |
| 131 | `, |
| 132 | HTTP: &cmds.HTTPHelpText{ |
| 133 | Description: "Request body should be `multipart/form-data` with the IPNS record bytes.", |
| 134 | }, |
| 135 | }, |
| 136 | Arguments: []cmds.Argument{ |
| 137 | cmds.FileArg("record", true, false, "The IPNS record payload to be verified.").EnableStdin(), |
| 138 | }, |
| 139 | Options: []cmds.Option{ |
| 140 | cmds.StringOption("verify", "CID of the public IPNS key to validate against."), |
| 141 | cmds.BoolOption("dump", "Include a full hex dump of the raw Protobuf record.").WithDefault(true), |
| 142 | }, |
| 143 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 144 | file, err := cmdenv.GetFileArg(req.Files.Entries()) |
| 145 | if err != nil { |
| 146 | return err |
| 147 | } |
| 148 | defer file.Close() |
| 149 | |
| 150 | var b bytes.Buffer |
| 151 | |
| 152 | _, err = io.Copy(&b, file) |
| 153 | if err != nil { |
| 154 | return err |
| 155 | } |
| 156 | |
| 157 | rec, err := ipns.UnmarshalRecord(b.Bytes()) |
| 158 | if err != nil { |
| 159 | return err |
| 160 | } |
| 161 | |
| 162 | result := &IpnsInspectResult{ |
| 163 | Entry: IpnsInspectEntry{}, |
| 164 | } |
| 165 | |
| 166 | // Best effort to get the fields. Show everything we can. |
| 167 | if v, err := rec.Value(); err == nil { |
| 168 | result.Entry.Value = v.String() |
| 169 | } |
| 170 | |
| 171 | if v, err := rec.ValidityType(); err == nil { |
| 172 | result.Entry.ValidityType = &v |
| 173 | } |
| 174 | |
| 175 | if v, err := rec.Validity(); err == nil { |
| 176 | result.Entry.Validity = &v |
| 177 | } |
| 178 | |
| 179 | if v, err := rec.Sequence(); err == nil { |
| 180 | result.Entry.Sequence = &v |
| 181 | } |
| 182 | |
| 183 | if v, err := rec.TTL(); err == nil { |
| 184 | result.Entry.TTL = &v |
| 185 | } |
| 186 | |
| 187 | // Here we need the raw protobuf just to decide the version. |
| 188 | var pbRecord ipns_pb.IpnsRecord |
| 189 | err = proto.Unmarshal(b.Bytes(), &pbRecord) |
| 190 | if err != nil { |
| 191 | return err |
| 192 | } |
| 193 | if len(pbRecord.SignatureV1) != 0 || len(pbRecord.Value) != 0 { |
| 194 | result.SignatureType = "V1+V2" |
| 195 | } else if pbRecord.Data != nil { |
| 196 | result.SignatureType = "V2" |
| 197 | } else { |
| 198 | result.SignatureType = "Unknown" |
| 199 | } |
| 200 | result.PbSize = proto.Size(&pbRecord) |
| 201 | |
| 202 | if verify, ok := req.Options["verify"].(string); ok { |
| 203 | name, err := ipns.NameFromString(verify) |
| 204 | if err != nil { |
| 205 | return err |
| 206 | } |
| 207 | |
| 208 | result.Validation = &IpnsInspectValidation{ |
| 209 | Name: name.String(), |
| 210 | } |
| 211 | |
| 212 | err = ipns.ValidateWithName(rec, name) |
| 213 | if err == nil { |
| 214 | result.Validation.Valid = true |
| 215 | } else { |
| 216 | result.Validation.Reason = err.Error() |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | if dump, ok := req.Options["dump"].(bool); ok && dump { |
| 221 | result.HexDump = hex.Dump(b.Bytes()) |
| 222 | } |
| 223 | |
| 224 | return cmds.EmitOnce(res, result) |
| 225 | }, |
| 226 | Type: IpnsInspectResult{}, |
| 227 | Encoders: cmds.EncoderMap{ |
| 228 | cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *IpnsInspectResult) error { |
| 229 | tw := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0) |
| 230 | defer tw.Flush() |
| 231 | |
| 232 | if out.Entry.Value != "" { |
| 233 | fmt.Fprintf(tw, "Value:\t%q\n", out.Entry.Value) |
| 234 | } |
| 235 | |
| 236 | if out.Entry.ValidityType != nil { |
| 237 | fmt.Fprintf(tw, "Validity Type:\t%d\n", *out.Entry.ValidityType) |
| 238 | } |
| 239 | |
| 240 | if out.Entry.Validity != nil { |
| 241 | fmt.Fprintf(tw, "Validity:\t%q\n", out.Entry.Validity.Format(time.RFC3339Nano)) |
| 242 | } |
| 243 | |
| 244 | if out.Entry.Sequence != nil { |
| 245 | fmt.Fprintf(tw, "Sequence:\t%d\n", *out.Entry.Sequence) |
| 246 | } |
| 247 | |
| 248 | if out.Entry.TTL != nil { |
| 249 | fmt.Fprintf(tw, "TTL:\t%s\n", out.Entry.TTL.String()) |
| 250 | } |
| 251 | |
| 252 | fmt.Fprintf(tw, "Protobuf Size:\t%d\n", out.PbSize) |
| 253 | fmt.Fprintf(tw, "Signature Type:\t%s\n", out.SignatureType) |
| 254 | |
| 255 | if out.Validation == nil { |
| 256 | tw.Flush() |
| 257 | fmt.Fprintf(w, "\nThis record was not verified. Pass '--verify k51...' to verify.\n") |
| 258 | } else { |
| 259 | tw.Flush() |
| 260 | fmt.Fprintf(w, "\nValidation results:\n") |
| 261 | |
| 262 | fmt.Fprintf(tw, "\tValid:\t%v\n", out.Validation.Valid) |
| 263 | if out.Validation.Reason != "" { |
| 264 | fmt.Fprintf(tw, "\tReason:\t%s\n", out.Validation.Reason) |
| 265 | } |
| 266 | fmt.Fprintf(tw, "\tName:\t%s\n", out.Validation.Name) |
| 267 | } |
| 268 | |
| 269 | if out.HexDump != "" { |
| 270 | tw.Flush() |
| 271 | |
| 272 | fmt.Fprintf(w, "\nHex Dump:\n%s", out.HexDump) |
| 273 | } |
| 274 | |
| 275 | return nil |
| 276 | }), |
| 277 | }, |
| 278 | } |
| 279 | |
| 280 | var IpnsGetCmd = &cmds.Command{ |
| 281 | Status: cmds.Experimental, |
| 282 | Helptext: cmds.HelpText{ |
| 283 | Tagline: "Retrieve a signed IPNS record.", |
| 284 | ShortDescription: ` |
| 285 | Retrieves the signed IPNS record for a given name from the routing system. |
| 286 | |
| 287 | The output is the raw IPNS record (protobuf) as defined in the IPNS spec: |
| 288 | https://specs.ipfs.tech/ipns/ipns-record/ |
| 289 | |
| 290 | The record can be inspected with 'ipfs name inspect': |
| 291 | |
| 292 | ipfs name get <name> | ipfs name inspect |
| 293 | |
| 294 | This is equivalent to 'ipfs routing get /ipns/<name>' but only accepts |
| 295 | IPNS names (not arbitrary routing keys). |
| 296 | |
| 297 | Note: The routing system returns the "best" IPNS record it knows about. |
| 298 | For IPNS, "best" means the record with the highest sequence number. |
| 299 | If multiple records exist (e.g., after using 'ipfs name put'), this command |
| 300 | returns the one the routing system considers most current. |
| 301 | `, |
| 302 | HTTP: &cmds.HTTPHelpText{ |
| 303 | ResponseContentType: "application/vnd.ipfs.ipns-record", |
| 304 | }, |
| 305 | }, |
| 306 | Arguments: []cmds.Argument{ |
| 307 | cmds.StringArg("name", true, false, "The IPNS name to look up."), |
| 308 | }, |
| 309 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 310 | api, err := cmdenv.GetApi(env, req) |
| 311 | if err != nil { |
| 312 | return err |
| 313 | } |
| 314 | |
| 315 | // Normalize the argument: accept both "k51..." and "/ipns/k51..." |
| 316 | name := req.Arguments[0] |
| 317 | if !strings.HasPrefix(name, "/ipns/") { |
| 318 | name = "/ipns/" + name |
| 319 | } |
| 320 | |
| 321 | data, err := api.Routing().Get(req.Context, name) |
| 322 | if err != nil { |
| 323 | return err |
| 324 | } |
| 325 | |
| 326 | res.SetEncodingType(cmds.OctetStream) |
| 327 | res.SetContentType("application/vnd.ipfs.ipns-record") |
| 328 | return res.Emit(bytes.NewReader(data)) |
| 329 | }, |
| 330 | } |
| 331 | |
| 332 | const ( |
| 333 | forceOptionName = "force" |
| 334 | putAllowOfflineOption = "allow-offline" |
| 335 | allowDelegatedOption = "allow-delegated" |
| 336 | putQuietOptionName = "quiet" |
| 337 | maxIPNSRecordSize = 10 << 10 // 10 KiB per IPNS spec |
| 338 | ) |
| 339 | |
| 340 | var errPutAllowOffline = errors.New("can't put while offline: pass `--allow-offline` to store locally or `--allow-delegated` if Ipns.DelegatedPublishers are set up") |
| 341 | |
| 342 | var IpnsPutCmd = &cmds.Command{ |
| 343 | Status: cmds.Experimental, |
| 344 | Helptext: cmds.HelpText{ |
| 345 | Tagline: "Store a pre-signed IPNS record in the routing system.", |
| 346 | ShortDescription: ` |
| 347 | Stores a pre-signed IPNS record in the routing system. |
| 348 | |
| 349 | This command accepts a raw IPNS record (protobuf) as defined in the IPNS spec: |
| 350 | https://specs.ipfs.tech/ipns/ipns-record/ |
| 351 | |
| 352 | The record must be signed by the private key corresponding to the IPNS name. |
| 353 | Use 'ipfs name get' to retrieve records and 'ipfs name inspect' to examine. |
| 354 | `, |
| 355 | LongDescription: ` |
| 356 | Stores a pre-signed IPNS record in the routing system. |
| 357 | |
| 358 | This command accepts a raw IPNS record (protobuf) as defined in the IPNS spec: |
| 359 | https://specs.ipfs.tech/ipns/ipns-record/ |
| 360 | |
| 361 | The record must be signed by the private key corresponding to the IPNS name. |
| 362 | Use 'ipfs name get' to retrieve records and 'ipfs name inspect' to examine. |
| 363 | |
| 364 | Use Cases: |
| 365 | |
| 366 | - Re-publishing third-party records: store someone else's signed record |
| 367 | - Cross-node sync: import records exported from another node |
| 368 | - Backup/restore: export with 'name get', restore with 'name put' |
| 369 | |
| 370 | Validation: |
| 371 | |
| 372 | By default, the command validates that: |
| 373 | |
| 374 | - The record is a valid IPNS record (protobuf) |
| 375 | - The record size is within 10 KiB limit |
| 376 | - The signature matches the provided IPNS name |
| 377 | - The record's sequence number is higher than any existing record |
| 378 | (identical records are allowed for republishing) |
| 379 | |
| 380 | The --force flag skips this command's validation and passes the record |
| 381 | directly to the routing system. Note that --force only affects this command; |
| 382 | it does not control how the routing system handles the record. The routing |
| 383 | system may still reject invalid records or prefer records with higher sequence |
| 384 | numbers. Use --force primarily for testing (e.g., to observe how the routing |
| 385 | system reacts to incorrectly signed or malformed records). |
| 386 | |
| 387 | Important: Even after a successful 'name put', a subsequent 'name get' may |
| 388 | return a different record if one with a higher sequence number exists. |
| 389 | This is expected IPNS behavior, not a bug. |
| 390 | |
| 391 | Publishing Modes: |
| 392 | |
| 393 | By default, IPNS records are published to both the DHT and any configured |
| 394 | HTTP delegated publishers. You can control this behavior with: |
| 395 | |
| 396 | --allow-offline Store locally without requiring network connectivity |
| 397 | --allow-delegated Publish via HTTP delegated publishers only (no DHT) |
| 398 | |
| 399 | Examples: |
| 400 | |
| 401 | Export and re-import a record: |
| 402 | |
| 403 | > ipfs name get k51... > record.bin |
| 404 | > ipfs name put k51... record.bin |
| 405 | |
| 406 | Store a record received from someone else: |
| 407 | |
| 408 | > ipfs name put k51... third-party-record.bin |
| 409 | |
| 410 | Force store a record to test routing validation: |
| 411 | |
| 412 | > ipfs name put --force k51... possibly-invalid-record.bin |
| 413 | `, |
| 414 | HTTP: &cmds.HTTPHelpText{ |
| 415 | Description: "Request body should be `multipart/form-data` with the IPNS record bytes.", |
| 416 | }, |
| 417 | }, |
| 418 | Arguments: []cmds.Argument{ |
| 419 | cmds.StringArg("name", true, false, "The IPNS name to store the record for (e.g., k51... or /ipns/k51...)."), |
| 420 | cmds.FileArg("record", true, false, "Path to file containing the signed IPNS record.").EnableStdin(), |
| 421 | }, |
| 422 | Options: []cmds.Option{ |
| 423 | cmds.BoolOption(forceOptionName, "f", "Skip validation (signature, sequence, size)."), |
| 424 | cmds.BoolOption(putAllowOfflineOption, "Store locally without broadcasting to the network."), |
| 425 | cmds.BoolOption(allowDelegatedOption, "Publish via HTTP delegated publishers only (no DHT)."), |
| 426 | cmds.BoolOption(putQuietOptionName, "q", "Write no output."), |
| 427 | }, |
| 428 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 429 | nd, err := cmdenv.GetNode(env) |
| 430 | if err != nil { |
| 431 | return err |
| 432 | } |
| 433 | |
| 434 | api, err := cmdenv.GetApi(env, req) |
| 435 | if err != nil { |
| 436 | return err |
| 437 | } |
| 438 | |
| 439 | // Parse options |
| 440 | force, _ := req.Options[forceOptionName].(bool) |
| 441 | allowOffline, _ := req.Options[putAllowOfflineOption].(bool) |
| 442 | allowDelegated, _ := req.Options[allowDelegatedOption].(bool) |
| 443 | |
| 444 | // Validate flag combinations |
| 445 | if allowOffline && allowDelegated { |
| 446 | return errors.New("cannot use both --allow-offline and --allow-delegated flags") |
| 447 | } |
| 448 | |
| 449 | // Handle different publishing modes |
| 450 | if allowDelegated { |
| 451 | // AllowDelegated mode: check if delegated publishers are configured |
| 452 | cfg, err := nd.Repo.Config() |
| 453 | if err != nil { |
| 454 | return fmt.Errorf("failed to read config: %w", err) |
| 455 | } |
| 456 | delegatedPublishers := cfg.DelegatedPublishersWithAutoConf() |
| 457 | if len(delegatedPublishers) == 0 { |
| 458 | return errors.New("no delegated publishers configured: add Ipns.DelegatedPublishers or use --allow-offline for local-only publishing") |
| 459 | } |
| 460 | // For allow-delegated mode, we proceed even if offline |
| 461 | // since we're using HTTP publishing via delegated publishers |
| 462 | } |
| 463 | |
| 464 | // Parse the IPNS name argument |
| 465 | nameArg := req.Arguments[0] |
| 466 | if !strings.HasPrefix(nameArg, "/ipns/") { |
| 467 | nameArg = "/ipns/" + nameArg |
| 468 | } |
| 469 | // Extract the name part after /ipns/ |
| 470 | namePart := strings.TrimPrefix(nameArg, "/ipns/") |
| 471 | name, err := ipns.NameFromString(namePart) |
| 472 | if err != nil { |
| 473 | return fmt.Errorf("invalid IPNS name: %w", err) |
| 474 | } |
| 475 | |
| 476 | // Read raw record bytes from file/stdin |
| 477 | file, err := cmdenv.GetFileArg(req.Files.Entries()) |
| 478 | if err != nil { |
| 479 | return err |
| 480 | } |
| 481 | defer file.Close() |
| 482 | |
| 483 | // Read record data (limit to 1 MiB for memory safety) |
| 484 | data, err := io.ReadAll(io.LimitReader(file, 1<<20)) |
| 485 | if err != nil { |
| 486 | return fmt.Errorf("failed to read record: %w", err) |
| 487 | } |
| 488 | if len(data) == 0 { |
| 489 | return errors.New("record is empty") |
| 490 | } |
| 491 | |
| 492 | // Validate unless --force |
| 493 | if !force { |
| 494 | // Check size limit per IPNS spec |
| 495 | if len(data) > maxIPNSRecordSize { |
| 496 | return fmt.Errorf("record exceeds maximum size of %d bytes, use --force to skip size check", maxIPNSRecordSize) |
| 497 | } |
| 498 | rec, err := ipns.UnmarshalRecord(data) |
| 499 | if err != nil { |
| 500 | return fmt.Errorf("invalid IPNS record: %w", err) |
| 501 | } |
| 502 | |
| 503 | // Validate signature against provided name |
| 504 | err = ipns.ValidateWithName(rec, name) |
| 505 | if err != nil { |
| 506 | return fmt.Errorf("record validation failed: %w", err) |
| 507 | } |
| 508 | |
| 509 | // Check for sequence conflicts with existing record |
| 510 | existingData, err := api.Routing().Get(req.Context, nameArg) |
| 511 | if err == nil { |
| 512 | // Allow republishing the exact same record (common use case: |
| 513 | // get a third-party record and put it back to refresh DHT) |
| 514 | if !bytes.Equal(existingData, data) { |
| 515 | existingRec, parseErr := ipns.UnmarshalRecord(existingData) |
| 516 | if parseErr == nil { |
| 517 | existingSeq, seqErr := existingRec.Sequence() |
| 518 | newSeq, newSeqErr := rec.Sequence() |
| 519 | if seqErr == nil && newSeqErr == nil && existingSeq >= newSeq { |
| 520 | return fmt.Errorf("existing IPNS record has sequence %d >= new record sequence %d, use 'ipfs name put --force' to skip this check", existingSeq, newSeq) |
| 521 | } |
| 522 | } |
| 523 | } |
| 524 | } |
| 525 | // If Get fails (no existing record), that's fine - proceed with put |
| 526 | } |
| 527 | |
| 528 | // Publish the original bytes as-is |
| 529 | // When allowDelegated is true, we set allowOffline to allow the operation |
| 530 | // even without DHT connectivity (delegated publishers use HTTP) |
| 531 | opts := []options.RoutingPutOption{ |
| 532 | options.Routing.AllowOffline(allowOffline || allowDelegated), |
| 533 | } |
| 534 | |
| 535 | err = api.Routing().Put(req.Context, nameArg, data, opts...) |
| 536 | if err != nil { |
| 537 | if err.Error() == "can't put while offline" { |
| 538 | return errPutAllowOffline |
| 539 | } |
| 540 | return err |
| 541 | } |
| 542 | |
| 543 | // Extract value from the record for the response |
| 544 | value := "" |
| 545 | if rec, err := ipns.UnmarshalRecord(data); err == nil { |
| 546 | if v, err := rec.Value(); err == nil { |
| 547 | value = v.String() |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | return cmds.EmitOnce(res, &IpnsEntry{ |
| 552 | Name: name.String(), |
| 553 | Value: value, |
| 554 | }) |
| 555 | }, |
| 556 | Encoders: cmds.EncoderMap{ |
| 557 | cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, ie *IpnsEntry) error { |
| 558 | quiet, _ := req.Options[putQuietOptionName].(bool) |
| 559 | if quiet { |
| 560 | return nil |
| 561 | } |
| 562 | _, err := fmt.Fprintln(w, cmdenv.EscNonPrint(ie.Name)) |
| 563 | return err |
| 564 | }), |
| 565 | }, |
| 566 | Type: IpnsEntry{}, |
| 567 | } |